bg
authorFrank DeMarco <frank.s.demarco@gmail.com>
Sun, 24 Aug 2014 13:57:07 +0000 (22:57 +0900)
committerFrank DeMarco <frank.s.demarco@gmail.com>
Sun, 24 Aug 2014 13:57:07 +0000 (22:57 +0900)
46 files changed:
.gitignore [new file with mode: 0644]
DejaVuSans-License.txt [new file with mode: 0644]
LGPL.txt [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
README [new file with mode: 0644]
config [new file with mode: 0644]
resource/DejaVuSans.ttf [new file with mode: 0644]
resource/aud/Amputator.xm [new file with mode: 0644]
resource/aud/Fine-Sheared-Mesh.xm [new file with mode: 0644]
resource/aud/Live-Burial.xm [new file with mode: 0644]
resource/img/Maltese/0.png [new file with mode: 0644]
resource/img/Maltese/1.png [new file with mode: 0644]
resource/img/Maltese/10.png [new file with mode: 0644]
resource/img/Maltese/2.png [new file with mode: 0644]
resource/img/Maltese/3.png [new file with mode: 0644]
resource/img/Maltese/4.png [new file with mode: 0644]
resource/img/Maltese/5.png [new file with mode: 0644]
resource/img/Maltese/6.png [new file with mode: 0644]
resource/img/Maltese/7.png [new file with mode: 0644]
resource/img/Maltese/8.png [new file with mode: 0644]
resource/img/Maltese/9.png [new file with mode: 0644]
resource/img/Scraps-ee59-h-Mh.png [new file with mode: 0644]
setup-win.py [new file with mode: 0644]
setup.py [new file with mode: 0644]
shaken-and-spit-out [new file with mode: 0755]
shaken_and_spit_out/SSO.py [new file with mode: 0644]
shaken_and_spit_out/__init__.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Animation.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Audio.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Configuration.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Delegate.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Display.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Game.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/GameChild.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Input.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Interpolator.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Mainloop.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Profile.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/ScreenGrabber.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Setup.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/SetupWin.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Sprite.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/TimeFilter.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/Vector.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/VideoRecorder.py [new file with mode: 0644]
shaken_and_spit_out/pgfw/__init__.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..e3d557f
--- /dev/null
@@ -0,0 +1,5 @@
+resource/local/
+*.pyc
+dist/
+MANIFEST
+*.tmp
diff --git a/DejaVuSans-License.txt b/DejaVuSans-License.txt
new file mode 100644 (file)
index 0000000..254e2cc
--- /dev/null
@@ -0,0 +1,99 @@
+Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
+Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
+
+Bitstream Vera Fonts Copyright
+------------------------------
+
+Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
+a trademark of Bitstream, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of the fonts accompanying this license ("Fonts") and associated
+documentation files (the "Font Software"), to reproduce and distribute the
+Font Software, including without limitation the rights to use, copy, merge,
+publish, distribute, and/or sell copies of the Font Software, and to permit
+persons to whom the Font Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright and trademark notices and this permission notice shall
+be included in all copies of one or more of the Font Software typefaces.
+
+The Font Software may be modified, altered, or added to, and in particular
+the designs of glyphs or characters in the Fonts may be modified and
+additional glyphs or characters may be added to the Fonts, only if the fonts
+are renamed to names not containing either the words "Bitstream" or the word
+"Vera".
+
+This License becomes null and void to the extent applicable to Fonts or Font
+Software that has been modified and is distributed under the "Bitstream
+Vera" names.
+
+The Font Software may be sold as part of a larger software package but no
+copy of one or more of the Font Software typefaces may be sold by itself.
+
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
+TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
+FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
+ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
+FONT SOFTWARE.
+
+Except as contained in this notice, the names of Gnome, the Gnome
+Foundation, and Bitstream Inc., shall not be used in advertising or
+otherwise to promote the sale, use or other dealings in this Font Software
+without prior written authorization from the Gnome Foundation or Bitstream
+Inc., respectively. For further information, contact: fonts at gnome dot
+org. 
+
+Arev Fonts Copyright
+------------------------------
+
+Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the fonts accompanying this license ("Fonts") and
+associated documentation files (the "Font Software"), to reproduce
+and distribute the modifications to the Bitstream Vera Font Software,
+including without limitation the rights to use, copy, merge, publish,
+distribute, and/or sell copies of the Font Software, and to permit
+persons to whom the Font Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright and trademark notices and this permission notice
+shall be included in all copies of one or more of the Font Software
+typefaces.
+
+The Font Software may be modified, altered, or added to, and in
+particular the designs of glyphs or characters in the Fonts may be
+modified and additional glyphs or characters may be added to the
+Fonts, only if the fonts are renamed to names not containing either
+the words "Tavmjong Bah" or the word "Arev".
+
+This License becomes null and void to the extent applicable to Fonts
+or Font Software that has been modified and is distributed under the 
+"Tavmjong Bah Arev" names.
+
+The Font Software may be sold as part of a larger software package but
+no copy of one or more of the Font Software typefaces may be sold by
+itself.
+
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
+TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
+
+Except as contained in this notice, the name of Tavmjong Bah shall not
+be used in advertising or otherwise to promote the sale, use or other
+dealings in this Font Software without prior written authorization
+from Tavmjong Bah. For further information, contact: tavmjong @ free
+. fr.
+
+$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $
diff --git a/LGPL.txt b/LGPL.txt
new file mode 100644 (file)
index 0000000..b1e3f5a
--- /dev/null
+++ b/LGPL.txt
@@ -0,0 +1,504 @@
+                 GNU LESSER GENERAL PUBLIC LICENSE
+                      Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                 GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                           NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..c8a1bb8
--- /dev/null
@@ -0,0 +1,3 @@
+include config
+recursive-include resource *
+prune resource/local
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..09bf6a3
--- /dev/null
+++ b/README
@@ -0,0 +1,37 @@
+Shaken and Spit Out
+^^^^^^^^^^^^^^^^^^^
+
+[[[
+It was a cybernetic adaptation that allowed it the predilection to
+attempt PSPPIT, continual establishment of foundation in the firmament
+]]]
+
+"Earth is hell, heaven is life"
+  - George Lee Quiñones
+
+Instructions
+~~~~~~~~~~~~
+Predict the path of an agitated particle by placing two points
+
+Keys
+~~~~
+LEFT-CLICK -- place head
+RIGHT-CLICK -- place tail
+
+License
+~~~~~~~
+Original source code and media are licensed to the public domain.  See
+http://creativecommons.org/publicdomain/zero/1.0/ for details.
+
+Python is Copyright © 2001-2014 Python Software Foundation and is
+distributed under the Python Software Foundation License.
+
+Pygame is distributed under the GNU Lesser General Public License.
+
+DejaVuSans is distributed under the Bitstream Vera Fonts Copyright and
+Arev Fonts Copyright.
+
+Further
+~~~~~~~
+http://A-O.in/
+http://twitter.com/Dometoerio/
diff --git a/config b/config
new file mode 100644 (file)
index 0000000..7ce1765
--- /dev/null
+++ b/config
@@ -0,0 +1,42 @@
+[setup]
+contact-name = Frank DeMarco
+license = Public Domain
+contact-email = frank.s.demarco@gmail.com
+title = Shaken and Spit Out
+url = http://A-O.in/
+summary = Predict the path of an agitated particle by placing two points
+platform = Linux
+version = 0.1.4
+init-script = shaken-and-spit-out
+package-root = shaken_and_spit_out
+
+[display]
+dimensions = 420, 700
+caption = Shaken and Spit Out
+font = DejaVuSans.ttf
+
+[mouse]
+visible = yes
+
+[keys]
+quit = K_ESCAPE
+
+[audio]
+title = aud/Fine-Sheared-Mesh.xm
+playing = aud/Live-Burial.xm
+game-over = aud/Amputator.xm
+
+[image]
+character = img/Scraps-ee59-h-Mh/
+background = img/Maltese/
+
+[text]
+preface = It was a cybernetic adaptation/that allowed it the predilection/to
+          attempt PSPPIT, continual/establishment of foundation in/the firmament
+intermediate = Good something/they that speak in proverbs say/come into the
+               city/let it be built/where the flies collect/on a live cadaver in
+               the road
+
+[unicode]
+
+[interpolate]
diff --git a/resource/DejaVuSans.ttf b/resource/DejaVuSans.ttf
new file mode 100644 (file)
index 0000000..9d40c32
Binary files /dev/null and b/resource/DejaVuSans.ttf differ
diff --git a/resource/aud/Amputator.xm b/resource/aud/Amputator.xm
new file mode 100644 (file)
index 0000000..77e6b35
Binary files /dev/null and b/resource/aud/Amputator.xm differ
diff --git a/resource/aud/Fine-Sheared-Mesh.xm b/resource/aud/Fine-Sheared-Mesh.xm
new file mode 100644 (file)
index 0000000..b04d168
Binary files /dev/null and b/resource/aud/Fine-Sheared-Mesh.xm differ
diff --git a/resource/aud/Live-Burial.xm b/resource/aud/Live-Burial.xm
new file mode 100644 (file)
index 0000000..62abca6
Binary files /dev/null and b/resource/aud/Live-Burial.xm differ
diff --git a/resource/img/Maltese/0.png b/resource/img/Maltese/0.png
new file mode 100644 (file)
index 0000000..2f89f84
Binary files /dev/null and b/resource/img/Maltese/0.png differ
diff --git a/resource/img/Maltese/1.png b/resource/img/Maltese/1.png
new file mode 100644 (file)
index 0000000..5be7484
Binary files /dev/null and b/resource/img/Maltese/1.png differ
diff --git a/resource/img/Maltese/10.png b/resource/img/Maltese/10.png
new file mode 100644 (file)
index 0000000..dd98dc8
Binary files /dev/null and b/resource/img/Maltese/10.png differ
diff --git a/resource/img/Maltese/2.png b/resource/img/Maltese/2.png
new file mode 100644 (file)
index 0000000..87f0621
Binary files /dev/null and b/resource/img/Maltese/2.png differ
diff --git a/resource/img/Maltese/3.png b/resource/img/Maltese/3.png
new file mode 100644 (file)
index 0000000..edf41ed
Binary files /dev/null and b/resource/img/Maltese/3.png differ
diff --git a/resource/img/Maltese/4.png b/resource/img/Maltese/4.png
new file mode 100644 (file)
index 0000000..78b1be0
Binary files /dev/null and b/resource/img/Maltese/4.png differ
diff --git a/resource/img/Maltese/5.png b/resource/img/Maltese/5.png
new file mode 100644 (file)
index 0000000..2acc279
Binary files /dev/null and b/resource/img/Maltese/5.png differ
diff --git a/resource/img/Maltese/6.png b/resource/img/Maltese/6.png
new file mode 100644 (file)
index 0000000..983cbde
Binary files /dev/null and b/resource/img/Maltese/6.png differ
diff --git a/resource/img/Maltese/7.png b/resource/img/Maltese/7.png
new file mode 100644 (file)
index 0000000..3d59ed7
Binary files /dev/null and b/resource/img/Maltese/7.png differ
diff --git a/resource/img/Maltese/8.png b/resource/img/Maltese/8.png
new file mode 100644 (file)
index 0000000..c1844c8
Binary files /dev/null and b/resource/img/Maltese/8.png differ
diff --git a/resource/img/Maltese/9.png b/resource/img/Maltese/9.png
new file mode 100644 (file)
index 0000000..de4c9af
Binary files /dev/null and b/resource/img/Maltese/9.png differ
diff --git a/resource/img/Scraps-ee59-h-Mh.png b/resource/img/Scraps-ee59-h-Mh.png
new file mode 100644 (file)
index 0000000..8ca2383
Binary files /dev/null and b/resource/img/Scraps-ee59-h-Mh.png differ
diff --git a/setup-win.py b/setup-win.py
new file mode 100644 (file)
index 0000000..e436c72
--- /dev/null
@@ -0,0 +1,4 @@
+from shaken_and_spit_out.pgfw.SetupWin import SetupWin
+
+if __name__ == "__main__":
+    SetupWin().setup()
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..a5bbdb4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,7 @@
+import os
+delattr(os, "link")
+
+from shaken_and_spit_out.pgfw.Setup import Setup
+
+if __name__ == "__main__":
+    Setup().setup()
diff --git a/shaken-and-spit-out b/shaken-and-spit-out
new file mode 100755 (executable)
index 0000000..492d94d
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+
+from os import environ, execvp, chdir, getcwd
+from os.path import exists, join, dirname
+from sys import version_info, argv
+
+def can_import(module_name):
+    try:
+        __import__(module_name)
+    except ImportError:
+        return False
+    else:
+        return True
+
+def is_python_3():
+    return version_info[0] >= 3
+
+def is_current_version(file_name):
+    version = map(int, file_name.replace("python", "").split("."))
+    return version == list(version_info)[:2]
+
+def launch_alternative(alternatives):
+    for alternative in alternatives:
+        if not is_current_version(alternative):
+            for root in environ["PATH"].split(":"):
+                if exists(join(root, alternative)):
+                    execvp(alternative, [alternative] + argv)
+
+def move_to_executable():
+    chdir(dirname(argv[0]))
+
+if is_python_3():
+    launch_alternative(["python2", "python2.7", "python2.6"])
+
+if not can_import("pygame"):
+    launch_alternative(["python2.7", "python2.6"])
+
+if "--go-to-dir" in argv:
+    move_to_executable()
+
+from shaken_and_spit_out.SSO import SSO
+
+SSO().run()
diff --git a/shaken_and_spit_out/SSO.py b/shaken_and_spit_out/SSO.py
new file mode 100644 (file)
index 0000000..2e04314
--- /dev/null
@@ -0,0 +1,74 @@
+from os import listdir
+from glob import glob
+from random import choice
+
+from pygame import cursors
+from pygame.mouse import set_cursor
+from pygame.font import Font
+from pygame.image import load
+from pygame.locals import *
+
+from shaken_and_spit_out.pgfw.Game import Game
+from shaken_and_spit_out.pgfw.GameChild import GameChild
+
+class SSO(Game):
+
+    def __init__(self):
+        Game.__init__(self)
+        set_cursor(*cursors.tri_left)
+        # string = u"\u2250\u2254\u2258"
+        # font = Font(self.get_resource("display", "font"), 24)
+        # self.message = font.render(string, True, (0, 255, 0))
+        self.backgrounds = [load(path) for path in \
+                            glob(self.get_resource("image", "background") + \
+                                 "*.png")]
+        self.used_backgrounds = []
+        self.set_random_background()
+        self.subscribe(self.respond)
+        self.title.activate()
+
+    def set_children(self):
+        Game.set_children(self)
+        self.title = Title(self)
+
+    def set_random_background(self):
+        index = choice(list(set(range(len(self.backgrounds))).\
+                            difference(self.used_backgrounds)))
+        self.used_backgrounds.append(index)
+        if len(self.used_backgrounds) > 3:
+            self.used_backgrounds.pop(0)
+        self.background = self.backgrounds[index]
+
+    def respond(self, event):
+        if self.delegate.compare(event, "reset-game"):
+            self.set_random_background()
+
+    def update(self):
+        self.get_screen().blit(self.background, (0, 0))
+        self.title.update()
+        # self.get_screen().fill((0, 0, 0))
+        # self.get_screen().blit(self.message, (100, 100))
+
+
+class Title(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.subscribe(self.respond)
+        self.subscribe(self.respond, MOUSEBUTTONDOWN)
+        self.deactivate()
+
+    def respond(self, event):
+        if self.active:
+            if event.type == MOUSEBUTTONDOWN and event.button == 1:
+                print "left-click"
+
+    def deactivate(self):
+        self.active = True
+
+    def activate(self):
+        self.active = True
+
+    def update(self):
+        if self.active:
+            pass
diff --git a/shaken_and_spit_out/__init__.py b/shaken_and_spit_out/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/shaken_and_spit_out/pgfw/Animation.py b/shaken_and_spit_out/pgfw/Animation.py
new file mode 100644 (file)
index 0000000..bd21ebb
--- /dev/null
@@ -0,0 +1,134 @@
+from GameChild import GameChild
+
+class Animation(GameChild):
+
+    def __init__(self, parent, method=None, interval=None, unfiltered=False):
+        GameChild.__init__(self, parent)
+        self.unfiltered = unfiltered
+        self.default_method = method or self.build_frame
+        self.accounts = {}
+        self.register(self.default_method, interval=interval)
+        self.current_elapsed = 0
+        self.last_update = 0
+
+    def build_frame(self):
+        pass
+
+    def register(self, *args, **kwargs):
+        interval = None
+        if kwargs.has_key("interval"):
+            interval = kwargs["interval"]
+        for method in args:
+            if method not in self.accounts:
+                self.accounts[method] = Account(interval, self)
+            else:
+                self.accounts[method].set_interval(interval)
+
+    def play(self, method=None, interval=None, delay=0, play_once=False,
+             **kwargs):
+        account = self.accounts[self.get_default(method)]
+        account.set_delay(delay)
+        account.set_args(kwargs)
+        account.set_play_once(play_once)
+        if interval:
+            account.set_interval(interval)
+        account.play()
+
+    def get_default(self, method):
+        if not method:
+            method = self.default_method
+        return method
+
+    def halt(self, method=None):
+        if not method:
+            for account in self.accounts.values():
+                account.halt()
+        else:
+            if self.accounts.has_key(method):
+                self.accounts[method].halt()
+
+    def is_playing(self, method=None, check_all=False, include_delay=False):
+        if check_all:
+            return any(self.is_account_playing(account, include_delay) for \
+                       method, account in self.accounts.iteritems())
+        return self.is_account_playing(self.accounts[self.get_default(method)],
+                                       include_delay)
+
+    def is_account_playing(self, account, include_delay):
+        return account.playing and (not include_delay or not account.delay)
+
+    def update(self):
+        for method, account in self.accounts.iteritems():
+            if account.update():
+                method(**account.args)
+
+
+class Account:
+
+    def __init__(self, interval, animation):
+        self.animation = animation
+        self.time_filter = animation.get_game().time_filter
+        self.set_interval(interval)
+        self.set_delay(0)
+        self.set_play_once(False)
+        self.interval_index = 0
+        self.last_frame = 0
+        self.halt()
+
+    def set_interval(self, interval):
+        if isinstance(interval, int) or isinstance(interval, str):
+            interval = [interval]
+        self.interval = interval
+
+    def set_delay(self, delay):
+        self.delay = delay
+
+    def set_play_once(self, play_once):
+        self.play_once = play_once
+
+    def set_args(self, args):
+        self.args = args
+
+    def play(self):
+        self.playing = True
+
+    def halt(self):
+        self.last_update = None
+        self.playing = False
+
+    def update(self):
+        if self.playing:
+            if self.animation.unfiltered:
+                ticks = self.time_filter.get_unfiltered_ticks()
+            else:
+                ticks = self.time_filter.get_ticks()
+            self.update_delay(ticks)
+            if not self.delay:
+                interval = self.interval
+                if interval:
+                    if ticks - self.last_frame < self.get_current_interval():
+                        return False
+                    self.last_frame = ticks
+                    self.increment_interval_index()
+                if self.play_once:
+                    self.halt()
+                return True
+
+    def get_current_interval(self):
+        return self.interval[self.interval_index]
+
+    def increment_interval_index(self):
+        index = self.interval_index + 1
+        if index >= len(self.interval):
+            index = 0
+        self.interval_index = index
+
+    def update_delay(self, ticks):
+        delay = self.delay
+        if delay > 0:
+            last_update = self.last_update or ticks
+            delay -= ticks - last_update
+            if delay < 0:
+                delay = 0
+        self.last_update = ticks
+        self.delay = delay
diff --git a/shaken_and_spit_out/pgfw/Audio.py b/shaken_and_spit_out/pgfw/Audio.py
new file mode 100644 (file)
index 0000000..2c230e9
--- /dev/null
@@ -0,0 +1,92 @@
+from os import listdir
+from os.path import join
+
+from pygame.mixer import Channel, Sound, music, find_channel
+
+from GameChild import *
+from Input import *
+
+class Audio(GameChild):
+
+    current_channel = None
+    paused = False
+    muted = False
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_fx()
+        self.subscribe(self.respond)
+
+    def load_fx(self):
+        fx = {}
+        if self.get_configuration().has_option("audio", "sfx-path"):
+            root = self.get_resource("audio", "sfx-path")
+            if root:
+                for name in listdir(root):
+                    fx[name.split(".")[0]] = Sound(join(root, name))
+        self.fx = fx
+
+    def respond(self, event):
+        if self.delegate.compare(event, "mute"):
+            self.mute()
+
+    def mute(self):
+        self.muted = True
+        self.set_volume()
+
+    def unmute(self):
+        self.muted = False
+        self.set_volume()
+
+    def set_volume(self):
+        volume = int(not self.muted)
+        music.set_volume(volume)
+        if self.current_channel:
+            self.current_channel.set_volume(volume)
+
+    def play_bgm(self, path, stream=False):
+        self.stop_current_channel()
+        if stream:
+            music.load(path)
+            music.play(-1)
+        else:
+            self.current_channel = Sound(path).play(-1)
+        self.set_volume()
+
+    def stop_current_channel(self):
+        music.stop()
+        if self.current_channel:
+            self.current_channel.stop()
+        self.current_channel = None
+        self.paused = False
+
+    def play_fx(self, name, panning=.5):
+        if not self.muted:
+            channel = find_channel(True)
+            if panning != .5:
+                offset = 1 - abs(panning - .5) * 2
+                if panning < .5:
+                    channel.set_volume(1, offset)
+                else:
+                    channel.set_volume(offset, 1)
+            channel.play(self.fx[name])
+
+    def pause(self):
+        channel = self.current_channel
+        paused = self.paused
+        if paused:
+            music.unpause()
+            if channel:
+                channel.unpause()
+        else:
+            music.pause()
+            if channel:
+                channel.pause()
+        self.paused = not paused
+
+    def is_bgm_playing(self):
+        current = self.current_channel
+        if current and current.get_sound():
+            return True
+        return music.get_busy()
diff --git a/shaken_and_spit_out/pgfw/Configuration.py b/shaken_and_spit_out/pgfw/Configuration.py
new file mode 100644 (file)
index 0000000..7e00a3b
--- /dev/null
@@ -0,0 +1,442 @@
+from os import sep, getcwd
+from os.path import join, exists, basename, dirname, expanduser
+from sys import argv
+from re import match
+from pprint import pformat
+
+from ConfigParser import RawConfigParser
+
+class Configuration(RawConfigParser):
+
+    default_project_file_rel_path = "config"
+    default_resource_paths = [".", "resource"]
+
+    def __init__(self, project_file_rel_path=None, resource_path=None,
+                 type_declarations=None):
+        RawConfigParser.__init__(self)
+        self.project_file_rel_path = project_file_rel_path
+        self.resource_path = resource_path
+        self.modifiable = {}
+        self.order = []
+        self.set_type_declarations(type_declarations)
+        self.set_defaults()
+        self.read_project_config_file()
+        self.modify_defaults()
+        self.print_debug(self)
+
+    def set_type_declarations(self, type_declarations):
+        if type_declarations is None:
+            type_declarations = TypeDeclarations()
+        self.type_declarations = type_declarations
+
+    def translate_path(self, path):
+        new = ""
+        if path and path[0] == sep:
+            new += sep
+        return expanduser("{0}{1}".format(new, join(*path.split(sep))))
+
+    def set_defaults(self):
+        add_section = self.add_section
+        set_option = self.set
+        section = "setup"
+        add_section(section)
+        set_option(section, "package-root", basename(getcwd()), False)
+        set_option(section, "additional-packages", "", False)
+        set_option(section, "title", "", False)
+        set_option(section, "classifiers", "", False)
+        set_option(section, "resource-search-path", "./, resource/", False)
+        set_option(section, "installation-dir", "/usr/local/share/games/",
+                   False)
+        set_option(section, "changelog", "changelog", False)
+        set_option(section, "description-file", "", False)
+        set_option(section, "init-script", "", False)
+        set_option(section, "version", "", False)
+        set_option(section, "summary", "", False)
+        set_option(section, "license", "", False)
+        set_option(section, "platforms", "", False)
+        set_option(section, "contact-name", "", False)
+        set_option(section, "contact-email", "", False)
+        set_option(section, "url", "", False)
+        set_option(section, "requirements", "", False)
+        set_option(section, "main-object", "pgfw/Game.py", False)
+        set_option(section, "resource-path-identifier", "resource_path", False)
+        set_option(section, "special-char-placeholder", "_", False)
+        set_option(section, "whitespace-placeholder", "-", False)
+        set_option(section, "windows-dist-path", "dist/win/", False)
+        set_option(section, "windows-icon-path", "", False)
+        set_option(section, "lowercase-boolean-true", "yes", False)
+        section = "display"
+        add_section(section)
+        set_option(section, "dimensions", "480, 360", False)
+        set_option(section, "frame-duration", "40", False)
+        set_option(section, "wait-duration", "2", False)
+        set_option(section, "caption", "", False)
+        set_option(section, "centered", "yes", False)
+        set_option(section, "icon-path", "", False)
+        set_option(section, "skip-frames", "no", False)
+        set_option(section, "fullscreen", "no", False)
+        set_option(section, "windowed-flag", "wi", False)
+        set_option(section, "show-framerate", "no", False)
+        set_option(section, "framerate-display-flag", "fr", False)
+        set_option(section, "framerate-text-size", "16", False)
+        set_option(section, "framerate-text-color", "0, 0, 0", False)
+        set_option(section, "framerate-text-background", "255, 255, 255", False)
+        section = "input"
+        add_section(section)
+        set_option(section, "release-suffix", "-release", False)
+        section = "sprite"
+        add_section(section)
+        set_option(section, "transparent-color", "magenta", False)
+        section = "screen-captures"
+        add_section(section)
+        set_option(section, "rel-path", "caps", False)
+        set_option(section, "file-name-format", "%Y%m%d%H%M%S", False)
+        set_option(section, "file-extension", "png", False)
+        section = "video-recordings"
+        add_section(section)
+        set_option(section, "rel-path", "vids", False)
+        set_option(section, "directory-name-format", "%Y%m%d%H%M%S", False)
+        set_option(section, "file-extension", "png", False)
+        set_option(section, "frame-format", "RGB", False)
+        set_option(section, "framerate", "100", False)
+        section = "mouse"
+        add_section(section)
+        set_option(section, "visible", "yes", False)
+        set_option(section, "double-click-time-limit", ".5", False)
+        section = "keys"
+        add_section(section)
+        set_option(section, "up", "K_UP, K_w", False)
+        set_option(section, "right", "K_RIGHT, K_d", False)
+        set_option(section, "down", "K_DOWN, K_s", False)
+        set_option(section, "left", "K_LEFT, K_a", False)
+        set_option(section, "capture-screen", "K_F9", False)
+        set_option(section, "toggle-fullscreen", "K_F11", False)
+        set_option(section, "reset-game", "K_F8", False)
+        set_option(section, "record-video", "K_F10", False)
+        set_option(section, "mute", "K_F12", False)
+        set_option(section, "toggle-interpolator", "K_F7", False)
+        section = "joy"
+        add_section(section)
+        set_option(section, "advance", "7", False)
+        set_option(section, "pause", "7", False)
+        set_option(section, "select", "6", False)
+        section = "event"
+        add_section(section)
+        set_option(section, "user-event-id", "USEREVENT", False)
+        set_option(section, "command-id-offset", "1", False)
+        set_option(section, "command-key", "command", False)
+        set_option(section, "cancel-flag-key", "cancel", False)
+        section = "audio"
+        add_section(section)
+        set_option(section, "sfx-path", "aud/fx/", False)
+        section = "interpolator-gui"
+        add_section(section)
+        set_option(section, "margin", "80", False)
+        set_option(section, "marker-color", "255, 0, 0", False)
+        set_option(section, "marker-size", "11", False)
+        set_option(section, "curve-color", "0, 255, 0", False)
+        set_option(section, "label-size", "16", False)
+        set_option(section, "label-precision", "2", False)
+        set_option(section, "axis-label-count", "8", False)
+        set_option(section, "prompt-size", "380, 60", False)
+        set_option(section, "prompt-border-color", "255, 0, 0", False)
+        set_option(section, "prompt-border-width", "3", False)
+        set_option(section, "prompt-character-limit", "21", False)
+        set_option(section, "prompt-text-size", "42", False)
+        set_option(section, "template-nodeset", "L 0 0, 1000 1", False)
+        set_option(section, "template-nodeset-name", "template", False)
+        set_option(section, "flat-y-range", "1", False)
+
+    def add_section(self, name):
+        if name not in self.order:
+            self.order.append(name)
+        RawConfigParser.add_section(self, name)
+
+    def set(self, section, option, value, modifiable=True):
+        if modifiable:
+            if section not in self.order:
+                self.order.append(section)
+            if section not in self.modifiable:
+                self.modifiable[section] = []
+            if option not in self.modifiable[section]:
+                self.modifiable[section].append(option)
+        RawConfigParser.set(self, section, option, value)
+
+    def read_project_config_file(self):
+        path = self.locate_project_config_file()
+        if path:
+            fp = open(path)
+            self.set_modifiable(fp)
+            fp.seek(0)
+            self.readfp(fp)
+            fp.seek(0)
+            self.set_order(fp)
+            fp.close()
+        else:
+            self.print_debug("No configuration file found")
+
+    def locate_project_config_file(self):
+        rel_path = self.project_file_rel_path
+        if not rel_path:
+            rel_path = self.default_project_file_rel_path
+        if exists(rel_path) and not self.is_shared_mode():
+            return rel_path
+        if self.resource_path:
+            installed_path = join(self.resource_path, rel_path)
+            if exists(installed_path):
+                return installed_path
+
+    def set_order(self, fp):
+        self.order = order = []
+        for line in file(self.locate_project_config_file()):
+            result = match("^\s*\[(.*)\]\s*$", line)
+            if result:
+                order.append(result.group(1))
+
+    def set_modifiable(self, fp):
+        config = RawConfigParser()
+        config.readfp(fp)
+        modifiable = self.modifiable
+        for section in config._sections:
+            if section not in modifiable:
+                modifiable[section] = []
+            for option in config._sections[section]:
+                if option != "__name__" and option not in modifiable[section]:
+                    modifiable[section].append(option)
+
+    def is_shared_mode(self):
+        return "-s" in argv
+
+    def print_debug(self, statement):
+        if self.is_debug_mode():
+            print statement
+
+    def is_debug_mode(self):
+        return "-d" in argv
+
+    def modify_defaults(self):
+        self.set_installation_path()
+        self.set_resource_search_path()
+        self.set_screen_captures_path()
+        self.set_video_recordings_path()
+        self.set_data_exclusion_list()
+        self.set_requirements()
+
+    def set_installation_path(self):
+        self.set("setup", "installation-path",
+                 join(self.get("setup", "installation-dir"),
+                      self.get("setup", "package-root")), False)
+
+    def set_resource_search_path(self):
+        section, option = "setup", "resource-search-path"
+        search_path = self.get(section, option)
+        if self.resource_path:
+            search_path.append(self.resource_path)
+        else:
+            search_path.append(self.get("setup", "installation-path"))
+        self.set(section, option, search_path, False)
+
+    def get(self, section, option):
+        value = RawConfigParser.get(self, section, option)
+        if value is None:
+            value = self.get_substitute(section, option)
+        return self.cast_value(section, option, value)
+
+    def get_substitute(self, section, option):
+        if section == "display":
+            if option == "caption":
+                return self.get("setup", "title")
+
+    def cast_value(self, section, option, value):
+        pair = section, option
+        types = self.type_declarations
+        if type(value) == str:
+            if pair in types["bool"]:
+                if value.lower() == self.get("setup", "lowercase-boolean-true"):
+                    return True
+                return False
+            elif pair in types["int"]:
+                return int(value)
+            elif pair in types["float"]:
+                return float(value)
+            elif pair in types["path"]:
+                return self.translate_path(value)
+            elif pair in types["list"]:
+                if value == "":
+                    return []
+                else:
+                    return map(str.strip, value.split(types.list_member_sep))
+            elif pair in types["int-list"]:
+                return map(int, value.split(types.list_member_sep))
+            elif pair in types["float-list"]:
+                return map(float, value.split(types.list_member_sep))
+        return value
+
+    def set_screen_captures_path(self):
+        section, option = "screen-captures", "path"
+        if not self.has_option(section, option):
+            self.set(section, option, join(self.build_home_path(),
+                                           self.get(section, "rel-path")),
+                     False)
+
+    def build_home_path(self):
+        return join("~", "." + self.get("setup", "package-root"))
+
+    def set_video_recordings_path(self):
+        section, option = "video-recordings", "path"
+        if not self.has_option(section, option):
+            self.set(section, option, join(self.build_home_path(),
+                                           self.get(section, "rel-path")),
+                     False)
+
+    def set_data_exclusion_list(self):
+        section, option = "setup", "data-exclude"
+        exclude = []
+        if self.has_option(section, option):
+            exclude = self.get(section, option)
+        exclude += [".git", ".gitignore", "README", "build/", "dist/",
+                    "setup.py", "MANIFEST", "PKG-INFO",
+                    self.get("setup", "changelog"),
+                    self.get("setup", "package-root")]
+        for location in self.get("setup", "additional-packages"):
+            exclude.append(location)
+        self.set(section, option, exclude, False)
+
+    def set_requirements(self):
+        section, option = "setup", "requirements"
+        requirements = []
+        if self.has_option(section, option):
+            requirements = self.get(section, option)
+        if "pygame" not in requirements:
+            requirements.append("pygame")
+        self.set(section, option, requirements, False)
+
+    def get_section(self, section):
+        assignments = {}
+        for option in self.options(section):
+            assignments[option] = self.get(section, option)
+        return assignments
+
+    def __repr__(self):
+        config = {}
+        for section in self.sections():
+            config[section] = self.get_section(section)
+        return pformat(config, 2, 1)
+
+    def items(self, section):
+        items = []
+        for option in self.options(section):
+            items.append((option, self.get(section, option)))
+        return items
+
+    def write(self, fp=None):
+        modifiable = self.modifiable
+        use_main = fp is None
+        if use_main:
+            path = self.locate_project_config_file()
+            if not path:
+                path = join(self.resource_path or "",
+                            self.default_project_file_rel_path)
+            fp = open(path, "w")
+        break_line = False
+        for section in self.order:
+            if section in modifiable:
+                break_line and fp.write("\n")
+                fp.write("[%s]\n" % section)
+                for option in modifiable[section]:
+                    if self.has_option(section, option):
+                        value = self.get(section, option)
+                        fp.write("%s = %s\n" % (option,
+                                                self.get_raw_value(value)))
+                break_line = True
+        if use_main:
+            fp.close()
+
+    def get_raw_value(self, value):
+        if isinstance(value, list):
+            raw = ""
+            for ii, value in enumerate(value):
+                if ii:
+                    raw += ", "
+                raw += str(value)
+        else:
+            raw = str(value)
+        return raw
+
+    def clear_section(self, section):
+        if self.has_section(section):
+            for option in self.options(section):
+                self.remove_option(section, option)
+
+
+class TypeDeclarations(dict):
+
+    list_member_sep = ','
+
+    defaults = {
+
+        "display": {"int": ["frame-duration", "wait-duration",
+                            "framerate-text-size"],
+
+                    "bool": ["centered", "skip-frames", "fullscreen",
+                             "show-framerate"],
+
+                    "int-list": ["dimensions", "framerate-text-color",
+                                 "framerate-text-background"]},
+
+        "screen-captures": {"path": ["rel-path", "path"]},
+
+        "video-recordings": {"path": ["rel-path", "path"],
+
+                             "int": "framerate"},
+
+        "setup": {"list": ["classifiers", "resource-search-path",
+                           "requirements", "data-exclude",
+                           "additional-packages"],
+
+                  "path": ["installation-dir", "changelog", "description-file",
+                           "main-object", "icon-path", "windows-dist-path",
+                           "package-root"]},
+
+        "mouse": {"float": "double-click-time-limit",
+
+                  "bool": "visible"},
+
+        "keys": {"list": ["up", "right", "down", "left"]},
+
+        "joy": {"int": ["advance", "pause", "select"]},
+
+        "audio": {"path": "sfx-path"},
+
+        "event": {"int": "command-id-offset"},
+
+        "interpolator-gui": {"int": ["margin", "marker-size", "label-size",
+                                     "axis-label-count", "label-precision",
+                                     "prompt-border-width",
+                                     "prompt-character-limit",
+                                     "prompt-text-size", "flat-y-range"],
+
+                             "int-list": ["marker-color", "curve-color",
+                                          "prompt-size",
+                                          "prompt-border-color"]},
+
+        }
+
+    additional_defaults = {}
+
+    def __init__(self):
+        dict.__init__(self, {"bool": [], "int": [], "float": [], "path": [],
+                             "list": [], "int-list": [], "float-list": []})
+        self.add_chart(self.defaults)
+        self.add_chart(self.additional_defaults)
+
+    def add(self, cast, section, option):
+        self[cast].append((section, option))
+
+    def add_chart(self, chart):
+        for section, declarations in chart.iteritems():
+            for cast, options in declarations.iteritems():
+                if type(options) != list:
+                    options = [options]
+                for option in options:
+                    self.add(cast, section, option)
diff --git a/shaken_and_spit_out/pgfw/Delegate.py b/shaken_and_spit_out/pgfw/Delegate.py
new file mode 100644 (file)
index 0000000..ce62c1b
--- /dev/null
@@ -0,0 +1,87 @@
+from pygame.event import get, pump, Event, post
+from pygame.locals import *
+
+from GameChild import GameChild
+from Input import Input
+
+class Delegate(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.subscribers = dict()
+        self.load_configuration()
+        self.disable()
+
+    def load_configuration(self):
+        config = self.get_configuration("event")
+        self.cancel_flag_key = config["cancel-flag-key"]
+        self.command_key = config["command-key"]
+        self.command_event_id = config["command-id-offset"] + \
+                                globals()[config["user-event-id"]]
+
+    def disable(self):
+        self.enabled = False
+
+    def enable(self):
+        self.enabled = True
+        self.interpolator = self.get_game().interpolator
+
+    def dispatch(self):
+        if self.enabled:
+            subscribers = self.subscribers
+            for evt in get():
+                kind = evt.type
+                if kind in subscribers:
+                    for subscriber in subscribers[kind]:
+                        if not self.interpolator.is_gui_active() or \
+                               hasattr(subscriber, "im_class") and \
+                               (subscriber.im_class == Input or \
+                                subscriber.im_class == \
+                                self.interpolator.gui.__class__):
+                            self.print_debug("Passing %s to %s" % (evt,
+                                                                   subscriber))
+                            subscriber(evt)
+        else:
+            pump()
+
+    def add_subscriber(self, callback, kind=None):
+        self.print_debug("Subscribing %s to %s" % (callback, kind))
+        if kind is None:
+            kind = self.command_event_id
+        subscribers = self.subscribers
+        if kind not in subscribers:
+            subscribers[kind] = list()
+        subscribers[kind].append(callback)
+
+    def is_command(self, event):
+        return event.type == self.command_event_id
+
+    def remove_subscriber(self, callback, kind=None):
+        if kind is None:
+            kind = self.command_event_id
+        self.subscribers[kind].remove(callback)
+
+    def compare(self, evt, commands=None, cancel=False, **attributes):
+        if evt.type == self.command_event_id:
+            self.add_cancel_flag_to_attributes(attributes, cancel)
+        if commands is not None and not isinstance(commands, list):
+            commands = [commands]
+        if commands is not None:
+            if not self.command_in_list(evt, commands):
+                return False
+        return all(key in evt.dict and evt.dict[key] == value for \
+                   key, value in attributes.iteritems())
+
+    def add_cancel_flag_to_attributes(self, attributes, cancel):
+        attributes[self.cancel_flag_key] = cancel
+
+    def command_in_list(self, evt, commands):
+        return self.get_command_attribute(evt) in commands
+
+    def get_command_attribute(self, evt):
+        return evt.dict[self.command_key]
+
+    def post(self, command=None, cancel=False, **attributes):
+        attributes[self.command_key] = command
+        self.add_cancel_flag_to_attributes(attributes, cancel)
+        post(Event(self.command_event_id, attributes))
diff --git a/shaken_and_spit_out/pgfw/Display.py b/shaken_and_spit_out/pgfw/Display.py
new file mode 100644 (file)
index 0000000..c9e4b52
--- /dev/null
@@ -0,0 +1,81 @@
+from os import environ
+
+from pygame import display, image, mouse
+from pygame.locals import *
+
+from GameChild import *
+
+class Display(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.align_window()
+        self.init_screen()
+        self.set_caption()
+        self.set_icon()
+        self.set_mouse_visibility()
+        self.subscribe(self.toggle_fullscreen)
+
+    def load_configuration(self):
+        config = self.get_configuration("display")
+        self.centered = config["centered"]
+        self.fullscreen_enabled = config["fullscreen"]
+        self.caption = config["caption"]
+        self.windowed_flag = config["windowed-flag"]
+        self.icon_path = self.get_resource("display", "icon-path")
+        self.mouse_visibility = self.get_configuration("mouse", "visible")
+
+    def align_window(self):
+        if self.centered:
+            environ["SDL_VIDEO_CENTERED"] = "1"
+
+    def init_screen(self):
+        flags = 0
+        if self.fullscreen_requested():
+            flags = FULLSCREEN
+        self.set_screen(flags)
+
+    def fullscreen_requested(self):
+        return not self.check_command_line(self.windowed_flag) and \
+               self.fullscreen_enabled
+
+    def set_screen(self, flags=0, dimensions=None):
+        self.dimensions_changed = dimensions is not None
+        if dimensions is None:
+            if display.get_surface():
+                dimensions = display.get_surface().get_size()
+            else:
+                dimensions = self.get_configuration("display", "dimensions")
+        self.screen = display.set_mode(dimensions, flags)
+        if self.dimensions_changed:
+            interpolator = self.get_game().interpolator
+            if interpolator.gui_enabled:
+                interpolator.gui.rearrange()
+
+    def set_caption(self):
+        display.set_caption(self.caption)
+
+    def set_icon(self):
+        if self.icon_path:
+            print self.icon_path
+            display.set_icon(image.load(self.icon_path).convert_alpha())
+
+    def set_mouse_visibility(self, visibility=None):
+        if visibility is None:
+            visibility = self.mouse_visibility
+        return mouse.set_visible(visibility)
+
+    def get_screen(self):
+        return self.screen
+
+    def get_size(self):
+        return self.screen.get_size()
+
+    def toggle_fullscreen(self, event):
+        if self.delegate.compare(event, "toggle-fullscreen"):
+            screen = self.screen
+            cpy = screen.convert()
+            self.set_screen(self.screen.get_flags() ^ FULLSCREEN)
+            screen.blit(cpy, (0, 0))
diff --git a/shaken_and_spit_out/pgfw/Game.py b/shaken_and_spit_out/pgfw/Game.py
new file mode 100644 (file)
index 0000000..47d47f6
--- /dev/null
@@ -0,0 +1,74 @@
+import pygame
+from pygame.locals import *
+
+from GameChild import GameChild
+from Mainloop import Mainloop
+from Audio import Audio
+from Display import Display
+from Configuration import Configuration
+from Delegate import Delegate
+from Input import Input
+from ScreenGrabber import ScreenGrabber
+from Profile import Profile
+from VideoRecorder import VideoRecorder
+from Interpolator import Interpolator
+from TimeFilter import TimeFilter
+
+class Game(GameChild):
+
+    resource_path = None
+
+    def __init__(self, config_rel_path=None, type_declarations=None):
+        self.profile = Profile(self)
+        GameChild.__init__(self)
+        self.print_debug(pygame.version.ver)
+        self.config_rel_path = config_rel_path
+        self.type_declarations = type_declarations
+        self.set_configuration()
+        pygame.init()
+        self.set_children()
+        self.subscribe(self.end, QUIT)
+        self.subscribe(self.end)
+        self.delegate.enable()
+
+    def set_configuration(self):
+        self.configuration = Configuration(self.config_rel_path,
+                                           self.resource_path,
+                                           self.type_declarations)
+
+    def set_children(self):
+        self.time_filter = TimeFilter(self)
+        self.delegate = Delegate(self)
+        self.display = Display(self)
+        self.mainloop = Mainloop(self)
+        self.input = Input(self)
+        self.audio = Audio(self)
+        self.screen_grabber = ScreenGrabber(self)
+        self.video_recorder = VideoRecorder(self)
+        self.interpolator = Interpolator(self)
+
+    def frame(self):
+        self.time_filter.update()
+        self.delegate.dispatch()
+        if not self.interpolator.is_gui_active():
+            self.update()
+        else:
+            self.interpolator.gui.update()
+        self.video_recorder.update()
+
+    def run(self):
+        self.mainloop.run()
+
+    def update(self):
+        pass
+
+    def blit(self, source, destination, area=None, special_flags=0):
+        self.get_screen().blit(source, destination, area, special_flags)
+
+    def get_rect(self):
+        return self.get_screen().get_rect()
+
+    def end(self, evt):
+        if evt.type == QUIT or self.delegate.compare(evt, "quit"):
+            self.mainloop.stop()
+            self.profile.end()
diff --git a/shaken_and_spit_out/pgfw/GameChild.py b/shaken_and_spit_out/pgfw/GameChild.py
new file mode 100644 (file)
index 0000000..8808dce
--- /dev/null
@@ -0,0 +1,85 @@
+from os.path import exists, join, basename, normpath, abspath
+from sys import argv
+
+from pygame import mixer, event, time
+from pygame.locals import *
+
+import Game
+
+class GameChild:
+
+    def __init__(self, parent=None):
+        self.parent = parent
+        self.game = self.get_game()
+
+    def get_game(self):
+        current = self
+        while not isinstance(current, Game.Game):
+            current = current.parent
+        return current
+
+    def get_configuration(self, section=None, option=None):
+        config = self.game.configuration
+        if option and section:
+            return config.get(section, option)
+        if section:
+            return config.get_section(section)
+        return config
+
+    def get_input(self):
+        return self.game.input
+
+    def get_screen(self):
+        return self.game.display.get_screen()
+
+    def get_display_surface(self):
+        current = self
+        attribute = "display_surface"
+        while not isinstance(current, Game.Game):
+            if hasattr(current, attribute):
+                return getattr(current, attribute)
+            current = current.parent
+        return current.display.get_screen()
+
+    def get_audio(self):
+        return self.game.audio
+
+    def get_delegate(self):
+        return self.game.delegate
+
+    def get_resource(self, path_or_section, option=None):
+        config = self.get_configuration()
+        rel_path = path_or_section
+        if option is not None:
+            rel_path = config.get(path_or_section, option)
+        if rel_path:
+            for root in config.get("setup", "resource-search-path"):
+                if self.is_shared_mode() and not self.is_absolute_path(root):
+                    continue
+                path = join(root, rel_path)
+                if exists(path):
+                    return path
+        self.print_debug("Couldn't find resource: {0} {1}".\
+                                   format(path_or_section, option))
+
+    def is_shared_mode(self):
+        return self.check_command_line("s")
+
+    def check_command_line(self, flag):
+        return "-" + flag in argv
+
+    def print_debug(self, statement):
+        if self.is_debug_mode():
+            print statement
+
+    def is_debug_mode(self):
+        return self.check_command_line("d")
+
+    def is_absolute_path(self, path):
+        return normpath(path) == abspath(path)
+
+    def subscribe(self, callback, kind=None):
+        self.game.delegate.add_subscriber(callback, kind)
+
+    def unsubscribe(self, callback, kind=None):
+        self.game.delegate.remove_subscriber(callback, kind)
diff --git a/shaken_and_spit_out/pgfw/Input.py b/shaken_and_spit_out/pgfw/Input.py
new file mode 100644 (file)
index 0000000..6f888bf
--- /dev/null
@@ -0,0 +1,204 @@
+from time import time as get_secs
+
+from pygame import joystick as joy
+from pygame.key import get_pressed
+from pygame.locals import *
+
+from GameChild import *
+
+class Input(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.last_mouse_down_left = None
+        self.joystick = Joystick()
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.set_any_press_ignore_list()
+        self.unsuppress()
+        self.subscribe_to_events()
+        self.build_key_map()
+        self.build_joy_button_map()
+
+    def load_configuration(self):
+        self.release_suffix = self.get_configuration("input", "release-suffix")
+        self.key_commands = self.get_configuration().items("keys")
+        self.double_click_time_limit = self.get_configuration(
+            "mouse", "double-click-time-limit")
+
+    def set_any_press_ignore_list(self):
+        self.any_press_ignored = set(["capture-screen", "toggle-fullscreen",
+                                      "reset-game", "record-video", "quit",
+                                      "mute", "toggle-interpolator"])
+        self.any_press_ignored_keys = set()
+
+    def unsuppress(self):
+        self.suppressed = False
+
+    def subscribe_to_events(self):
+        self.subscribe(self.translate_key, KEYDOWN)
+        self.subscribe(self.translate_key, KEYUP)
+        self.subscribe(self.translate_joy_button, JOYBUTTONDOWN)
+        self.subscribe(self.translate_joy_button, JOYBUTTONUP)
+        self.subscribe(self.translate_axis_motion, JOYAXISMOTION)
+        self.subscribe(self.translate_mouse_input, MOUSEBUTTONDOWN)
+        self.subscribe(self.translate_mouse_input, MOUSEBUTTONUP)
+
+    def build_key_map(self):
+        key_map = {}
+        for command, keys in self.key_commands:
+            key_map[command] = []
+            if type(keys) == str:
+                keys = [keys]
+            for key in keys:
+                key_map[command].append(globals()[key])
+        self.key_map = key_map
+
+    def build_joy_button_map(self):
+        self.joy_button_map = self.get_configuration("joy")
+
+    def suppress(self):
+        self.suppressed = True
+
+    def translate_key(self, event):
+        if not self.suppressed:
+            cancel = event.type == KEYUP
+            posted = None
+            key = event.key
+            for cmd, keys in self.key_map.iteritems():
+                if key in keys:
+                    self.post_command(cmd, cancel=cancel)
+                    posted = cmd
+            if (not posted or posted not in self.any_press_ignored) and \
+                   key not in self.any_press_ignored_keys:
+                self.post_any_command(key, cancel)
+
+    def post_command(self, cmd, **attributes):
+        self.delegate.post(cmd, **attributes)
+
+    def post_any_command(self, id, cancel=False):
+        self.post_command("any", id=id, cancel=cancel)
+
+    def translate_joy_button(self, event):
+        if not self.suppressed:
+            cancel = event.type == JOYBUTTONUP
+            posted = None
+            for command, button in self.joy_button_map.iteritems():
+                if button == event.button:
+                    self.post_command(command, cancel=cancel)
+                    posted = command
+            if not posted or posted not in self.any_press_ignored:
+                self.post_any_command(event.button, cancel)
+
+    def translate_axis_motion(self, event):
+        if not self.suppressed:
+            axis = event.axis
+            value = event.value
+            if not value:
+                for command in "up", "right", "down", "left":
+                    self.post_command(command, cancel=True)
+                    if command not in self.any_press_ignored:
+                        self.post_any_command(command, True)
+            else:
+                if axis == 1:
+                    if value < 0:
+                        command = "up"
+                    elif value > 0:
+                        command = "down"
+                else:
+                    if value > 0:
+                        command = "right"
+                    elif value < 0:
+                        command = "left"
+                self.post_command(command)
+                if command not in self.any_press_ignored:
+                    self.post_any_command(command)
+
+    def is_command_active(self, command):
+        if not self.suppressed:
+            if self.is_key_pressed(command):
+                return True
+            joystick = self.joystick
+            joy_map = self.joy_button_map
+            if command in joy_map and joystick.get_button(joy_map[command]):
+                return True
+            if command == "up":
+                return joystick.is_direction_pressed(Joystick.up)
+            elif command == "right":
+                return joystick.is_direction_pressed(Joystick.right)
+            elif command == "down":
+                return joystick.is_direction_pressed(Joystick.down)
+            elif command == "left":
+                return joystick.is_direction_pressed(Joystick.left)
+
+    def is_key_pressed(self, command):
+        poll = get_pressed()
+        for key in self.key_map[command]:
+            if poll[key]:
+                return True
+
+    def translate_mouse_input(self, event):
+        button = event.button
+        pos = event.pos
+        post = self.post_command
+        if event.type == MOUSEBUTTONDOWN:
+            if button == 1:
+                last = self.last_mouse_down_left
+                if last:
+                    limit = self.double_click_time_limit
+                    if get_secs() - last < limit:
+                        post("mouse-double-click-left", pos=pos)
+                last = get_secs()
+                self.last_mouse_down_left = last
+
+    def get_axes(self):
+        axes = {}
+        for direction in "up", "right", "down", "left":
+            axes[direction] = self.is_command_active(direction)
+        return axes
+
+    def register_any_press_ignore(self, *args, **attributes):
+        self.any_press_ignored.update(args)
+        self.any_press_ignored_keys.update(self.extract_keys(attributes))
+
+    def extract_keys(self, attributes):
+        keys = []
+        if "keys" in attributes:
+            keys = attributes["keys"]
+            if type(keys) == int:
+                keys = [keys]
+        return keys
+
+    def unregister_any_press_ignore(self, *args, **attributes):
+        self.any_press_ignored.difference_update(args)
+        self.any_press_ignored_keys.difference_update(
+            self.extract_keys(attributes))
+
+
+class Joystick:
+
+    (up, right, down, left) = range(4)
+
+    def __init__(self):
+        js = None
+        if joy.get_count() > 0:
+            js = joy.Joystick(0)
+            js.init()
+        self.js = js
+
+    def is_direction_pressed(self, direction):
+        js = self.js
+        if not js or direction > 4:
+            return False
+        if direction == 0:
+            return js.get_axis(1) < 0
+        elif direction == 1:
+            return js.get_axis(0) > 0
+        elif direction == 2:
+            return js.get_axis(1) > 0
+        elif direction == 3:
+            return js.get_axis(0) < 0
+
+    def get_button(self, id):
+       if self.js:
+           return self.js.get_button(id)
diff --git a/shaken_and_spit_out/pgfw/Interpolator.py b/shaken_and_spit_out/pgfw/Interpolator.py
new file mode 100644 (file)
index 0000000..8a578f1
--- /dev/null
@@ -0,0 +1,733 @@
+from re import match
+from os.path import join
+from tempfile import gettempdir
+
+from pygame import Surface
+from pygame.font import Font
+from pygame.draw import aaline
+from pygame.locals import *
+
+from GameChild import GameChild
+from Sprite import Sprite
+from Animation import Animation
+
+class Interpolator(list, GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.set_nodesets()
+        self.gui_enabled = self.check_command_line("-interpolator")
+        if self.gui_enabled:
+            self.gui = GUI(self)
+
+    def set_nodesets(self):
+        config = self.get_configuration()
+        if config.has_section("interpolate"):
+            for name, value in config.get_section("interpolate").iteritems():
+                self.add_nodeset(name, value)
+
+    def add_nodeset(self, name, value, method=None):
+        self.append(Nodeset(name, value, method))
+        return len(self) - 1
+
+    def is_gui_active(self):
+        return self.gui_enabled and self.gui.active
+
+    def get_nodeset(self, name):
+        for nodeset in self:
+            if nodeset.name == name:
+                return nodeset
+
+    def remove(self, outgoing):
+        for ii, nodeset in enumerate(self):
+            if nodeset.name == outgoing.name:
+                self.pop(ii)
+                break
+
+
+class Nodeset(list):
+
+    LINEAR, CUBIC = range(2)
+
+    def __init__(self, name, nodes, method=None):
+        list.__init__(self, [])
+        self.name = name
+        if isinstance(nodes, str):
+            self.parse_raw(nodes)
+        else:
+            self.interpolation_method = method
+            self.parse_list(nodes)
+        self.set_splines()
+
+    def parse_raw(self, raw):
+        raw = raw.strip()
+        if raw[0].upper() == "L":
+            self.set_interpolation_method(self.LINEAR, False)
+        else:
+            self.set_interpolation_method(self.CUBIC, False)
+        for node in raw[1:].strip().split(","):
+            self.add_node(map(float, node.strip().split()), False)
+
+    def set_interpolation_method(self, method, refresh=True):
+        self.interpolation_method = method
+        if refresh:
+            self.set_splines()
+
+    def add_node(self, coordinates, refresh=True):
+        x = coordinates[0]
+        inserted = False
+        index = 0
+        for ii, node in enumerate(self):
+            if x < node.x:
+                self.insert(ii, Node(coordinates))
+                inserted = True
+                index = ii
+                break
+            elif x == node.x:
+                return None
+        if not inserted:
+            self.append(Node(coordinates))
+            index = len(self) - 1
+        if refresh:
+            self.set_splines()
+        return index
+
+    def parse_list(self, nodes):
+        for node in nodes:
+            self.add_node(node)
+
+    def set_splines(self):
+        if self.interpolation_method == self.LINEAR:
+            self.set_linear_splines()
+        else:
+            self.set_cubic_splines()
+
+    def set_linear_splines(self):
+        self.splines = splines = []
+        for ii in xrange(len(self) - 1):
+            x1, y1, x2, y2 = self[ii] + self[ii + 1]
+            m = float(y2 - y1) / (x2 - x1)
+            splines.append(LinearSpline(x1, y1, m))
+
+    def set_cubic_splines(self):
+        n = len(self) - 1
+        a = [node.y for node in self]
+        b = [None] * n
+        d = [None] * n
+        h = [self[ii + 1].x - self[ii].x for ii in xrange(n)]
+        alpha = [None] + [(3.0 / h[ii]) * (a[ii + 1] - a[ii]) - \
+                          (3.0 / h[ii - 1]) * (a[ii] - a[ii - 1]) \
+                          for ii in xrange(1, n)]
+        c = [None] * (n + 1)
+        l = [None] * (n + 1)
+        u = [None] * (n + 1)
+        z = [None] * (n + 1)
+        l[0] = 1
+        u[0] = z[0] = 0
+        for ii in xrange(1, n):
+            l[ii] = 2 * (self[ii + 1].x - self[ii - 1].x) - \
+                    h[ii - 1] * u[ii - 1]
+            u[ii] = h[ii] / l[ii]
+            z[ii] = (alpha[ii] - h[ii - 1] * z[ii - 1]) / l[ii]
+        l[n] = 1
+        z[n] = c[n] = 0
+        for jj in xrange(n - 1, -1, -1):
+            c[jj] = z[jj] - u[jj] * c[jj + 1]
+            b[jj] = (a[jj + 1] - a[jj]) / h[jj] - \
+                    (h[jj] * (c[jj + 1] + 2 * c[jj])) / 3
+            d[jj] = (c[jj + 1] - c[jj]) / (3 * h[jj])
+        self.splines = [CubicSpline(self[ii].x, a[ii], b[ii], c[ii],
+                                    d[ii]) for ii in xrange(n)]
+
+    def get_y(self, t, loop=False, reverse=False, natural=False):
+        if loop or reverse:
+            if reverse and int(t) / int(self[-1].x) % 2:
+                t = self[-1].x - t
+            t %= self[-1].x
+        elif not natural:
+            if t < self[0].x:
+                t = self[0].x
+            elif t > self[-1].x:
+                t = self[-1].x
+        splines = self.splines
+        for ii in xrange(len(splines) - 1):
+            if t < splines[ii + 1].x:
+                return splines[ii].get_y(t)
+        return splines[-1].get_y(t)
+
+    def remove(self, node, refresh=True):
+        list.remove(self, node)
+        if refresh:
+            self.set_splines()
+
+    def resize(self, left, length, refresh=True):
+        old_left = self[0].x
+        old_length = self.get_length()
+        for node in self:
+            node.x = left + length * (node.x - old_left) / old_length
+        if refresh:
+            self.set_splines()
+
+    def get_length(self):
+        return self[-1].x - self[0].x
+
+
+class Node(list):
+
+    def __init__(self, coordinates):
+        list.__init__(self, coordinates)
+
+    def __getattr__(self, name):
+        if name == "x":
+            return self[0]
+        elif name == "y":
+            return self[1]
+        return list.__get__(self, name)
+
+    def __setattr__(self, name, value):
+        if name == "x":
+            list.__setitem__(self, 0, value)
+        elif name == "y":
+            list.__setitem__(self, 1, value)
+        else:
+            list.__setattr__(self, name, value)
+
+
+class Spline:
+
+    def __init__(self, x):
+        self.x = x
+
+
+class CubicSpline(Spline):
+
+    def __init__(self, x, a, b, c, d):
+        Spline.__init__(self, x)
+        self.a = a
+        self.b = b
+        self.c = c
+        self.d = d
+
+    def get_y(self, t):
+        x = self.x
+        return self.a + self.b * (t - x) + self.c * (t - x) ** 2 + self.d * \
+               (t - x) ** 3
+
+
+class LinearSpline(Spline):
+
+    def __init__(self, x, y, m):
+        Spline.__init__(self, x)
+        self.y = y
+        self.m = m
+
+    def get_y(self, t):
+        return self.m * (t - self.x) + self.y
+
+
+class GUI(Animation):
+
+    B_DUPLICATE, B_WRITE, B_DELETE, B_LINEAR, B_CUBIC, B_SPLIT = range(6)
+    S_NONE, S_LEFT, S_RIGHT = range(3)
+
+    def __init__(self, parent):
+        Animation.__init__(self, parent, unfiltered=True)
+        self.audio = self.get_audio()
+        self.display = self.get_game().display
+        self.display_surface = self.get_display_surface()
+        self.time_filter = self.get_game().time_filter
+        self.delegate = self.get_delegate()
+        self.split = self.S_NONE
+        self.success_indicator_active = True
+        self.success_indicator_blink_count = 0
+        self.load_configuration()
+        self.font = Font(None, self.label_size)
+        self.prompt = Prompt(self)
+        self.set_temporary_file()
+        self.set_background()
+        self.set_success_indicator()
+        self.set_plot_rect()
+        self.set_marker_frame()
+        self.set_buttons()
+        self.active = False
+        self.set_nodeset_index()
+        self.set_y_range()
+        self.set_markers()
+        self.subscribe(self.respond_to_command)
+        self.subscribe(self.respond_to_mouse_down, MOUSEBUTTONDOWN)
+        self.subscribe(self.respond_to_key, KEYDOWN)
+        self.register(self.show_success_indicator, interval=100)
+        self.register(self.save_temporary_file, interval=10000)
+        self.play(self.save_temporary_file)
+
+    def load_configuration(self):
+        config = self.get_configuration("interpolator-gui")
+        self.label_size = config["label-size"]
+        self.axis_label_count = config["axis-label-count"]
+        self.margin = config["margin"]
+        self.curve_color = config["curve-color"]
+        self.marker_size = config["marker-size"]
+        self.marker_color = config["marker-color"]
+        self.label_precision = config["label-precision"]
+        self.template_nodeset = config["template-nodeset"]
+        self.template_nodeset_name = config["template-nodeset-name"]
+        self.flat_y_range = config["flat-y-range"]
+
+    def set_temporary_file(self):
+        self.temporary_file = open(join(gettempdir(), "pgfw-config"), "w")
+
+    def set_background(self):
+        surface = Surface(self.display_surface.get_size())
+        surface.fill((0, 0, 0))
+        self.background = surface
+
+    def set_success_indicator(self):
+        surface = Surface((10, 10))
+        surface.fill((0, 255, 0))
+        rect = surface.get_rect()
+        rect.topleft = self.display_surface.get_rect().topleft
+        self.success_indicator, self.success_indicator_rect = surface, rect
+
+    def set_plot_rect(self):
+        margin = self.margin
+        self.plot_rect = self.display_surface.get_rect().inflate(-margin,
+                                                                 -margin)
+
+    def set_marker_frame(self):
+        size = self.marker_size
+        surface = Surface((size, size))
+        transparent_color = (255, 0, 255)
+        surface.fill(transparent_color)
+        surface.set_colorkey(transparent_color)
+        line_color = self.marker_color
+        aaline(surface, line_color, (0, 0), (size - 1, size - 1))
+        aaline(surface, line_color, (0, size - 1), (size - 1, 0))
+        self.marker_frame = surface
+
+    def set_buttons(self):
+        self.buttons = buttons = []
+        text = "Duplicate", "Write", "Delete", "Linear", "Cubic", "Split: No"
+        x = 0
+        for instruction in text:
+            buttons.append(Button(self, instruction, x))
+            x += buttons[-1].location.w + 10
+
+    def set_nodeset_index(self, increment=None, index=None):
+        parent = self.parent
+        if index is None:
+            if not increment:
+                index = 0
+            else:
+                index = self.nodeset_index + increment
+                limit = len(parent) - 1
+                if index > limit:
+                    index = 0
+                elif index < 0:
+                    index = limit
+        self.nodeset_index = index
+        self.set_nodeset_label()
+
+    def set_nodeset_label(self):
+        surface = self.font.render(self.get_nodeset().name, True, (0, 0, 0),
+                                   (255, 255, 255))
+        rect = surface.get_rect()
+        rect.bottomright = self.display_surface.get_rect().bottomright
+        self.nodeset_label, self.nodeset_label_rect = surface, rect
+
+    def get_nodeset(self):
+        if not len(self.parent):
+            self.parent.add_nodeset(self.template_nodeset_name,
+                                    self.template_nodeset)
+            self.set_nodeset_index(0)
+        return self.parent[self.nodeset_index]
+
+    def set_y_range(self):
+        width = self.plot_rect.w
+        nodeset = self.get_nodeset()
+        self.y_range = y_range = [nodeset[0].y, nodeset[-1].y]
+        x = 0
+        while x < width:
+            y = nodeset.get_y(self.get_function_coordinates(x)[0])
+            if y < y_range[0]:
+                y_range[0] = y
+            elif y > y_range[1]:
+                y_range[1] = y
+            x += width * .01
+        if y_range[1] - y_range[0] == 0:
+            y_range[1] += self.flat_y_range
+        if self.split:
+            self.adjust_for_split(y_range, nodeset)
+        self.set_axis_labels()
+
+    def get_function_coordinates(self, xp=0, yp=0):
+        nodeset = self.get_nodeset()
+        x_min, x_max, (y_min, y_max) = nodeset[0].x, nodeset[-1].x, self.y_range
+        rect = self.plot_rect
+        x = float(xp) / (rect.right - rect.left) * (x_max - x_min) + x_min
+        y = float(yp) / (rect.bottom - rect.top) * (y_min - y_max) + y_max
+        return x, y
+
+    def adjust_for_split(self, y_range, nodeset):
+        middle = nodeset[0].y if self.split == self.S_LEFT else nodeset[-1].y
+        below, above = middle - y_range[0], y_range[1] - middle
+        if below > above:
+            y_range[1] += below - above
+        else:
+            y_range[0] -= above - below
+
+    def set_axis_labels(self):
+        self.axis_labels = labels = []
+        nodeset, formatted, render, rect, yr = (self.get_nodeset(),
+                                                self.get_formatted_measure,
+                                                self.font.render,
+                                                self.plot_rect, self.y_range)
+        for ii, node in enumerate(nodeset[0::len(nodeset) - 1]):
+            xs = render(formatted(node.x), True, (0, 0, 0), (255, 255, 255))
+            xsr = xs.get_rect()
+            xsr.top = rect.bottom
+            if not ii:
+                xsr.left = rect.left
+            else:
+                xsr.right = rect.right
+            ys = render(formatted(yr[ii]), True, (0, 0, 0), (255, 255, 255))
+            ysr = ys.get_rect()
+            ysr.right = rect.left
+            if not ii:
+                ysr.bottom = rect.bottom
+            else:
+                ysr.top = rect.top
+            labels.append(((xs, xsr), (ys, ysr)))
+
+    def get_formatted_measure(self, measure):
+        return "%s" % float(("%." + str(self.label_precision) + "g") % measure)
+
+    def deactivate(self):
+        self.active = False
+        self.time_filter.open()
+        self.audio.muted = self.saved_mute_state
+        self.display.set_mouse_visibility(self.saved_mouse_state)
+
+    def respond_to_command(self, event):
+        compare = self.delegate.compare
+        if compare(event, "toggle-interpolator"):
+            self.toggle()
+        elif self.active:
+            if compare(event, "reset-game"):
+                self.deactivate()
+            elif compare(event, "quit"):
+                self.get_game().end(event)
+
+    def toggle(self):
+        if self.active:
+            self.deactivate()
+        else:
+            self.activate()
+
+    def activate(self):
+        self.active = True
+        self.time_filter.close()
+        self.saved_mute_state = self.audio.muted
+        self.audio.mute()
+        self.draw()
+        self.saved_mouse_state = self.display.set_mouse_visibility(True)
+
+    def respond_to_mouse_down(self, event):
+        redraw = False
+        if self.active and not self.prompt.active:
+            nodeset_rect = self.nodeset_label_rect
+            plot_rect = self.plot_rect
+            if event.button == 1:
+                pos = event.pos
+                if nodeset_rect.collidepoint(pos):
+                    self.set_nodeset_index(1)
+                    redraw = True
+                elif self.axis_labels[0][0][1].collidepoint(pos):
+                    text = "{0} {1}".format(*map(self.get_formatted_measure,
+                                                 self.get_nodeset()[0]))
+                    self.prompt.activate(text, self.resize_nodeset, 0)
+                elif self.axis_labels[1][0][1].collidepoint(pos):
+                    text = "{0} {1}".format(*map(self.get_formatted_measure,
+                                                 self.get_nodeset()[-1]))
+                    self.prompt.activate(text, self.resize_nodeset, -1)
+                else:
+                    bi = self.collide_buttons(pos)
+                    if bi is not None:
+                        if bi == self.B_WRITE:
+                            self.get_configuration().write()
+                            self.play(self.show_success_indicator)
+                        elif bi in (self.B_LINEAR, self.B_CUBIC):
+                            nodeset = self.get_nodeset()
+                            if bi == self.B_LINEAR:
+                                nodeset.set_interpolation_method(Nodeset.LINEAR)
+                            else:
+                                nodeset.set_interpolation_method(Nodeset.CUBIC)
+                            self.store_in_configuration()
+                            redraw = True
+                        elif bi == self.B_DUPLICATE:
+                            self.prompt.activate("", self.add_nodeset)
+                        elif bi == self.B_DELETE and len(self.parent) > 1:
+                            self.parent.remove(self.get_nodeset())
+                            self.set_nodeset_index(1)
+                            self.store_in_configuration()
+                            redraw = True
+                        elif bi == self.B_SPLIT:
+                            self.toggle_split()
+                            redraw = True
+                    elif plot_rect.collidepoint(pos) and \
+                             not self.collide_markers(pos):
+                        xp, yp = pos[0] - plot_rect.left, pos[1] - plot_rect.top
+                        self.get_nodeset().add_node(
+                            self.get_function_coordinates(xp, yp))
+                        self.store_in_configuration()
+                        redraw = True
+            elif event.button == 3:
+                pos = event.pos
+                if nodeset_rect.collidepoint(pos):
+                    self.set_nodeset_index(-1)
+                    redraw = True
+                elif plot_rect.collidepoint(pos):
+                    marker = self.collide_markers(pos)
+                    if marker:
+                        self.get_nodeset().remove(marker.node)
+                        self.store_in_configuration()
+                        redraw = True
+        elif self.active and self.prompt.active and \
+                 not self.prompt.rect.collidepoint(event.pos):
+            self.prompt.deactivate()
+            redraw = True
+        if redraw:
+            self.set_y_range()
+            self.set_markers()
+            self.draw()
+
+    def resize_nodeset(self, text, index):
+        result = match("^\s*(-{,1}\d*\.{,1}\d*)\s+(-{,1}\d*\.{,1}\d*)\s*$",
+                       text)
+        if result:
+            try:
+                nodeset = self.get_nodeset()
+                x, y = map(float, result.group(1, 2))
+                if (index == -1 and x > nodeset[0].x) or \
+                       (index == 0 and x < nodeset[-1].x):
+                    nodeset[index].y = y
+                    if index == -1:
+                        nodeset.resize(nodeset[0].x, x - nodeset[0].x)
+                    else:
+                        nodeset.resize(x, nodeset[-1].x - x)
+                    self.store_in_configuration()
+                    self.set_y_range()
+                    self.set_axis_labels()
+                    self.set_markers()
+                    self.draw()
+                    return True
+            except ValueError:
+                return False
+
+    def collide_buttons(self, pos):
+        for ii, button in enumerate(self.buttons):
+            if button.location.collidepoint(pos):
+                return ii
+
+    def store_in_configuration(self):
+        config = self.get_configuration()
+        section = "interpolate"
+        config.clear_section(section)
+        for nodeset in self.parent:
+            code = "L" if nodeset.interpolation_method == Nodeset.LINEAR else \
+                   "C"
+            for ii, node in enumerate(nodeset):
+                if ii > 0:
+                    code += ","
+                code += " {0} {1}".format(*map(self.get_formatted_measure,
+                                               node))
+            if not config.has_section(section):
+                config.add_section(section)
+            config.set(section, nodeset.name, code)
+
+    def toggle_split(self):
+        self.split += 1
+        if self.split > self.S_RIGHT:
+            self.split = self.S_NONE
+        self.buttons[self.B_SPLIT].set_frame(["Split: No", "Split: L",
+                                              "Split: R"][self.split])
+
+    def add_nodeset(self, name):
+        nodeset = self.get_nodeset()
+        self.set_nodeset_index(index=self.parent.add_nodeset(\
+            name, nodeset, nodeset.interpolation_method))
+        self.store_in_configuration()
+        self.draw()
+        return True
+
+    def collide_markers(self, pos):
+        for marker in self.markers:
+            if marker.location.collidepoint(pos):
+                return marker
+
+    def set_markers(self):
+        self.markers = markers = []
+        for node in self.get_nodeset()[1:-1]:
+            markers.append(Marker(self, node))
+            markers[-1].location.center = self.get_plot_coordinates(*node)
+
+    def get_plot_coordinates(self, x=0, y=0):
+        nodeset = self.get_nodeset()
+        x_min, x_max, (y_min, y_max) = nodeset[0].x, nodeset[-1].x, self.y_range
+        x_ratio = float(x - x_min) / (x_max - x_min)
+        rect = self.plot_rect
+        xp = x_ratio * (rect.right - rect.left) + rect.left
+        y_ratio = float(y - y_min) / (y_max - y_min)
+        yp = rect.bottom - y_ratio * (rect.bottom - rect.top)
+        return xp, yp
+
+    def draw(self):
+        display_surface = self.display_surface
+        display_surface.blit(self.background, (0, 0))
+        display_surface.blit(self.nodeset_label, self.nodeset_label_rect)
+        self.draw_axes()
+        self.draw_function()
+        self.draw_markers()
+        self.draw_buttons()
+
+    def draw_axes(self):
+        display_surface = self.display_surface
+        for xl, yl in self.axis_labels:
+            display_surface.blit(*xl)
+            display_surface.blit(*yl)
+
+    def draw_function(self):
+        rect = self.plot_rect
+        surface = self.display_surface
+        nodeset = self.get_nodeset()
+        step = 1
+        for x in xrange(rect.left, rect.right + step, step):
+            ii = x - rect.left
+            fx = nodeset.get_y(self.get_function_coordinates(ii)[0])
+            y = self.get_plot_coordinates(y=fx)[1]
+            if ii > 0:
+                aaline(surface, self.curve_color, (x - step, last_y), (x, y))
+            last_y = y
+
+    def draw_markers(self):
+        for marker in self.markers:
+            marker.update()
+
+    def draw_buttons(self):
+        for button in self.buttons:
+            button.update()
+
+    def respond_to_key(self, event):
+        if self.prompt.active:
+            prompt = self.prompt
+            if event.key == K_RETURN:
+                if prompt.callback[0](prompt.text, *prompt.callback[1]):
+                    prompt.deactivate()
+            elif event.key == K_BACKSPACE:
+                prompt.text = prompt.text[:-1]
+                prompt.update()
+                prompt.draw_text()
+            elif (event.unicode.isalnum() or event.unicode.isspace() or \
+                  event.unicode in (".", "-", "_")) and len(prompt.text) < \
+                  prompt.character_limit:
+                prompt.text += event.unicode
+                prompt.update()
+                prompt.draw_text()
+
+    def show_success_indicator(self):
+        self.draw()
+        if self.success_indicator_blink_count > 1:
+            self.success_indicator_blink_count = 0
+            self.halt(self.show_success_indicator)
+        else:
+            if self.success_indicator_active:
+                self.display_surface.blit(self.success_indicator,
+                                          self.success_indicator_rect)
+            if self.success_indicator_active:
+                self.success_indicator_blink_count += 1
+            self.success_indicator_active = not self.success_indicator_active
+
+    def save_temporary_file(self):
+        fp = self.temporary_file
+        fp.seek(0)
+        fp.truncate()
+        self.get_configuration().write(fp)
+
+    def rearrange(self):
+        self.set_background()
+        self.set_success_indicator()
+        self.set_plot_rect()
+        self.set_markers()
+        self.set_nodeset_label()
+        self.set_axis_labels()
+        self.set_buttons()
+        self.prompt.reset()
+
+class Marker(Sprite):
+
+    def __init__(self, parent, node):
+        Sprite.__init__(self, parent)
+        self.add_frame(parent.marker_frame)
+        self.node = node
+
+
+class Button(Sprite):
+
+    def __init__(self, parent, text, left):
+        Sprite.__init__(self, parent)
+        self.set_frame(text)
+        self.location.bottomleft = left, \
+                                   self.get_display_surface().get_rect().bottom
+
+    def set_frame(self, text):
+        self.clear_frames()
+        self.add_frame(self.parent.font.render(text, True, (0, 0, 0),
+                                               (255, 255, 255)))
+
+
+class Prompt(Sprite):
+
+    def __init__(self, parent):
+        Sprite.__init__(self, parent)
+        self.load_configuration()
+        self.font = Font(None, self.text_size)
+        self.reset()
+        self.deactivate()
+
+    def deactivate(self):
+        self.active = False
+
+    def load_configuration(self):
+        config = self.get_configuration("interpolator-gui")
+        self.size = config["prompt-size"]
+        self.border_color = config["prompt-border-color"]
+        self.border_width = config["prompt-border-width"]
+        self.character_limit = config["prompt-character-limit"]
+        self.text_size = config["prompt-text-size"]
+
+    def reset(self):
+        self.set_frame()
+        self.place()
+
+    def set_frame(self):
+        self.clear_frames()
+        surface = Surface(self.size)
+        self.add_frame(surface)
+        surface.fill(self.border_color)
+        width = self.border_width * 2
+        surface.fill((0, 0, 0), surface.get_rect().inflate(-width, -width))
+
+    def place(self):
+        self.location.center = self.display_surface.get_rect().center
+
+    def activate(self, text, callback, *args):
+        self.active = True
+        self.text = str(text)
+        self.callback = callback, args
+        self.update()
+        self.draw_text()
+
+    def draw_text(self):
+        surface = self.font.render(self.text, True, (255, 255, 255), (0, 0, 0))
+        rect = surface.get_rect()
+        rect.center = self.location.center
+        self.display_surface.blit(surface, rect)
diff --git a/shaken_and_spit_out/pgfw/Mainloop.py b/shaken_and_spit_out/pgfw/Mainloop.py
new file mode 100644 (file)
index 0000000..419a966
--- /dev/null
@@ -0,0 +1,108 @@
+from pygame import display
+from pygame.font import Font
+from pygame.time import get_ticks, wait
+
+from GameChild import GameChild
+
+class Mainloop(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.overflow = 0
+        self.frame_count = 1
+        self.actual_frame_duration = 0
+        self.frames_this_second = 0
+        self.last_framerate_display = 0
+        self.load_configuration()
+        self.init_framerate_display()
+        self.last_ticks = get_ticks()
+        self.stopping = False
+
+    def load_configuration(self):
+        config = self.get_configuration("display")
+        self.target_frame_duration = config["frame-duration"]
+        self.wait_duration = config["wait-duration"]
+        self.skip_frames = config["skip-frames"]
+        self.show_framerate = config["show-framerate"]
+        self.framerate_text_size = config["framerate-text-size"]
+        self.framerate_text_color = config["framerate-text-color"]
+        self.framerate_text_background = config["framerate-text-background"]
+        self.framerate_display_flag = config["framerate-display-flag"]
+
+    def init_framerate_display(self):
+        if self.framerate_display_active():
+            screen = self.get_screen()
+            self.last_framerate_count = 0
+            self.framerate_topright = screen.get_rect().topright
+            self.display_surface = screen
+            self.font = Font(None, self.framerate_text_size)
+            self.font.set_bold(True)
+            self.render_framerate()
+
+    def framerate_display_active(self):
+        return self.check_command_line(self.framerate_display_flag) or \
+               self.show_framerate
+
+    def render_framerate(self):
+        text = self.font.render(str(self.last_framerate_count), False,
+                                self.framerate_text_color,
+                                self.framerate_text_background)
+        rect = text.get_rect()
+        rect.topright = self.framerate_topright
+        self.framerate_text = text
+        self.framerate_text_rect = rect
+
+    def run(self):
+        while not self.stopping:
+            self.advance_frame()
+            self.update_frame_duration()
+            self.update_overflow()
+        self.stopping = False
+
+    def advance_frame(self):
+        refresh = False
+        while self.frame_count > 0:
+            refresh = True
+            self.parent.frame()
+            if self.framerate_display_active():
+                self.update_framerate()
+            self.frame_count -= 1
+            if not self.skip_frames:
+                break
+        if refresh:
+            display.update()
+
+    def update_frame_duration(self):
+        last_ticks = self.last_ticks
+        actual_frame_duration = get_ticks() - last_ticks
+        last_ticks = get_ticks()
+        while actual_frame_duration < self.target_frame_duration:
+            wait(self.wait_duration)
+            actual_frame_duration += get_ticks() - last_ticks
+            last_ticks = get_ticks()
+        self.actual_frame_duration = actual_frame_duration
+        self.last_ticks = last_ticks
+
+    def update_overflow(self):
+        self.frame_count = 1
+        target_frame_duration = self.target_frame_duration
+        overflow = self.overflow
+        overflow += self.actual_frame_duration - target_frame_duration
+        while overflow > target_frame_duration:
+            self.frame_count += 1
+            overflow -= target_frame_duration
+        overflow = self.overflow
+
+    def update_framerate(self):
+        count = self.frames_this_second + 1
+        if get_ticks() - self.last_framerate_display > 1000:
+            if count != self.last_framerate_count:
+                self.last_framerate_count = count
+                self.render_framerate()
+            self.last_framerate_display = get_ticks()
+            count = 0
+        self.display_surface.blit(self.framerate_text, self.framerate_text_rect)
+        self.frames_this_second = count
+
+    def stop(self):
+        self.stopping = True
diff --git a/shaken_and_spit_out/pgfw/Profile.py b/shaken_and_spit_out/pgfw/Profile.py
new file mode 100644 (file)
index 0000000..87a74d2
--- /dev/null
@@ -0,0 +1,26 @@
+import cProfile
+from time import strftime
+from os import mkdir
+from os.path import join, exists
+
+from GameChild import GameChild
+
+class Profile(cProfile.Profile, GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        cProfile.Profile.__init__(self)
+        if self.requested():
+            self.enable()
+
+    def requested(self):
+        return self.check_command_line("p")
+
+    def end(self):
+        if self.requested():
+            root = "stat/"
+            if not exists(root):
+                mkdir(root)
+            self.disable()
+            self.create_stats()
+            self.dump_stats(join(root, strftime("%Y%m%d-%H%M_%S.stat")))
diff --git a/shaken_and_spit_out/pgfw/ScreenGrabber.py b/shaken_and_spit_out/pgfw/ScreenGrabber.py
new file mode 100644 (file)
index 0000000..2058683
--- /dev/null
@@ -0,0 +1,41 @@
+from os import makedirs
+from os.path import exists, join
+from sys import exc_info
+from time import strftime
+
+from pygame import image
+
+from GameChild import *
+from Input import *
+
+class ScreenGrabber(GameChild):
+
+    def __init__(self, game):
+        GameChild.__init__(self, game)
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.subscribe(self.save_display)
+
+    def load_configuration(self):
+        config = self.get_configuration("screen-captures")
+        self.save_path = config["path"]
+        self.file_name_format = config["file-name-format"]
+        self.file_extension = config["file-extension"]
+
+    def save_display(self, event):
+        if self.delegate.compare(event, "capture-screen"):
+            directory = self.save_path
+            try:
+                if not exists(directory):
+                    makedirs(directory)
+                name = self.build_name()
+                path = join(directory, name)
+                capture = image.save(self.get_screen(), path)
+                self.print_debug("Saved screen capture to %s" % (path))
+            except:
+                self.print_debug("Couldn't save screen capture to %s, %s" %\
+                                 (directory, exc_info()[1]))
+
+    def build_name(self):
+        return "{0}.{1}".format(strftime(self.file_name_format),
+                                self.file_extension)
diff --git a/shaken_and_spit_out/pgfw/Setup.py b/shaken_and_spit_out/pgfw/Setup.py
new file mode 100644 (file)
index 0000000..04cf753
--- /dev/null
@@ -0,0 +1,131 @@
+from os import walk, remove
+from os.path import sep, join, exists, normpath
+from re import findall, sub
+from distutils.core import setup
+from distutils.command.install import install
+from pprint import pprint
+from fileinput import FileInput
+from re import sub, match
+
+from Configuration import *
+
+class Setup:
+
+    config = Configuration()
+    manifest_path = "MANIFEST"
+
+    def __init__(self):
+        pass
+
+    def remove_old_mainfest(self):
+        path = self.manifest_path
+        if exists(path):
+            remove(path)
+
+    def build_package_list(self):
+        packages = []
+        config = self.config.get_section("setup")
+        locations = [config["package-root"]] + config["additional-packages"]
+        for location in locations:
+            if exists(location):
+                for root, dirs, files in walk(location, followlinks=True):
+                    packages.append(root.replace(sep, "."))
+        return packages
+
+    def build_data_map(self):
+        include = []
+        config = self.config.get_section("setup")
+        exclude = map(normpath, config["data-exclude"])
+        for root, dirs, files in walk("."):
+            dirs = self.remove_excluded(dirs, root, exclude)
+            files = [join(root, f) for f in self.remove_excluded(files, root,
+                                                                 exclude)]
+            if files:
+                include.append((normpath(join(config["installation-path"],
+                                              root)), files))
+        return include
+
+    def remove_excluded(self, paths, root, exclude):
+        removal = []
+        for path in paths:
+            if normpath(join(root, path)) in exclude:
+                removal.append(path)
+        for path in removal:
+            paths.remove(path)
+        return paths
+
+    def translate_title(self):
+        config = self.config.get_section("setup")
+        title = config["title"].replace(" ", config["whitespace-placeholder"])
+        return sub("[^\w-]", config["special-char-placeholder"], title)
+
+    def build_description(self):
+        description = ""
+        path = self.config.get("setup", "description-file")
+        if exists(path):
+            description = "\n%s\n%s\n%s" % (file(path).read(),
+                                            "Changelog\n=========",
+                                            self.translate_changelog())
+        return description
+
+    def translate_changelog(self):
+        translation = ""
+        path = self.config.get("setup", "changelog")
+        if exists(path):
+            lines = file(path).readlines()
+            package_name = lines[0].split()[0]
+            for line in lines:
+                line = line.strip()
+                if line.startswith(package_name):
+                    version = findall("\((.*)\)", line)[0]
+                    translation += "\n%s\n%s\n" % (version, "-" * len(version))
+                elif line and not line.startswith("--"):
+                    if line.startswith("*"):
+                        translation += line + "\n"
+                    else:
+                        translation += "  " + line + "\n"
+        return translation
+
+    def setup(self, windows=[], options={}):
+       print "running setup..."
+        self.remove_old_mainfest()
+        config = self.config.get_section("setup")
+       scripts = []
+       if config["init-script"]:
+           scripts.append(config["init-script"])
+        setup(cmdclass={"install": insert_resource_path},
+              name=self.translate_title(),
+              packages=self.build_package_list(),
+              scripts=scripts,
+              data_files=self.build_data_map(),
+              requires=config["requirements"],
+              version=config["version"],
+              description=config["summary"],
+              classifiers=config["classifiers"],
+              long_description=self.build_description(),
+              license=config["license"],
+              platforms=config["platforms"],
+              author=config["contact-name"],
+              author_email=config["contact-email"],
+              url=config["url"],
+             windows=windows,
+             options=options)
+
+
+class insert_resource_path(install):
+
+    def run(self):
+        install.run(self)
+        self.edit_game_object_file()
+
+    def edit_game_object_file(self):
+        config = Configuration().get_section("setup")
+        for path in self.get_outputs():
+            if path.endswith(config["main-object"]):
+                for line in FileInput(path, inplace=True):
+                    pattern = "^ *{0} *=.*".\
+                              format(config["resource-path-identifier"])
+                    if match(pattern, line):
+                        line = sub("=.*$", "= \"{0}\"".\
+                                   format(config["installation-path"]), line)
+                    print line.strip("\n")
diff --git a/shaken_and_spit_out/pgfw/SetupWin.py b/shaken_and_spit_out/pgfw/SetupWin.py
new file mode 100644 (file)
index 0000000..232322b
--- /dev/null
@@ -0,0 +1,69 @@
+from os import makedirs, walk, sep, remove
+from os.path import join, dirname, basename, exists
+from shutil import rmtree, copy, rmtree
+from itertools import chain
+from zipfile import ZipFile
+
+import py2exe
+
+from Setup import Setup
+
+class SetupWin(Setup):
+
+    def __init__(self):
+        Setup.__init__(self)
+        self.replace_isSystemDLL()
+
+    def replace_isSystemDLL(self):
+        origIsSystemDLL = py2exe.build_exe.isSystemDLL
+        def isSystemDLL(pathname):
+            if basename(pathname).lower() in ("libogg-0.dll", "sdl_ttf.dll"):
+                return 0
+            return origIsSystemDLL(pathname)
+        py2exe.build_exe.isSystemDLL = isSystemDLL
+
+    def setup(self):
+        config = self.config.get_section("setup")
+       windows = [{}]
+       if config["init-script"]:
+           windows[0]["script"] = config["init-script"]
+       if config["windows-icon-path"]:
+           windows[0]["icon-resources"] = [(1, config["windows-icon-path"])]
+        Setup.setup(self, windows,
+                    {"py2exe": {"packages": self.build_package_list(),
+                                "dist_dir": config["windows-dist-path"]}})
+        rmtree("build")
+        self.copy_data_files()
+        self.create_archive()
+
+    def copy_data_files(self):
+       root = self.config.get("setup", "windows-dist-path")
+        for path in chain(*zip(*self.build_data_map())[1]):
+            dest = join(root, dirname(path))
+            if not exists(dest):
+                makedirs(dest)
+            copy(path, dest)
+       self.include_readme(root)
+
+    def include_readme(self, root):
+       name = "README"
+       if exists(name):
+           readme = open(name, "r")
+           reformatted = open(join(root, name + ".txt"), "w")
+           for line in open(name, "r"):
+               reformatted.write(line.rstrip() + "\r\n")
+
+    def create_archive(self):
+        config = self.config.get_section("setup")
+        title = self.translate_title() + "-" + config["version"] + "-win"
+        archive_name = title + ".zip"
+        archive = ZipFile(archive_name, "w")
+        destination = config["windows-dist-path"]
+        for root, dirs, names in walk(destination):
+            for name in names:
+                path = join(root, name)
+                archive.write(path, path.replace(destination, title + sep))
+        archive.close()
+        copy(archive_name, "dist")
+        remove(archive_name)
+        rmtree(destination)
diff --git a/shaken_and_spit_out/pgfw/Sprite.py b/shaken_and_spit_out/pgfw/Sprite.py
new file mode 100644 (file)
index 0000000..5ceb7c5
--- /dev/null
@@ -0,0 +1,358 @@
+from os import listdir
+from os.path import isfile, join
+from sys import exc_info, stdout
+from glob import glob
+from traceback import print_exc, print_stack
+
+from pygame import Color, Rect, Surface
+from pygame.image import load
+from pygame.transform import flip
+from pygame.locals import *
+
+from Animation import Animation
+from Vector import Vector
+
+class Sprite(Animation):
+
+    def __init__(self, parent, framerate=None):
+        Animation.__init__(self, parent, self.shift_frame, framerate)
+        self.frames = []
+        self.mirrored = False
+        self.hidden = False
+        self.alpha = 255
+        self.locations = [Location(self)]
+        self.framesets = [Frameset(self, framerate=framerate)]
+        self.set_frameset(0)
+        self.motion_overflow = Vector()
+        self.display_surface = self.get_display_surface()
+
+    def __getattr__(self, name):
+        if name in ("location", "rect"):
+            return self.locations[0]
+        if hasattr(Animation, "__getattr__"):
+            return Animation.__getattr__(self, name)
+        raise AttributeError, name
+
+    def set_frameset(self, identifier):
+        if isinstance(identifier, str):
+            for ii, frameset in enumerate(self.framesets):
+                if frameset.name == identifier:
+                    identifier = ii
+                    break
+        self.frameset_index = identifier
+        self.register_interval()
+        self.update_location_size()
+        if self.get_current_frameset().length() > 1:
+            self.play()
+
+    def register_interval(self):
+        self.register(self.shift_frame,
+                      interval=self.get_current_frameset().framerate)
+
+    def get_current_frameset(self):
+        return self.framesets[self.frameset_index]
+
+    def update_location_size(self):
+        size = self.get_current_frameset().rect.size
+        for location in self.locations:
+            location.size = size
+            location.fader.init_surface()
+
+    def set_framerate(self, framerate):
+        self.get_current_frameset().set_framerate(framerate)
+        self.register_interval()
+
+    def load_from_path(self, path, transparency=False, ppa=True, key=None,
+                       extension=None, omit=False):
+        if isfile(path):
+            paths = [path]
+        else:
+            if extension:
+                paths = sorted(glob(join(path, "*." + extension)))
+            else:
+                paths = [join(path, name) for name in sorted(listdir(path))]
+        for path in paths:
+            img = load(path)
+            if transparency:
+                if ppa:
+                    frame = img.convert_alpha()
+                else:
+                    frame = self.fill_colorkey(img, key)
+            else:
+                frame = img.convert()
+            self.add_frame(frame, omit)
+
+    def fill_colorkey(self, img, key=None):
+        if not key:
+            key = (255, 0, 255)
+        img = img.convert_alpha()
+        frame = Surface(img.get_size())
+        frame.fill(key)
+        frame.set_colorkey(key)
+        frame.blit(img, (0, 0))
+        return frame
+
+    def add_frame(self, frame, omit=False):
+        self.frames.append(frame)
+        frame.set_alpha(self.alpha)
+        if not omit:
+            frameset = self.get_current_frameset()
+            frameset.add_index(self.frames.index(frame))
+            self.update_location_size()
+            if frameset.length() > 1:
+                self.play()
+
+    def shift_frame(self):
+        self.get_current_frameset().shift()
+
+    def get_current_frame(self):
+        return self.frames[self.get_current_frameset().get_current_id()]
+
+    def move(self, dx=0, dy=0):
+        for location in self.locations:
+            location.move_ip(dx, dy)
+
+    def reset_motion_overflow(self):
+        for location in self.locations:
+            location.reset_motion_overflow()
+
+    def collide(self, other):
+        if not isinstance(other, Rect):
+            other = other.rect
+        for location in self.locations:
+            if location.colliderect(other):
+                return location
+
+    def mirror(self):
+        frames = self.frames
+        for ii, frame in enumerate(frames):
+             frames[ii] = flip(frame, True, False)
+        self.mirrored = not self.mirrored
+
+    def clear_frames(self):
+        self.frames = []
+        for frameset in self.framesets:
+            frameset.order = []
+            frameset.reset()
+            frameset.measure_rect()
+
+    def add_location(self, topleft=None, offset=(0, 0), count=1, base=0):
+        if topleft is not None:
+            for ii in xrange(count):
+                self.locations.append(Location(
+                    self, Rect(topleft, self.locations[0].size)))
+        else:
+            base = self.locations[base]
+            current_offset = list(offset)
+            for ii in xrange(count):
+                self.locations.append(Location(self,
+                                               base.move(*current_offset)))
+                current_offset[0] += offset[0]
+                current_offset[1] += offset[1]
+        return self.locations[-1]
+
+    def fade(self, length=0, out=None, index=None):
+        if index is None:
+            for location in self.locations:
+                location.fader.start(length, out)
+        else:
+            self.locations[index].fader.start(length, out)
+
+    def set_alpha(self, alpha):
+        self.alpha = alpha
+        for frame in self.frames:
+            frame.set_alpha(alpha)
+        for location in self.locations:
+            location.fader.set_alpha()
+
+    def add_frameset(self, order, framerate=None, name=None):
+        frameset = Frameset(self, order, framerate, name)
+        self.framesets.append(frameset)
+        return frameset
+
+    def hide(self):
+        self.hidden = True
+
+    def unhide(self):
+        self.hidden = False
+
+    def remove_locations(self, location=None):
+        if location:
+            self.locations.remove(location)
+        else:
+            self.locations = self.locations[:1]
+
+    def reverse(self, frameset=None):
+        if frameset:
+            frameset.reverse()
+        else:
+            for frameset in self.framesets:
+                frameset.reverse()
+
+    def update(self):
+        Animation.update(self)
+        self.draw()
+
+    def draw(self):
+        for location in self.locations:
+            location.fader.draw()
+
+
+class Location(Rect):
+
+    def __init__(self, sprite, rect=(0, 0, 0, 0)):
+        self.sprite = sprite
+        Rect.__init__(self, rect)
+        self.motion_overflow = Vector()
+        self.fader = Fader(self)
+
+    def move_ip(self, dx, dy):
+        if isinstance(dx, float) or isinstance(dy, float):
+            excess = self.update_motion_overflow(dx, dy)
+            Rect.move_ip(self, int(dx) + excess[0], int(dy) + excess[1])
+        else:
+            Rect.move_ip(self, dx, dy)
+
+    def update_motion_overflow(self, dx, dy):
+        overflow = self.motion_overflow
+        overflow.move(dx - int(dx), dy - int(dy))
+        excess = map(int, overflow)
+        overflow[0] -= int(overflow[0])
+        overflow[1] -= int(overflow[1])
+        return excess
+
+    def reset_motion_overflow(self):
+        self.motion_overflow.place_at_origin()
+
+
+class Fader(Surface):
+
+    def __init__(self, location):
+        self.location = location
+        self.time_filter = location.sprite.get_game().time_filter
+        self.reset()
+
+    def reset(self):
+        self.init_surface()
+        self.fade_remaining = None
+
+    def init_surface(self):
+        Surface.__init__(self, self.location.size)
+        if self.location.sprite.frames:
+            background = Surface(self.get_size())
+            sprite = self.location.sprite
+            key = sprite.get_current_frame().get_colorkey() or (255, 0, 255)
+            self.set_colorkey(key)
+            background.fill(key)
+            self.background = background
+            self.set_alpha()
+
+    def set_alpha(self, alpha=None):
+        if alpha is None:
+            alpha = self.location.sprite.alpha
+        Surface.set_alpha(self, alpha)
+
+    def start(self, length, out=None):
+        if self.fade_remaining <= 0:
+            alpha = self.get_alpha()
+            maximum = self.location.sprite.alpha
+            if out is None:
+                out = alpha == maximum
+            if out and alpha > 0 or not out and alpha < maximum:
+                self.fade_length = self.fade_remaining = length
+                self.start_time = self.time_filter.get_ticks()
+                self.fading_out = out
+
+    def draw(self):
+        sprite = self.location.sprite
+        if self.fade_remaining >= 0:
+            self.update_alpha()
+            self.clear()
+            frame = sprite.get_current_frame()
+            frame.set_alpha(255)
+            self.blit(frame, (0, 0))
+            frame.set_alpha(sprite.alpha)
+            if not sprite.hidden:
+                self.blit_to_display(self)
+        elif self.fade_remaining is None or self.get_alpha() >= sprite.alpha:
+            if self.fade_remaining >= 0:
+                self.update_alpha()
+            if not sprite.hidden:
+                self.blit_to_display(sprite.get_current_frame())
+
+    def blit_to_display(self, frame):
+        self.location.sprite.display_surface.blit(frame, self.location)
+
+    def update_alpha(self):
+        remaining = self.fade_remaining = self.fade_length - \
+                    (self.time_filter.get_ticks() - self.start_time)
+        ratio = self.fade_length and float(remaining) / self.fade_length
+        if not self.fading_out:
+            ratio = 1 - ratio
+        maximum = self.location.sprite.alpha
+        alpha = int(ratio * maximum)
+        if alpha > maximum:
+            alpha = maximum
+        elif alpha < 0:
+            alpha = 0
+        self.set_alpha(alpha)
+
+    def clear(self):
+        self.blit(self.background, (0, 0))
+
+
+class Frameset():
+
+    def __init__(self, sprite, order=[], framerate=None, name=None):
+        self.sprite = sprite
+        self.name = name
+        self.reversed = False
+        self.order = []
+        self.rect = Rect(0, 0, 0, 0)
+        self.add_index(order)
+        self.set_framerate(framerate)
+        self.reset()
+
+    def add_index(self, order):
+        if isinstance(order, int):
+            order = [order]
+        self.order += order
+        self.measure_rect()
+
+    def set_framerate(self, framerate):
+        self.framerate = framerate
+
+    def reset(self):
+        self.current_index = 0
+
+    def get_current_id(self):
+        return self.order[self.current_index]
+
+    def measure_rect(self):
+        max_width, max_height = 0, 0
+        frames = self.sprite.frames
+        for index in self.order:
+            frame = frames[index]
+            width, height = frame.get_size()
+            max_width = max(width, max_width)
+            max_height = max(height, max_height)
+        self.rect.size = max_width, max_height
+
+    def shift(self):
+        if len(self.order) > 1:
+            self.increment_index()
+
+    def increment_index(self):
+        increment = 1 if not self.reversed else -1
+        index = self.current_index + increment
+        if index < 0:
+            index = self.length() - 1
+        elif index >= self.length():
+            index = 0
+        self.current_index = index
+
+    def length(self):
+        return len(self.order)
+
+    def reverse(self):
+        self.reversed = not self.reversed
diff --git a/shaken_and_spit_out/pgfw/TimeFilter.py b/shaken_and_spit_out/pgfw/TimeFilter.py
new file mode 100644 (file)
index 0000000..e57e3c1
--- /dev/null
@@ -0,0 +1,36 @@
+from pygame.time import get_ticks
+
+from GameChild import GameChild
+
+class TimeFilter(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.ticks = self.unfiltered_ticks = self.last_ticks = get_ticks()
+        self.open()
+
+    def close(self):
+        self.closed = True
+
+    def open(self):
+        self.closed = False
+
+    def get_ticks(self):
+        return self.ticks
+
+    def get_unfiltered_ticks(self):
+        return self.unfiltered_ticks
+
+    def get_last_ticks(self):
+        return self.last_ticks
+
+    def get_last_frame_duration(self):
+        return self.last_frame_duration
+
+    def update(self):
+        ticks = get_ticks()
+        self.last_frame_duration = duration = ticks - self.last_ticks
+        if not self.closed:
+            self.ticks += duration
+        self.unfiltered_ticks += duration
+        self.last_ticks = ticks
diff --git a/shaken_and_spit_out/pgfw/Vector.py b/shaken_and_spit_out/pgfw/Vector.py
new file mode 100644 (file)
index 0000000..819e847
--- /dev/null
@@ -0,0 +1,69 @@
+class Vector(list):
+
+    def __init__(self, x=0, y=0):
+        list.__init__(self, (x, y))
+
+    def __getattr__(self, name):
+        if name == "x":
+            return self[0]
+        elif name == "y":
+            return self[1]
+
+    def __setattr__(self, name, value):
+        if name == "x":
+            self[0] = value
+        elif name == "y":
+            self[1] = value
+        else:
+            list.__setattr__(self, name, value)
+
+    def __add__(self, other):
+        return Vector(self.x + other[0], self.y + other[1])
+
+    __radd__ = __add__
+
+    def __iadd__(self, other):
+        self.x += other[0]
+        self.y += other[1]
+        return self
+
+    def __sub__(self, other):
+        return Vector(self.x - other[0], self.y - other[1])
+
+    def __rsub__(self, other):
+        return Vector(other[0] - self.x, other[1] - self.y)
+
+    def __isub__(self, other):
+        self.x -= other[0]
+        self.y -= other[1]
+        return self
+
+    def __mul__(self, other):
+        return Vector(self.x * other, self.y * other)
+
+    __rmul__ = __mul__
+
+    def __imul__(self, other):
+        self.x *= other
+        self.y *= other
+        return self
+
+    def apply_to_components(self, function):
+        self.x = function(self.x)
+        self.y = function(self.y)
+
+    def place(self, x=None, y=None):
+        if x is not None:
+            self.x = x
+        if y is not None:
+            self.y = y
+
+    def move(self, dx=0, dy=0):
+        if dx:
+            self.x += dx
+        if dy:
+            self.y += dy
+
+    def place_at_origin(self):
+        self.x = 0
+        self.y = 0
diff --git a/shaken_and_spit_out/pgfw/VideoRecorder.py b/shaken_and_spit_out/pgfw/VideoRecorder.py
new file mode 100644 (file)
index 0000000..2aba53c
--- /dev/null
@@ -0,0 +1,71 @@
+from os import makedirs
+from os.path import exists, join
+from tempfile import TemporaryFile
+from time import strftime
+
+from pygame.image import tostring, frombuffer, save
+from pygame.time import get_ticks
+
+from GameChild import GameChild
+
+class VideoRecorder(GameChild):
+
+    def __init__(self, parent):
+        GameChild.__init__(self, parent)
+        self.display_surface = self.get_display_surface()
+        self.delegate = self.get_delegate()
+        self.load_configuration()
+        self.reset()
+        self.subscribe(self.respond)
+
+    def load_configuration(self):
+        config = self.get_configuration("video-recordings")
+        self.root = config["path"]
+        self.directory_name_format = config["directory-name-format"]
+        self.file_extension = config["file-extension"]
+        self.frame_format = config["frame-format"]
+        self.framerate = config["framerate"]
+
+    def reset(self):
+        self.recording = False
+        self.frame_length = None
+        self.frames = None
+        self.last_frame = 0
+
+    def respond(self, event):
+        compare = self.delegate.compare
+        if compare(event, "record-video"):
+            self.toggle_record()
+        elif compare(event, "reset-game"):
+            self.reset()
+
+    def toggle_record(self):
+        recording = not self.recording
+        if recording:
+            self.frame_length = len(self.get_string())
+            self.frames = TemporaryFile()
+        else:
+            self.write_frames()
+        self.recording = recording
+
+    def get_string(self):
+        return tostring(self.display_surface, self.frame_format)
+
+    def write_frames(self):
+        root = join(self.root, strftime(self.directory_name_format))
+        if not exists(root):
+            makedirs(root)
+        size = self.display_surface.get_size()
+        frames = self.frames
+        frames.seek(0)
+        for ii, frame in enumerate(iter(lambda: frames.read(self.frame_length),
+                                        "")):
+            path = join(root, "%04i.png" % ii)
+            save(frombuffer(frame, size, self.frame_format), path)
+        print "wrote video frames to " + root
+
+    def update(self):
+        ticks = get_ticks()
+        if self.recording and ticks - self.last_frame >= self.framerate:
+            self.frames.write(self.get_string())
+            self.last_frame = ticks
diff --git a/shaken_and_spit_out/pgfw/__init__.py b/shaken_and_spit_out/pgfw/__init__.py
new file mode 100644 (file)
index 0000000..e69de29