From 3ab0fee0434bce55452e70c5d73ef713e0de214c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20R=C3=B8sdal?= Date: Sat, 28 Oct 2006 16:37:30 +0000 Subject: [PATCH] Add glChess to gnome-games. --- BUGS | 4 + COPYING | 340 ++++++ ChangeLog | 868 +++++++++++++ MANIFEST.in | 17 + Makefile.am | 25 + README | 38 + TODO | 28 + data/Makefile.am | 3 + data/ai.xml | 54 + debian/changelog | 120 ++ debian/compat | 1 + debian/control | 16 + debian/copyright | 27 + debian/docs | 3 + debian/glchess.mime | 8 + debian/manpage.sgml | 111 ++ debian/menu | 1 + debian/rules | 76 ++ glade/Makefile.am | 14 + glade/about.glade | 31 + glade/ai.glade | 212 ++++ glade/chess_view.glade | 116 ++ glade/error_dialog.glade | 160 +++ glade/glchess.glade | 1463 ++++++++++++++++++++++ glade/load_game.glade | 111 ++ glade/network_game.glade | 413 +++++++ glade/new_game.glade | 1230 +++++++++++++++++++ glade/save_game.glade | 80 ++ glchess.desktop.in | 15 + help/C/Makefile.am | 7 + help/C/glchess-C.omf | 24 + help/C/glchess.xml | 15 + help/C/legal.xml | 76 ++ help/Makefile.am | 1 + mime/glchess.xml | 8 + setup.py | 63 + src/Makefile.am | 1 + src/glchess | 10 + src/lib/Makefile.am | 12 + src/lib/__init__.py | 1 + src/lib/ai.py | 335 +++++ src/lib/cecp.py | 218 ++++ src/lib/chess/Makefile.am | 7 + src/lib/chess/__init__.py | 4 + src/lib/chess/board.py | 1025 ++++++++++++++++ src/lib/chess/lan.py | 178 +++ src/lib/chess/pgn.py | 686 +++++++++++ src/lib/chess/san.py | 456 +++++++ src/lib/defaults.py | 47 + src/lib/game.py | 526 ++++++++ src/lib/glchess.py | 4 + src/lib/gtkui/Makefile.am | 5 + src/lib/gtkui/__init__.py | 3 + src/lib/gtkui/dialogs.py | 540 +++++++++ src/lib/gtkui/gtkui.py | 931 ++++++++++++++ src/lib/main.py | 992 +++++++++++++++ src/lib/network/Makefile.am | 5 + src/lib/network/__init__.py | 2 + src/lib/network/announce.py | 127 ++ src/lib/network/protocol.py | 352 ++++++ src/lib/scene/Makefile.am | 6 + src/lib/scene/__init__.py | 127 ++ src/lib/scene/cairo/Makefile.am | 4 + src/lib/scene/cairo/__init__.py | 297 +++++ src/lib/scene/cairo/pieces.py | 47 + src/lib/scene/human.py | 165 +++ src/lib/scene/opengl/Makefile.am | 8 + src/lib/scene/opengl/__init__.py | 17 + src/lib/scene/opengl/builtin_models.py | 527 ++++++++ src/lib/scene/opengl/new_models.py | 1552 ++++++++++++++++++++++++ src/lib/scene/opengl/opengl.py | 680 +++++++++++ src/lib/scene/opengl/png.py | 1022 ++++++++++++++++ src/lib/scene/opengl/texture.py | 133 ++ src/lib/uci.py | 161 +++ src/lib/ui/Makefile.am | 4 + src/lib/ui/__init__.py | 1 + src/lib/ui/ui.py | 259 ++++ textures/Makefile.am | 6 + textures/board.png | Bin 0 -> 8804 bytes textures/glchess.svg | 103 ++ textures/piece.png | Bin 0 -> 9836 bytes 81 files changed, 17365 insertions(+) create mode 100644 BUGS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 MANIFEST.in create mode 100644 Makefile.am create mode 100644 README create mode 100644 TODO create mode 100644 data/Makefile.am create mode 100644 data/ai.xml create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/docs create mode 100644 debian/glchess.mime create mode 100644 debian/manpage.sgml create mode 100644 debian/menu create mode 100755 debian/rules create mode 100644 glade/Makefile.am create mode 100644 glade/about.glade create mode 100644 glade/ai.glade create mode 100644 glade/chess_view.glade create mode 100644 glade/error_dialog.glade create mode 100644 glade/glchess.glade create mode 100644 glade/load_game.glade create mode 100644 glade/network_game.glade create mode 100644 glade/new_game.glade create mode 100644 glade/save_game.glade create mode 100644 glchess.desktop.in create mode 100644 help/C/Makefile.am create mode 100644 help/C/glchess-C.omf create mode 100644 help/C/glchess.xml create mode 100644 help/C/legal.xml create mode 100644 help/Makefile.am create mode 100644 mime/glchess.xml create mode 100644 setup.py create mode 100644 src/Makefile.am create mode 100755 src/glchess create mode 100644 src/lib/Makefile.am create mode 100644 src/lib/__init__.py create mode 100644 src/lib/ai.py create mode 100644 src/lib/cecp.py create mode 100644 src/lib/chess/Makefile.am create mode 100644 src/lib/chess/__init__.py create mode 100644 src/lib/chess/board.py create mode 100644 src/lib/chess/lan.py create mode 100644 src/lib/chess/pgn.py create mode 100644 src/lib/chess/san.py create mode 100644 src/lib/defaults.py create mode 100644 src/lib/game.py create mode 100644 src/lib/glchess.py create mode 100644 src/lib/gtkui/Makefile.am create mode 100644 src/lib/gtkui/__init__.py create mode 100644 src/lib/gtkui/dialogs.py create mode 100644 src/lib/gtkui/gtkui.py create mode 100644 src/lib/main.py create mode 100644 src/lib/network/Makefile.am create mode 100644 src/lib/network/__init__.py create mode 100644 src/lib/network/announce.py create mode 100644 src/lib/network/protocol.py create mode 100644 src/lib/scene/Makefile.am create mode 100644 src/lib/scene/__init__.py create mode 100644 src/lib/scene/cairo/Makefile.am create mode 100644 src/lib/scene/cairo/__init__.py create mode 100644 src/lib/scene/cairo/pieces.py create mode 100644 src/lib/scene/human.py create mode 100644 src/lib/scene/opengl/Makefile.am create mode 100644 src/lib/scene/opengl/__init__.py create mode 100644 src/lib/scene/opengl/builtin_models.py create mode 100644 src/lib/scene/opengl/new_models.py create mode 100644 src/lib/scene/opengl/opengl.py create mode 100644 src/lib/scene/opengl/png.py create mode 100644 src/lib/scene/opengl/texture.py create mode 100644 src/lib/uci.py create mode 100644 src/lib/ui/Makefile.am create mode 100644 src/lib/ui/__init__.py create mode 100644 src/lib/ui/ui.py create mode 100644 textures/Makefile.am create mode 100644 textures/board.png create mode 100644 textures/glchess.svg create mode 100644 textures/piece.png diff --git a/BUGS b/BUGS new file mode 100644 index 0000000..eba3133 --- /dev/null +++ b/BUGS @@ -0,0 +1,4 @@ +If glChess exits without deleting AIs then they remain as processes +using 100% CPU. + +If AIs fail to execute or crash then glChess does not know this. diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d60c31a --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 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. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +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 give any other recipients of the Program a copy of this License +along with the Program. + +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. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +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 Program, 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 Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) 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; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, 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 executable. However, as a +special exception, the source code 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. + +If distribution of executable or 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 counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program 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. + + 5. 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 Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program 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 to +this License. + + 7. 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 Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program 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 Program. + +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. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program 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. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 Program +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 Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, 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 + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. 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 PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. 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. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; 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. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e351fb3 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,868 @@ +------------------------------------------------------------------------------ +CHANGELOG +------------------------------------------------------------------------------ +2006-10-23 vers. 1.0 RC1 + o AI options can be entered into the AI configuration file. + o Game defaults to human (white) versus the first detected AI. + o Lightened 3D models. + o Updated German translation from Rosetta. + o Changed AI file IO and animation from polled to event driven to save + CPU. + +2006-10-20 + o Fix bug with long castling. + o AI configuration moved into XML file. + +------------------------------------------------------------------------------ +2006-10-18 vers. 0.9.12 + o Gracefully handle (disable) missing stock icons. + +2006-10-13 + o Updated German translation from launchpad.net. + o Added Turkish translation (Thanks to Hakan Bekdas + ). + +2006-10-11 + o Finished converting all internal move formats to LAN. + o Added support for Universal Chess Interface (UCI) chess engines. + o Added support for Glaurung engine (UCI). + +2006-10-05 + o Updated translations from Rosetta (new fr and es). + +2006-10-04 + o Fix bug with decoding promotion from CECP engines. + o Refactored SAN and LAN en/decoding, changed much of the integer + internal location format to algebraic (i.e. (0, 0) -> 'a1'). + +------------------------------------------------------------------------------ +2006-10-04 vers. 0.9.11 + o OpenGL rendering is now optional + +2006-10-03 + o Recoloured 3D mode to be clearer and more consistent with 2D mode. + o Textures are now luminence only (save 1/3 memory). + o Python Imaging Library now optional - falls back to native Python + PNG loader. + o New game window has 'start' button active by default. + +2006-10-02 + o Cairo animation is now all double buffered. + o Simplified new game dialog - player types in single combo boxes with + icons. + o Add MIME file for /usr/share/mime - the freedesktop.org one is + missing the sub_class_of field (freedesktop.org bug #8286). + o Debian package updates desktop and MIME databases so .pgn games can + be executed graphically. + +------------------------------------------------------------------------------ +2006-10-01 vers. 0.9.10 + o Set default configuration values (otherwise upgrading users hit + configuration exception). + +------------------------------------------------------------------------------ +2006-10-01 vers. 0.9.9 + o Updated .desktop file to register application/x-chess-pgn MIME type. + +2006-09-30 + o Games can be 2D (Cairo) or 3D (OpenGL). The code does not currently + work if OpenGL is not installed (for next release). + o Fix bugs in validating castle moves. + o Made colour scheme softer (still needs someone skilled to pick + colours and textures). + o Stack trace is printed to stdout when OpenGL rendering errors occur. + o Only print out OpenGL info once + +2006-09-29 + o Fix bug in game saving. (Bug from Jens Hamacher) + o Fix bug reporting OpenGL errors. (Bug from Jens Hamacher) + o Send both 'sd' and 'depth' to CECP engines to set search + depth. CECP specifies only 'sd' but GNUchess does not accept + this. (Bug from Jens Hamacher) + o Append .pgn to the end of save games + o Report the version on stdout on startup - should help in bug reports + +2006-09-16 + o Refactor. + o Remove notebook border and add spacing. + +2006-09-12 + o Removed more dbus code that shouldn't have been there... + o Added support to load games from the command line. + +------------------------------------------------------------------------------ +2006-09-11 vers. 0.9.8 + o Whoops release - some networking code was accidently enabled + (avahi/dbus) that caused problems for people. + +------------------------------------------------------------------------------ +2006-09-09 vers. 0.9.7 + o Added window that shows the communication with the AIs. + o Kill AIs that don't quit properly. + o Catch exceptions and autosave and kill any AIs. This stops the 100% + CPU GNUchess processes being left running. + +2006-09-07 + o Handle exceptions from rendering - Mesa is still producing the odd + error. This will cause a corrupt frame but the program will still + run. + o Fix bug with notifying players of moves. This caused AI players to + fail if playing black. + o Added support for the Phalanx AI. + +------------------------------------------------------------------------------ +2006-08-28 vers. 0.9.6 + o Tweaked perspective to make better use of visual space + o Clear the OpenGL depth buffer after drawing the board. This stops + Mesa from corrupting the bottom of the piece models. + +2006-08-24 + o Changed display list code to avoid Mesa bug + (http://bugs.freedesktop.org/show_bug.cgi?id=7984). + o Disabled networking code to make 0.9.6 release that avoids + the Mesa bug. + +2006-08-21 + o Debian package now depends on Python OpenGL bindings. + +2006-08-11 + o Moved existing splashscreen to about dialog. When no games are + present an empty board is displayed. + +2006-08-09 + o Added Italian translation from Luca Marturana + , thanks again! + +2006-07-29 + o Added icon from Luca Marturana , + thanks Luca! + +------------------------------------------------------------------------------ +2006-06-26 vers. 0.9.5 + o Changed build process to use distutils + +2006-06-10 + o Added 'faile' chess engine to autodetection list + (http://faile.sourceforge.net) + +------------------------------------------------------------------------------ +2006-06-10 vers. 0.9.4 + o UI saves view options + +2006-05-29 + o Added internationalisation support (I18N) + o Added translations for en, en_NZ, en_AU, en_US and de + +------------------------------------------------------------------------------ +2006-05-14 vers. 0.9.3 + o Refactored the game/scene/view relationship + o Added a lot more pydoc documentation + o Animate pieces when moving + o Change 'File' menu to 'Game' menu with all menubar options + +2006-05-13 + o Added move history controls + +------------------------------------------------------------------------------ +2006-05-07 vers. 0.9.2 + o Replaced model set with new high-res version from John-Paul + Gignac + o Install a .desktop file (from anonymous commenter on glchess.sf.net) + o Reverted piece textures to "wood" from version 0.4.6 + o Added material properties for models (now shiny) + +------------------------------------------------------------------------------ +2006-05-04 vers. 0.9.1 + o Quick tidy up for release. + +2006-03-05 + o Added animation to rotate board so human player is in foreground. + o Render pieces and the board to display lists where possible - huge + performance improvement. + o Fixed bug in rendering (was linking top of revolved models to the + bottom). + o Added texturing support. + +2006-02-19 + o More refactoring: Reduce code complexity and add comments. + +2006-02-17 + o Refactored code into src/. + o Filled out README, INSTALL. + o Added menu (new feature is ability to hide toolbar). + +------------------------------------------------------------------------------ +2006-02-11 vers. 0.9.0 + o Complete rewrite in Python, uses code ported from 0.8 series. + o Board rules appear to be more reliable than previous series + (tested with AI vs AI games). + o Able to load and save files to PGN format - also stores AI + information. + o Automatically saves games on exit. + o Network support removed (to be added before 1.0.0). + o Supports the GNUchess, Crafty, Sjeng and Amy AIs. + (autodetected on startup). + o UI simplified (will add menus before 1.0.0). + o Able to drag pieces when moving (no animation yet). + +------------------------------------------------------------------------------ +2005-04-23 vers. 0.8.6 + o UI can now choose different AIs + o Engine processes are now completed on (normal) exit + o Able to save PGN files from the UI + +2005-04-22 + o Slimmed down states for core board rules + o Added support for PGN output + o Changed default engine to gnuchess (crafty complains about illegal + moves when playing AI vs. AI - I think this is a crafty bug) + o Support promotion pieces from SAN moves + o Fix bug where glChess thought a castle was in progress but it was + just an illegal two square king move. + +2005-04-21 + o Board module now records all previous states with a board stack. + o Got rid of all compiler warnings + +------------------------------------------------------------------------------ +2005-04-14 vers. 0.8.5 + o Fix missing datapath for chess view UI (affects installed versions) + o Fix .deb build process so copies directory to correct name + o Update about dialog to reflect copyright for old and new versions + +------------------------------------------------------------------------------ +2005-03-28 vers. 0.8.4 + o Servers now announce themselves when created (protocol change) + o Now can be build as a .deb package (RPM build using alien) + +------------------------------------------------------------------------------ +2005-03-28 vers. 0.8.3 + +2005-03-26 + o End game button now works + o Removed some obsolete printfs + +2005-03-25 + o Only the server displays the network game dialog + o Server informs connecting players of current players + o Players must connect with unique names + o Fixed dobule free bug when cancelling servers + +2005-03-24 + o Games now opened in tabs + o Human vs human local games have chat entry disabled + +------------------------------------------------------------------------------ +2005-03-23 vers. 0.8.2 + o Messages are marked with which player sent them / if they are from the server + o Network messages contain the source client + o Clients can now disconnect from the server + o Clients now correctly connect as spectators + +2005-02-27 + o SAN module now generates mate and checkmate symbols (+ and #) + o Got message sending working in main view + +2005-02-26 + o Made board and UI modules into objects. + o Board rotates around for human players (as in glChess 0.4) + +2005-02-23 + o Players now have names for local game + o AI players no longer use hard-coded name + o Network game servers can swap players (e.g. if the black player is + selected to be the same as the white player they are swapped). + o Made scene module into an object (so future support for multiple games/scenes possible) + +------------------------------------------------------------------------------ +2005-02-20 vers. 0.8.1 + o Network support works 95% + - Can have n clients connected (2 players + spectators) + - Players can be human or AI + - Multiple servers on one machine + +2005-02-05 + o Added copyright headers on the files + +------------------------------------------------------------------------------ +2005-02-05 vers. 0.8.0 + o Complete rewrite from the ground up + o Uses GTK+ 2 + 0 Uses gtkglext over gtkglarea + o Planned support for (by release 0.9) + - Network play + - Textures + - Configuration using gconf + - Distribution packages + - etc... + o Note also the new website (in drupal) + +------------------------------------------------------------------------------ +2002-09-09 vers. 0.4.7 + +2002-09-09 + o Applied patches from Martin Jacobs, fixes some problems getting glChess + to work with gnuchess, and misc. bugs + +2002-03-27 + o Some GUI/glchessrc patches from Michael Moerz + o Fixed ambiguity in shared files install dir, it is now /usr/local/share/games/ +------------------------------------------------------------------------------ +2002-03-25 vers. 0.4.6 + +2002-03-25 + o Applied some Makefile simplification patches from Michael Moerz + o glChess now sleeps when not doing anything -- doesn't waste CPU +------------------------------------------------------------------------------ +2002-03-23 vers. 0.4.5 + +2002-03-23 + o Applied patch from Michael Hanson to fix calls with execvp() when + argv == NULL +------------------------------------------------------------------------------ +2002-01-23 vers. 0.4.4 + +2002-01-23 + o main.c: (Re)fixed bug which had strcmp() on NULL pointers + +2002-01-22 + o san.c: Fixed bug in SAN parsing code + +2002-01-18 + o san.c: Resolved a bug with one given coordinate + +2002-01-17 + o dialog.c: Implemented a file dialog for binary paths + o README: Updated some info, added configuration section + +2002-01-15 + o move.c: Fixed a bug which could appear when castling, + 'en passant'ing or promoting and king would be in check +------------------------------------------------------------------------------ +2002-01-10 vers. 0.4.3 + +2002-01-07 + o move.c: Improved some move rule code + +2001-12-29 + o dialog.c: Applied patch by Robert Mibus to prevent a SEGFAULT + +2001-12-28 + o *.[ch]: Replaced "Copyright 2001" by "Copyright 2002" + + o dialog.c: Interpret user defined NULL binary path as "do not use" + +2001-12-27 + o move.c: Fixed a promotion bug when unselecting + o dialog.c: Added gtk_window_set_position to all dialogs + o dialog.c: Make entries uneditable if "do not use" is set + + o move.c: Replaced "---" by "do not use" + o dialog.c: dto. +------------------------------------------------------------------------------ +2001-12-25 vers. 0.4.2 + +2001-12-25 + o move.c: Finally fixed promotion dialog + o dialog.c: dto. + o menu.c: dto. + +2001-12-24 + o dialog.c: Added save button to binary dialog + o dialog.h: Clean up + +2001-12-23 + o dialog.c: Added check whether user defined paths exist + o *.[ch]: Ran indent + +2001-12-20 + o menu.c: Added "Edit binary paths" to menu + o dialog.c: Added "Don't have engine" check boxes + o menu.c: Adapted to "Don't have engine" + o engine.c: dto. + +2001-12-19 + o san.c: Hopefully improved debug ouput to track down a quite + strange bug + +2001-12-18 + o anim.[ch]: Renamed animation.[ch] + o prefs.[ch]: Renamed preferences.[ch] + (I like short file names :-) + +2001-12-16 + o dialog.c: Fixed GdkWarning by adding gtk_widget_realize + o *.[ch]: Changed debug_output format + +2001-12-14 + o global.h: Removed GLCHESS_VERSION_STRING + o dialog.c, interface.c: Use VERSION instead + + o preferences.c: Rewrote some code for reflection patch + o interface.c: Disabled board rotation when engine is AI + +2001-12-09 + o Makefile, configure.in: Finished autoconf/automake + o README: documentation update + +2001-12-08 + o dialog.c: Added quit dialog + o dialog.c: set engine dialog modal +------------------------------------------------------------------------------ +2001-12-07 vers. 0.4.1 + +2001-12-07 + o src/Makefile: Removed the -s flag + o Makefile: Included strip in install target as Robert proposed + o *.[ch]: Ensured that every debug_output checks debug first + + o Makefile: Added $(PREFIX) = /usr/local as suggested by PLD + + Did a include-cleanup: + o image.c: Removed string.h include + o menu.c: Removed string.h include + o models.c: Removed stdio.h include + o move.c: Removed gdk/gdk.h include + + Moved all code concerning dialogs into dialog.c + o dialog.[ch]: Added + o engine.c, menu.c: Moved dialog code to dialog.c + +2001-12-06 + o main.c: Reordered functions, so they do what they should do + o main.c, engine.c, menu.c: Removed some DogFood(tm) + +2001-12-05 + o main.c: Fixed segfault due to looking at NULL strings + o menu.c: Fixed binary path dialog, made modal + o menu.c: Stopped indent from messing up ItemFactory struct, + cleaned up struct, so is readable +------------------------------------------------------------------------------ +2001-12-01 vers 0.4.0 (inofficial release) + +2001-11-30 + o menu.c: Added dialog for entry of binary path + +2001-11-26 + o engine.c: Added find_binary and check_binary + o glchessrc: Added entries for binary paths + o global.h: Bumped version to 0.4.0 + o glchess.spec: dto. + +2001-11-20 + o interface.c: Removed handle menu box + o interface.c: Same for status bar + +2001-11-16 + o draw.c, menu.c: + Applied a patch from Bartosz Taudul to + speed up reflections + o AUTHORS: Added patches section + +2001-11-09 + o menu.c: Corrected a menu entry + o interface.c: Tweaked whole app by removing unneeded animate call + + +2001-10-31 + o menu.c: Made "piece size" menu a radio button one + o menu.c: Moved "piece size" menu to "View" + +2001-10-23 + o Some cosmetic changes +------------------------------------------------------------------------------ +2001-09-04 vers 0.3.5 + +2001-09-04 + o menu.c: Tweaked about dialog + o logo.xpm: Added + o move.c: Fixed a critical bug + o san.c: dto. + +2001-09-03 + o interface.c: Changed timer interval to 1 sec + o move.c: Move list now scrolls down when it reachs bottom + +2001-09-01 + o Makefile: Fixed compile line to have LIBS last. + Thanks to Sean Fleming for the bug report + o Makefile.IRIX: dto. +------------------------------------------------------------------------------ +2001-08-18 vers 0.3.4 + +2001-08-17 + o menu.c: Started to implement promotion dialog (not finished yet) + +2001-08-15 + Thanks to Guiseppe Borzi' we now have a RPM package + for the very first time. + o glchess.spec: Added + o glchess.menu: dto. + o glchess.patch: dto. + +2001-08-14 + o menu.[ch]: Renamed help_about about_dialog + +2001-08-13 + o move.c: Implemented material statistic display + o interface.c: dto. + +2001-08-12 + o glchessrc.installed: Removed + o main.c: Let us exit normally when no config file was found + +2001-08-11 + o src/interface.c: Finished move list + +2001-08-08 + o src/interface.c: Added frame for move list + o dto.: Renamed create_window to create_main_window + +2001-08-07 + o src/Makefile*: Added optimization and stripping at compile time + o src/move.c: Limited the trackball zoom range + o dto.: Fixed PAWN move bug + o src/models.c: Removed some useless lines + +2001-08-06 + o src/global.h: Did a little clean-up in the player structure + +2001-08-02 + o src/splash.c: Resolved FIXME by changing rotation code + +2001-08-01 + o src/move.c: Removed a useless line + o src/game.c: Removed two crufty functions + o src/interface.c: Removed unused 'killcount' code + o src/global.h: dto. + +2001-07-31 + o src/models.c: Added ability to change size of pieces + o dto.: Removed some unneeded lines + o dto.: Removed constant variable norm_theta + o src/menu.c: Added menu entries for this + +2001-07-19 + o src/Makefile: Let's `make` do the work, not us + o src/Makefile.IRIX: dto. + +2001-07-18 + o Makefile: Noticed that 'tar zcvf' doesn't compress on the highest + level, added a separate call for zipping with -9v +------------------------------------------------------------------------------ +2001-07-17 vers 0.3.3 + +2001-07-17 + o interface.c: Fixed crash, when playing against an engine, + changing color and then playing against yourself + o menu.c: Fixed crash, when changing color without a running game + o config.c: Simplified some stuff + o config.h: Added prototype of get_colour + +2001-07-16 + o config.c: Wiped out the length limits for options, esp. paths + +2001-07-11 + o san.c: Complete rewrite. Used move rules to do the thing + o menu.c: Added menu entries for color choice + +2001-07-09 + o san.c: Replaced stupid linear code by more sophisticated code:-) + KING: 49 lines -> 7 lines + KNIGTH: 47 lines -> 8 lines + o BUGS: Added + +2001-07-08 + o interface.c, preferences.c, splash.c: Rename all + gtk_gl_area_swapbuffers() calls to gtk_gl_area_swap_buffers() + calls, following the GtkGLArea HOWTO + o game.c: Little optimization in reset_board + o texture.c: Made several file name length dynamically allocatable +------------------------------------------------------------------------------ +2001-07-07 vers 0.3.2 + +2001-07-07 + o move.c: Fixed castling rule by using internal move list + o Makefile: Added version and indent target + o move.c: Added a 'in check' check algorithm, this means + move rules finally and completely work now + o move.c: Fixed some strange movement bugs with PAWN and KING + +2001-07-05 + o engine.c, move.c, global.c: Implemented internal move list + o move.c: Fixed a bug with PAWN which could be moved sidewards + +2001-07-04 + o engine.c., menu.c: You could have started an engine while playing + local Human vs. local Human, fixed + o move.c: Moved some code to check_move function, mainly enhanced + KING and PAWN checking code + o move.c: Rule checking is now completely done in the enhanced old, + but very sophisticated code. Though some things are missing + +2001-07-03 + o picksquare.[ch]: Renamed to move.[ch] + o config.c, glchessrc: Added debugging option to config file + (Until now debugging was on by default) + o engine.c, san.c, move.c: Adapted +------------------------------------------------------------------------------ +2001-07-04 vers 0.3.1 + +2001-07-02 + o interface.c: Fixed a long standing NVIDIA bug + (It _was_ our fault... :-) + o menu.c: Fixed bug where splashscreen wasn't displayed with NVIDIA + drivers +------------------------------------------------------------------------------ +2001-06-27 vers 0.3.0 + +2001-06-26 + o engine.c: Enabled engine pawn promotion (not only QUEEN) + +2001-06-25 + o picksquare.c: Added dialog to local H vs H code + o engine.c: Fixed bug, when starting several engines in a row + o engine.c: Fixed local H vs H again(!) + o menu.c: Renamed debug option + o TODO: Enlarged + +2001-06-24 + o san.c: Fixed a coordinate validity bug + o engine.c: Further improved read stability + o engine.c, picksquare.c: + Implemented GNU Chess support + o engine.c: fixed a newly introduced bug in read_from_engine + o menu.c: Simplified help_about + o engine.c: added engine_dialog + +2001-06-23 + o menu_preferences.[ch]: renamed to preferences.[ch] + o splash.c: moved init_game call to menu.c + o menu.c: added entries for engine choice + o *.[ch]: removed all USE_ENGINE and DEBUG_ENGINE defines + o menu.c: added entries whether or not to show debug information + o INSTALL: Rewrote some parts + +2001-06-22 + o san.c: Further optimization, esp. ROOK, KNIGHT, QUEEN + o san.c: Fixed a bug where the wrong piece could be moved + +2001-06-21 + o README, AUTHORS: Rewrote some parts + o TODO: Reordered, sorted entries by category and importance, + added estimated version number of implementation + +2001-06-20 + o san.c: Reduced memory usage + o Makefile: Added irix target, simply type make irix for IRIX now + o Makefile: Added make target, simply type make help for install notes + o *.[ch]: Changed code style, did a indent with: '-kr -i2 -bli0 -bl' +------------------------------------------------------------------------------ +2001-06-19 vers 0.2.9 + +2001-06-19 + o Added SAN support => Crafty > 18.x is usable now + +2001-06-18 + o Updated Makefile.IRIX + o Added NEWS file to inform you about recent plans + +2001-06-15 + o Replaced all malloc.h includes by stdlib.h ones + +2001-06-12 + o Added read_char which won't wait for ever if there is no char + o Added check_notation_format which will tell you if glChess + supports the format the engine uses +------------------------------------------------------------------------------ +2001-05-30 vers 0.2.8 + +2001-05-30 + o Resolved a segfault problem in engine.c/clear_engine_msg on startup + o Fixed a misbehaviour in make release + +2001-05-28 + o Merged types.h and global.h to global.h, added global.c + o Added info on how to pull glChess via CVS + +2001-05-25 + o Added full "en passant" support + Be warned! It's a rare case (at least for me), I couldn't + really test it. + o Fixed a bug in engine.c/parse_move_from_engine with learn message + o Did a TODO file clean-up + o Automated versioning in documentation + o Replaced "!!note" by "FIXME" entries (it's more logical to me) +------------------------------------------------------------------------------ +2001-05-24 vers 0.2.7 + +2001-05-24 + o glChess now quits normally if OpenGL is not supported + o Fixed a SEGFAULT in engine.c/clear_engine_msg + o Re-added COPYING to sources due to problems with CVS and symlinks + +2001-05-24 + o Finally resolved my problems when starting glChess as root + by writing clear_startup_msg + o Improved reading from engine mechanism + +2001-05-12 + o Fixed two bugs in Makefile (install, release) + o Brought man page up to date + o Added target 'uninstall' to Makefile, self-explanatory + o Enhanced About dialog + +2001-05-11 + o Changed ChangeLog format, so you can see when something was done + o Fully enabled beep on illegal move, added config and menu entry + for this +------------------------------------------------------------------------------ +2001-05-09 vers 0.2.6 + o Changed normal printf(...) to fprintf(stderr, ...) in config.c + o First approach for pawn promotion, still lacking a dialog + promotion to queen is default + o Integrated beep on move + o Changed GlChessWidget to glChessWidget for conformity + o Changed coords color to white + o Added an IRIX Makefile in + o Did a shoddy fix for NVIDIAS worthless drivers +------------------------------------------------------------------------------ +2001-05-06 vers 0.2.5 + o Config files with no newline at the end now supported. + o Improved readability in engine.c + o Changed description text + o Reduced startup problems with engine. Anybody with pipe + experience out there ? + o Crafty/CECP options now in config file + o glChess now stops crafty when game is resigned + o crafty is only started when game is started, not when glChess starts + o Added in 'make release' -- packages up the source +------------------------------------------------------------------------------ +2001-05-01 vers 0.2.4 + o Code readibiliy fixes + o Co-ordinates reflected on other side of board now + o Moved man/man6/glchess.6.gz to man/glchess.6.gz + o Started implementing the Chess Engine Communication Protocol. + So far you can only use crafty and CECP is not fully + useable. + Basic game playing works. We need feedback !! + o Expanded TODO for engine + o Changed the date strings in Changelog + o Fixed a little bug in picksquare with new castling rules +------------------------------------------------------------------------------ +2001-04-16 vers 0.2.3 + o Changed COPYING to be a symlink to /usr/share/automake/COPYING + o Fixed bug when try to resign from splash screen, and similar when start from game + screen + o Added in grid co-ordinates + o Added new config options -- texture_dir number_colour letter_colour + o Added in Michael Duelli as an author +------------------------------------------------------------------------------ +2001-04-15 vers 0.2.2 + o Fixed signal disconnection error + o Improved rc file format (added in quotes) + o Got rid of lots of bad global variables/bad programming + (isn't it fun to come back to something you wrote ages ago... :) ) + o Using shortcut to start game now refreshes view + o Changed pitch of rotating view to look down more (easier to play) + o Lots of good bug/convention fixes from Michael Duelli -- thanks! + including: + o Changed default install dir to /usr/local/bin since most distributions + don't have /usr/local/games in their path. + o Limited the trackball mode so can't see under the board + o Changed glchess.[ch] to main.[ch] -- more conventional + o Fixed error with default flat/smooth shading + o Misc code readibility fixes + o Fixed stretching in Ortho mode + o Added in preferences dialog -- warning it doesn't work 100%!!! + o Added in AUTHORS file -- becoming a multi-developer project + o Changed base directory to glchess-VERSION + o Added in man page +------------------------------------------------------------------------------ +2001-04-08 vers 0.2.1 + o Reorded menus + o Reenabled trackball mode + o Reenabled free mode + o Added in save options menu item + o Got rid of all C++ style comments ('//') +------------------------------------------------------------------------------ +2001-04-08 vers 0.2.0 + o Changed email address + o Changed interface to GTK+ + o Changed piece textures +------------------------------------------------------------------------------ +2001-02-18 vers 0.1.11 + o Added in texturing + o Added in revolve_line() -- greatly simplified models.c + o Improved rc file loading + o Now checks the current dir for a rc file (if not installed) +------------------------------------------------------------------------------ +2001-01-13 vers 0.1.10 + o Added some more libs into the Makefile + o Changed the selected piece colour to red with a higher alpha -- easier to see + o FPS only updates every 1 sec now -- easier to read + o Reflections are initially disabled -- performance (press j to activate) + o Added a _very_ simple rc file, not very robust, just copy /etc/glchessrc to ~/.glchessrc +------------------------------------------------------------------------------ +2001-01-11 vers 0.1.9 + o Fixed some problems in the Makefile (sorry) + o Linked to GLU, one person's glut or similar not linking it seems +------------------------------------------------------------------------------ +2001-01-08 vers 0.1.8 + o Now compiled with a makefile + o Fixed error in lighting from selected piece + o Realised castling rule is not really correct, not fixed, will not change the rules + again because a) I don't think anyone is actually using it + and b) It will use crafty or something similar *soon* so no need to bother + o FPS meter fades when max FPS is reached + o Name changes to red when editing +------------------------------------------------------------------------------ +2001-01-05 vers 0.1.7 + o Fixed castling rule (I wasn't fully aware of what the rule was) + o Changed the colour of the pieces/board + o Fixed an error in the shading toggle + o Added a pop-up menu for quit/toggles/viewmode + o Changed default view-mode to 2 (rotating) + o Got stencil buffer working by enabling it in glut (oops) + o Added a frame rate limiter in + o Added ability to change player names + o Game no longer automatically starts (r-click for menu) + o Fixed error in knight model + o Changed lighting on selected piece + o Added a fourth view mode (trackball) +------------------------------------------------------------------------------ +2001-01-01 vers. 0.1.6 + o Added floor reflections (stencil buffer doesn't seem to work though) + o Added castling rule + o Added in a make script (it _will_ be a makefile one day) + o Fixed the location of the kings/queens (you can tell I don't play chess) + o Removed some debugging output to stdout +------------------------------------------------------------------------------ +2000-10-13 vers. 0.1.5 + o Actually finished knight model :) + o Found out how to cull back facing polygons -- fps greatly improved + o Fixed alignment of black players time on HUD (for 480x480 res) + o Disabled killcount -- slowing down fps too much + o Added webpage url into README +------------------------------------------------------------------------------ +2000-09-28 vers. 0.1.4 + o Co-ordinate system changed - y is up instead of z now (native OpenGL) + o Finished knight model (finally) + o Selected model lighting fixed + o Error (segfault) in selection algorithm fixed + o Beginnings of HUD added - fps meter, player time, killcount +------------------------------------------------------------------------------ +2000-05-27 vers. 0.1.3 + o Players now take turns + o 3 Modes of view - Dynamic/Rotating/Ortho +------------------------------------------------------------------------------ +2000-05-26 vers. 0.1.2 + o Pieces now have movement rules +------------------------------------------------------------------------------ +2000-05-22 vers. 0.1.1 + o fixed draw.c (accidentially overwritten in previous release) + o pieces can now be moved +------------------------------------------------------------------------------ +2000-05-15 vers. 0.1.0 + o first release +------------------------------------------------------------------------------ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..723c451 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,17 @@ +include lib/glchess/gtkui/*.glade +include po/*.po po/*.pot +include BUGS ChangeLog COPYING INSTALL README TODO +include ai.xml glchess.svg glchess.desktop +include Makefile +include MANIFEST.in +include textures/*.png +include mime/glchess.xml +include debian/changelog +include debian/compat +include debian/control +include debian/copyright +include debian/docs +include debian/manpage.sgml +include debian/menu +include debian/rules +include debian/glchess.mime diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..056ab72 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,25 @@ +SUBDIRS = data glade help src textures + +# Source files +EXTRA_DIST = +CLEAN_DIST = +CLEANFILES = + +EXTRA_DIST += intltool-extract.in intltool-merge.in intltool-update.in +DISTCLEANFILES = intltool-extract intltool-merge intltool-update + +################################################################# +# Launcher +@INTLTOOL_DESKTOP_RULE@ + +desktopdir = $(datadir)/applications +desktop_in_files = glchess.desktop.in +desktop_DATA = $(desktop_in_files:.desktop.in=.desktop) +EXTRA_DIST += $(desktop_in_files) +CLEANFILES += $(desktop_DATA) + +################################################################# + +## Executable +bin_SCRIPTS = src/glchess +EXTRA_DIST += src/glchess diff --git a/README b/README new file mode 100644 index 0000000..2cf2723 --- /dev/null +++ b/README @@ -0,0 +1,38 @@ +1. Description + +glChess is a 2D/3D chess game interfacing via the Chess Engine +Communication Protocol (CECP) by Tim Mann. This means it can currently use +engines such as GNUChess, Sjeng, Faile, Amy, Crafty and Phalanx. + +2. Homepage + +Visit the homepage for information on how to report bugs and to keep +up on development progress. + +SourceForge Homepage -> http://glchess.sourceforge.net +SourceForge Project Page -> http://sourgeforge.net/projects/glchess + +3. Requirements + +glChess requires the following software to be installed to work: + +- Python +- Gtk+ including the python bindings + +For 3D support the following are optionally required: + +- GtkGLExt including the python bindings +- OpenGL python bindings + +For faster texture loading: + +- Python Imaging Library + +Optionally you can install CECP compatible AIs - glChess will detect the ones +it can use. + +4. License + +glChess is released under the GNU General Public License. In short, you may +copy this program (including source) freely, but see the COPYING file for +full details. diff --git a/TODO b/TODO new file mode 100644 index 0000000..6aaed7b --- /dev/null +++ b/TODO @@ -0,0 +1,28 @@ +1.0 Features +------------- + +Supply application/x-chess-pgn icons + +1.1 Features +-------------- + +Network support +Crash dialog so users can easily make bug reports +Make configuration dialog (with AI config too) +Don't autosave games that have finished (move into a history file) +Reflect chess pieces in board +Chess piece shadows +Anti-aliasing +Drag and drop 2D pieces +Drag and drop application/x-chess-pgn files +Add icons for human/network/ai players. Use Red Robot for the AI! +Share textures and display lists between games + +1.2+ Features +-------------- + +ICS support +Dialog if don't have correct version of GTK+, GTKGLarea installed +Help file +Get setup.py to install translations. This may be a limitation of distutils? They need compiling and renaming +Make glChess able to generate thumbnails for Nautilus diff --git a/data/Makefile.am b/data/Makefile.am new file mode 100644 index 0000000..58870e7 --- /dev/null +++ b/data/Makefile.am @@ -0,0 +1,3 @@ +EXTRA_DIST = ai.xml +aidir = $(datadir)/glchess/ +ai_DATA = ai.xml diff --git a/data/ai.xml b/data/ai.xml new file mode 100644 index 0000000..3bf3343 --- /dev/null +++ b/data/ai.xml @@ -0,0 +1,54 @@ + + + + GNUchess + gnuchess + + + + + Sjeng + sjeng + + + + + + Amy + Amy + + + + + + Crafty + crafty + + + + + + Faile + faile + + + + + + Phalanx + phalanx + + + + + + + Glaurung + glaurung + + + + + + + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..a7a2356 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,120 @@ +glchess (1.0RC1-1) unstable; urgency=low + + + New upstream release + + Changed PIL and OpenGL packages from Suggests to Recommends. + + -- Robert Cleaver Ancell Mon, 23 October 2006 10:17:00 +0000 + +glchess (0.9.12-1) unstable; urgency=low + + + New upstream release + + -- Robert Cleaver Ancell Mon, 2 October 2006 13:31:00 +0000 + +glchess (0.9.11-1) unstable; urgency=low + + + New upstream release + + Update desktop and mime databases after installation + + -- Robert Cleaver Ancell Mon, 2 October 2006 13:31:00 +0000 + +glchess (0.9.10-1) unstable; urgency=low + + + New upstream release + + -- Robert Cleaver Ancell Sat, 1 October 2006 17:04:00 +0000 + +glchess (0.9.9-1) unstable; urgency=low + + + New upstream release + + -- Robert Cleaver Ancell Sat, 1 October 2006 11:32:00 +0000 + +glchess (0.9.8-1) unstable; urgency=low + + + New upstream release + + Added dependencies to python2.4, python2.4-gtk2 + + -- Robert Cleaver Ancell Sat, 11 September 2006 21:01:00 +0000 + +glchess (0.9.7-1) unstable; urgency=low + + + New upstream release + + -- Robert Cleaver Ancell Sat, 9 September 2006 12:40:00 +0000 + +glchess (0.9.6-1) unstable; urgency=low + + + New upstream release + + -- Robert Cleaver Ancell Mon, 28 August 2006 20:59:00 +0000 + +glchess (0.9.5-2) unstable; urgency=low + + + New upstream release + + Package now depends on python2.4-opengl + + -- Robert Cleaver Ancell Mon, 21 August 2006 15:23:00 +0000 + +glchess (0.9.5-1) unstable; urgency=low + + + new upstream release + + Python files are compiled on installation + + -- Robert Cleaver Ancell Tue, 13 June 2006 22:44:00 +0000 + +glchess (0.9.4-2) unstable; urgency=low + + + Fix menu entry to point at correct binary + + Add man page to package + + -- Robert Cleaver Ancell Sat, 10 June 2006 16:04:00 +0100 + +glchess (0.9.4-1) unstable; urgency=low + + + new upstream release + + -- Robert Cleaver Ancell Sat, 10 June 2006 12:46:00 +0000 + +glchess (0.9.3-1) unstable; urgency=low + + + new upstream release + + -- Robert Cleaver Ancell Sun, 14 May 2006 15:14:00 +0000 + +glchess (0.9.2-1) unstable; urgency=low + + + new upstream release + + -- Robert Cleaver Ancell Sun, 7 May 2006 13:03:00 +0000 + +glchess (0.9.1-1) unstable; urgency=low + + + new upstream release + + -- Robert Cleaver Ancell Thu, 4 May 2006 21:29:00 +0000 + +glchess (0.9.0-1) unstable; urgency=low + + + new upstream release - rewrite in Python + + -- Robert Cleaver Ancell Sat, 11 Feb 2006 12:09:00 +0000 + +glchess (0.8.6-1) unstable; urgency=low + + + new upstream release + + -- Robert Cleaver Ancell Thu, 23 Apr 2005 18:15:00 +1200 + +glchess (0.8.5-1) unstable; urgency=low + + + new upstream release - fixes UI path bug + + -- Robert Cleaver Ancell Thu, 14 Apr 2005 23:51:00 +1200 + +glchess (0.8.4-1) unstable; urgency=low + + * Initial Release. + + -- Robert Cleaver Ancell Mon, 28 Mar 2005 15:37:31 +1200 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..b8626c4 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +4 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..3b1a771 --- /dev/null +++ b/debian/control @@ -0,0 +1,16 @@ +Source: glchess +Section: gnome +Priority: optional +Maintainer: Robert Ancell +Build-Depends: debhelper (>= 4.0.0), docbook-to-man, python-glade2 (>=2.8.1) +Standards-Version: 3.6.1 + +Package: glchess +Architecture: all +Depends: python2.4, python2.4-gtk2, python2.4-glade2 (>= 2.8.1) +Recommends: python2.4-gtkglext1, python2.4-opengl, python2.4-imaging +Description: A 2D/3D chess interface. + glChess is a 2D/3D chess program. It supports: + * Multiple simultaneous games using tabs + * Artificial intelligence using external engines + * 2D/3D rendering diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..a0895f9 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,27 @@ +This package was debianized by Robert Cleaver Ancell on +Mon, 28 Mar 2005 15:37:31 +1200. + +It was downloaded from http://glchess.sourceforge.net + +Copyright Holder: Robert Ancell + +License: + + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This package 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this package; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. + +On Debian systems, the complete text of the GNU General +Public License can be found in `/usr/share/common-licenses/GPL'. + diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..d5aaf52 --- /dev/null +++ b/debian/docs @@ -0,0 +1,3 @@ +BUGS +README +TODO diff --git a/debian/glchess.mime b/debian/glchess.mime new file mode 100644 index 0000000..4c7a987 --- /dev/null +++ b/debian/glchess.mime @@ -0,0 +1,8 @@ + + + + + PGN chess game + + + \ No newline at end of file diff --git a/debian/manpage.sgml b/debian/manpage.sgml new file mode 100644 index 0000000..fcd7b81 --- /dev/null +++ b/debian/manpage.sgml @@ -0,0 +1,111 @@ + manpage.1'. You may view + the manual page with: `docbook-to-man manpage.sgml | nroff -man | + less'. A typical entry in a Makefile or Makefile.am is: + +manpage.1: manpage.sgml + docbook-to-man $< > $@ + + + The docbook-to-man binary is found in the docbook-to-man package. + Please remember that if you create the nroff version in one of the + debian/rules file targets (such as build), you will need to include + docbook-to-man in your Build-Depends control field. + + --> + + Robert"> + Ancell"> + + March 28, 2005"> + 6"> + bob27@users.sourceforge.net"> + + GLCHESS"> + + + Debian"> + GNU"> + GPL"> +]> + + + +
+ &dhemail; +
+ + &dhfirstname; + &dhsurname; + + + 2005-2006 + &dhusername; + + &dhdate; +
+ + &dhucpackage; + + &dhsection; + + + &dhpackage; + + A 3D chess application + + + + &dhpackage; + + + + DESCRIPTION + + &dhpackage; is a 3D chess application for + GTK+ which supports CECP compatible artificial intelligences. + + More information can be found at http://glchess.sourceforge.net. + + + + SEE ALSO + + gnuchess(6), crafty(6), amy(6), faile(6), xboard(6). + + + AUTHOR + + glChess and this manual page were written by &dhusername; &dhemail;. + Permission is granted to copy, distribute and/or modify this document under + the terms of the &gnu; General Public License, Version 2 any + later version published by the Free Software Foundation. + + + On Debian systems, the complete text of the GNU General Public + License can be found in /usr/share/common-licenses/GPL. + + + +
+ + + + diff --git a/debian/menu b/debian/menu new file mode 100644 index 0000000..72b3d07 --- /dev/null +++ b/debian/menu @@ -0,0 +1 @@ +?package(glchess):needs="X11" section="Games/Board" title="glChess" command="/usr/games/glchess" diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..6b7724a --- /dev/null +++ b/debian/rules @@ -0,0 +1,76 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +configure: configure-stamp +configure-stamp: + dh_testdir + # Add here commands to configure the package. + + touch configure-stamp + + +build: build-stamp + +build-stamp: configure-stamp + dh_testdir + + # Add here commands to compile the package. + $(MAKE) + python setup.py build + docbook-to-man debian/manpage.sgml > glchess.6 + + touch build-stamp + +clean: + dh_testdir + dh_testroot + rm -f build-stamp configure-stamp + + # Add here commands to clean up after the build process. + python setup.py clean --all + + dh_clean + +install: build + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + + # Add here commands to install the package into debian/glchess. + $(MAKE) install DESTDIR=$(CURDIR)/debian/glchess + python setup.py install --root $(CURDIR)/debian/glchess + +# Build architecture-independent files here. +binary-indep: build install + dh_testdir + dh_testroot + dh_installmenu + dh_installman glchess.6 + dh_installchangelogs ChangeLog + dh_link -i + dh_compress -i + dh_fixperms -i + dh_python -i + #dh_pysupport -i # Used by Debian + dh_desktop -i + dh_installmime -i + dh_installdeb -i + dh_gencontrol -i + dh_md5sums -i + dh_builddeb -i + +# Build architecture-dependent files here. +binary-arch: build install + # No architecture dependant files + +binary: binary-indep binary-arch +.PHONY: build clean binary-indep binary-arch binary install diff --git a/glade/Makefile.am b/glade/Makefile.am new file mode 100644 index 0000000..91ad06b --- /dev/null +++ b/glade/Makefile.am @@ -0,0 +1,14 @@ + +uidir = $(datadir)/glchess +ui_DATA = \ + about.glade \ + ai.glade \ + chess_view.glade \ + error_dialog.glade \ + glchess.glade \ + load_game.glade \ + network_game.glade \ + new_game.glade \ + save_game.glade + +EXTRA_DIST = $(ui_DATA) diff --git a/glade/about.glade b/glade/about.glade new file mode 100644 index 0000000..3734775 --- /dev/null +++ b/glade/about.glade @@ -0,0 +1,31 @@ + + + + + + + True + True + glChess 2.17.1 + Copyright 2005-2006 Robert Ancell (and contributors) + A 2D/3D chess interface for Gnome + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + True + http://glchess.sourceforge.net + glChess homepage + Robert Ancell <bob27@users.sourceforge.net> + John-Paul Gignac (3D Models) +Thomas Dybdahl Ahle (2D Models) + Luca Marturana <lucamarturana@gmail.com> (It) +Hakan Bekdas <hakanbekdas@yahoo.com> (tr) +Many translators on launchpad.net. + + glchess.svg + + + + diff --git a/glade/ai.glade b/glade/ai.glade new file mode 100644 index 0000000..9eba93e --- /dev/null +++ b/glade/ai.glade @@ -0,0 +1,212 @@ + + + + + + + (dummy window) + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + 6 + True + 4 + 2 + False + 6 + 6 + + + + True + Executable: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + True + True + (executable name) + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 0 + 1 + + + + + + + True + Playing as: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + True + (player in game) + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 1 + 2 + fill + + + + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_ALWAYS + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + True + False + True + GTK_JUSTIFY_LEFT + GTK_WRAP_NONE + True + 0 + 0 + 0 + 0 + 0 + 0 + + + + + + 0 + 2 + 3 + 4 + fill + + + + + + True + Communication: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 2 + 2 + 3 + fill + + + + + + + + diff --git a/glade/chess_view.glade b/glade/chess_view.glade new file mode 100644 index 0000000..6b8ae8d --- /dev/null +++ b/glade/chess_view.glade @@ -0,0 +1,116 @@ + + + + + + + + 300 + 300 + True + window1 + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + True + True + 300 + + + + True + make_chess_view + 0 + 0 + Thu, 24 Mar 2005 01:41:43 GMT + + + True + True + + + + + + True + False + 0 + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + False + False + True + GTK_JUSTIFY_LEFT + GTK_WRAP_NONE + False + 0 + 0 + 0 + 0 + 0 + 0 + + + + + + 0 + True + True + + + + + + True + True + True + True + 0 + + True + * + False + + + + 0 + False + False + + + + + True + True + + + + + + + diff --git a/glade/error_dialog.glade b/glade/error_dialog.glade new file mode 100644 index 0000000..01bd615 --- /dev/null +++ b/glade/error_dialog.glade @@ -0,0 +1,160 @@ + + + + + + + 12 + True + + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + True + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + True + False + + + + + True + False + 6 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-close + True + GTK_RELIEF_NORMAL + True + -7 + + + + + + 0 + False + True + GTK_PACK_END + + + + + + True + False + 6 + + + + True + True + <b><big>Error Title</big></b> + False + True + GTK_JUSTIFY_LEFT + False + True + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + False + 12 + + + + True + gtk-dialog-warning + 6 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + True + + + + + + 350 + 50 + True + True + This area contains a description of the error. + +In this case the error is made up and I'll just fill this with a pile of junk. Oh yes look at all the junk here. + False + False + GTK_JUSTIFY_LEFT + True + True + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + True + True + + + + + 0 + True + True + + + + + 0 + True + True + + + + + + + diff --git a/glade/glchess.glade b/glade/glchess.glade new file mode 100644 index 0000000..88e450f --- /dev/null +++ b/glade/glchess.glade @@ -0,0 +1,1463 @@ + + + + + + + Preferences + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + True + + + + + True + False + 0 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-close + True + GTK_RELIEF_NORMAL + True + -7 + + + + + + 0 + False + True + GTK_PACK_END + + + + + + True + True + True + True + GTK_POS_TOP + False + False + + + + 3 + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + False + 6 + + + + True + True + Start with unfinished games + True + GTK_RELIEF_NORMAL + True + True + False + True + + + 0 + False + False + + + + + + True + If no previous game loaded then start with: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 18 + 0 + + + + True + False + 6 + + + + True + False + 6 + + + + True + True + Human versus AI + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 0 + False + False + + + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 18 + 0 + + + + True + False + True + + + + + 0 + True + True + + + + + 0 + False + False + + + + + + True + True + Human versus human + True + GTK_RELIEF_NORMAL + True + False + False + True + radiobutton2 + + + 0 + False + False + + + + + + True + True + No game + True + GTK_RELIEF_NORMAL + True + False + False + True + radiobutton2 + + + 0 + False + False + + + + + + + 0 + True + True + + + + + + + + + + True + <b>Initial Game</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + False + True + + + + + + True + Startup + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + + 6 + True + False + 6 + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + 2 + 2 + False + 6 + 6 + + + + True + True + True + GTK_POS_TOP + 1 + GTK_UPDATE_CONTINUOUS + False + 0 0 0 0 0 0 + + + 1 + 2 + 1 + 2 + + + + + + True + False + True + + + 1 + 2 + 0 + 1 + fill + + + + + + True + Quality: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + True + Models: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + + + + + True + <b>Chess Pieces</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + False + True + + + + + + True + False + 6 + + + + True + 6 + gtk-dialog-info + 0.5 + 0.5 + 0 + 0 + + + 0 + False + True + + + + + + True + New piece sets can be installed by dragging them into this window + False + False + GTK_JUSTIFY_LEFT + True + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + True + True + GTK_PACK_END + + + + + 0 + True + True + + + + + False + True + + + + + + True + Graphics + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + + 6 + True + 2 + 2 + False + 6 + 6 + + + + True + True + + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 1 + 2 + 0 + 1 + fill + + + + + + + True + True + + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 1 + 2 + 1 + 2 + fill + + + + + + + True + Rotate board to face human player: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + True + Animate moving pieces + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + False + True + + + + + + True + Animation + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + + 6 + True + False + 6 + + + + True + True + GTK_POLICY_NEVER + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + True + False + False + True + False + False + False + + + + + 0 + True + True + + + + + + True + TODO + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + + + + False + True + + + + + + True + Artifical Intelligence + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + 0 + True + True + + + + + + + + True + glChess + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 400 + True + False + glchess.svg + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + + True + False + 0 + + + + True + GTK_PACK_DIRECTION_LTR + GTK_PACK_DIRECTION_LTR + + + + True + _Game + True + + + + + + + True + gtk-new + True + + + + + + + _Join Game + True + + + + + + True + gtk-connect + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + gtk-open + True + + + + + + + True + False + gtk-save + True + + + + + + + True + False + _End Game + True + + + + + + True + gtk-close + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + + + + + + True + gtk-quit + True + + + + + + + + + + + True + _View + True + + + + + + + True + _3D + True + False + + + + + + + True + _Toolbar + True + True + + + + + + + True + _History + True + False + + + + + + + True + + + + + + True + _AI Information + True + False + + + + + + + + + + + True + _Help + True + + + + + + True + _Contents + True + True + + + + + + + True + gtk-about + True + + + + + + + + + + 0 + False + False + + + + + + True + GTK_ORIENTATION_HORIZONTAL + GTK_TOOLBAR_BOTH + True + True + + + + True + Start a new game + New Game + True + gtk-new + True + True + False + + + + False + True + + + + + + Join an existing game + Join Game + True + gtk-connect + True + True + False + + + + False + True + + + + + + True + Load a saved game + gtk-open + True + True + False + + + + False + True + + + + + + gtk-preferences + True + True + False + + + + False + True + + + + + + True + True + True + True + + + False + False + + + + + + True + False + Save the current game + gtk-save + True + True + False + + + + False + True + + + + + + False + Surrender + True + gtk-dialog-warning + True + True + False + + + + False + True + + + + + + True + False + End the current game + End Game + True + gtk-close + True + True + False + + + + False + True + + + + + 0 + False + False + + + + + + True + False + 3 + + + + 300 + 300 + True + GTK_SHADOW_NONE + + + + + + + 0 + True + True + + + + + + False + False + 0 + + + + True + Rewind to the game start + True + GTK_RELIEF_NORMAL + True + + + + + True + gtk-goto-first + 4 + 0.5 + 0.5 + 0 + 0 + + + + + 0 + False + False + + + + + + True + Show the previous move + True + GTK_RELIEF_NORMAL + True + + + + + True + gtk-go-back + 4 + 0.5 + 0.5 + 0 + 0 + + + + + 0 + False + False + + + + + + True + False + True + + + + 0 + True + True + + + + + + True + Show the next move + True + GTK_RELIEF_NORMAL + True + + + + + True + gtk-go-forward + 4 + 0.5 + 0.5 + 0 + 0 + + + + + 0 + False + False + + + + + + True + Show the current move + True + GTK_RELIEF_NORMAL + True + + + + + True + gtk-goto-last + 4 + 0.5 + 0.5 + 0 + 0 + + + + + 0 + False + False + + + + + 0 + False + True + + + + + 0 + True + True + + + + + + + + 6 + AI Information + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 300 + 200 + True + False + glchess.svg + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + + True + False + False + GTK_POS_TOP + True + False + + + + 6 + True + False + 6 + + + + True + gtk-dialog-info + 6 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + True + + + + + + True + There are not artificial intelligences in use + False + False + GTK_JUSTIFY_LEFT + True + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + True + True + + + + + False + True + + + + + + True + Summary + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + + + diff --git a/glade/load_game.glade b/glade/load_game.glade new file mode 100644 index 0000000..a4c5084 --- /dev/null +++ b/glade/load_game.glade @@ -0,0 +1,111 @@ + + + + + + + 6 + True + Load a chess game + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + glchess.svg + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + True + + + + + True + False + 0 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + -6 + + + + + + + True + False + True + True + gtk-properties + True + GTK_RELIEF_NORMAL + True + -10 + + + + + + + True + False + True + True + gtk-open + True + GTK_RELIEF_NORMAL + True + -5 + + + + + + 0 + False + True + GTK_PACK_END + + + + + + 600 + 400 + True + GTK_FILE_CHOOSER_ACTION_OPEN + True + False + False + + + + + 0 + True + True + + + + + + + diff --git a/glade/network_game.glade b/glade/network_game.glade new file mode 100644 index 0000000..9ed9506 --- /dev/null +++ b/glade/network_game.glade @@ -0,0 +1,413 @@ + + + + + + + 300 + 300 + Waiting for players + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + True + + + + True + False + 0 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + -6 + + + + + + True + False + True + True + GTK_RELIEF_NORMAL + True + 0 + + + + True + 0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + + + + True + False + 2 + + + + True + gtk-go-forward + 4 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + False + + + + + + True + Ready + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + + + + + + 0 + False + True + GTK_PACK_END + + + + + + 12 + True + False + 12 + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + 2 + 2 + False + 6 + 6 + + + + True + White Player: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + True + Black Player: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + True + False + False + True + + + 1 + 2 + 0 + 1 + fill + + + + + + True + False + False + True + + + 1 + 2 + 1 + 2 + fill + + + + + + + + + + True + <b>Players</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + False + True + + + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + False + 6 + + + + True + True + GTK_POLICY_NEVER + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + False + False + True + GTK_JUSTIFY_LEFT + GTK_WRAP_NONE + False + 0 + 0 + 0 + 0 + 0 + 0 + + + + + + 0 + True + True + + + + + + True + True + True + True + 0 + + True + * + False + + + 0 + False + False + + + + + + + + + + True + <b>Status / Chat</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + True + True + + + + + 0 + True + True + + + + + + + diff --git a/glade/new_game.glade b/glade/new_game.glade new file mode 100644 index 0000000..84c93d5 --- /dev/null +++ b/glade/new_game.glade @@ -0,0 +1,1230 @@ + + + + + + + New Game + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + glchess.svg + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + True + + + + + True + False + 6 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + -6 + + + + + + True + Start the game. The game can be started once all fields are complete + True + True + True + True + GTK_RELIEF_NORMAL + True + -5 + + + + True + 0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + + + + True + False + 2 + + + + True + gtk-new + 4 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + False + + + + + + True + _Start + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + + + + + + 0 + False + True + GTK_PACK_END + + + + + + 12 + True + False + 12 + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + 2 + 2 + False + 6 + 6 + + + + True + Game name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + Allow Spectators: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + 250 + True + Enter the title for this game + True + True + True + 0 + Local chess game + True + * + False + + + + 1 + 2 + 0 + 1 + + + + + + + Allow remote clients to watch this game + True + + True + GTK_RELIEF_NORMAL + True + True + False + True + + + 1 + 2 + 1 + 2 + + + + + + + + + + + True + <b>Game Properties</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + False + True + + + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + 3 + 2 + False + 6 + 6 + + + + True + False + True + + + + 1 + 2 + 1 + 2 + + + + + + True + Type: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + Difficulty: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 2 + 3 + fill + + + + + + + True + True + GTK_POS_RIGHT + 0 + GTK_UPDATE_DISCONTINUOUS + False + 0 0 0 0 0 0 + + + 1 + 2 + 2 + 3 + fill + fill + + + + + + True + True + True + True + 0 + White + True + * + False + + + + 1 + 2 + 0 + 1 + + + + + + + True + Name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + + + + + True + <b>White Player</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + False + True + + + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + 3 + 2 + False + 6 + 6 + + + + True + False + True + + + + 1 + 2 + 1 + 2 + + + + + + True + Type: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + Difficulty: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 2 + 3 + fill + + + + + + + True + True + GTK_POS_RIGHT + 0 + GTK_UPDATE_DISCONTINUOUS + False + 0 0 0 0 0 0 + + + 1 + 2 + 2 + 3 + fill + fill + + + + + + True + True + True + True + 0 + Black + True + * + False + + + + 1 + 2 + 0 + 1 + + + + + + + True + Name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + + + + + True + <b>Black Player</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + False + True + + + + + 0 + True + True + + + + + + + + True + Join Game + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + glchess.svg + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + True + + + + + True + False + 0 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + -6 + + + + + + True + True + True + GTK_RELIEF_NORMAL + True + -5 + + + + True + 0.5 + 0.5 + 0 + 0 + 0 + 0 + 0 + 0 + + + + True + False + 2 + + + + True + gtk-connect + 4 + 0.5 + 0.5 + 0 + 0 + + + 0 + False + False + + + + + + True + _Join + True + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + + + + + + 0 + False + True + GTK_PACK_END + + + + + + 12 + True + False + 12 + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + 2 + 3 + False + 6 + 6 + + + + True + Servers: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + fill + + + + + + True + True + Find Servers + True + GTK_RELIEF_NORMAL + True + + + + 2 + 3 + 1 + 2 + fill + + + + + + + True + The hostname/IP address to search for servers on + True + True + True + 0 + + True + * + False + + + + + 1 + 2 + 1 + 2 + + + + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + GTK_CORNER_TOP_LEFT + + + + True + True + True + False + False + True + False + False + False + + + + + 1 + 3 + 0 + 1 + fill + fill + + + + + + + + + + True + <b>Game to Join</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + False + True + + + + + + True + 0 + 0.5 + GTK_SHADOW_NONE + + + + True + 0.5 + 0.5 + 1 + 1 + 0 + 0 + 12 + 0 + + + + True + 3 + 2 + False + 6 + 6 + + + + True + False + True + + + + 1 + 2 + 1 + 2 + + + + + + True + Type: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + Difficulty: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 2 + 3 + fill + + + + + + + True + True + GTK_POS_RIGHT + 0 + GTK_UPDATE_DISCONTINUOUS + False + 0 0 0 0 0 0 + + + 1 + 2 + 2 + 3 + fill + fill + + + + + + 250 + True + True + True + True + 0 + Local chess player + True + * + False + + + + 1 + 2 + 0 + 1 + + + + + + + True + Name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + + + + + True + <b>Local Player</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + label_item + + + + + 0 + False + False + + + + + 0 + True + True + + + + + + + diff --git a/glade/save_game.glade b/glade/save_game.glade new file mode 100644 index 0000000..676e9df --- /dev/null +++ b/glade/save_game.glade @@ -0,0 +1,80 @@ + + + + + + + True + GTK_FILE_CHOOSER_ACTION_SAVE + True + False + False + False + Save chess game + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + glchess.svg + True + False + False + GDK_WINDOW_TYPE_HINT_DIALOG + GDK_GRAVITY_NORTH_WEST + True + False + + + + + True + False + 24 + + + + True + GTK_BUTTONBOX_END + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + -6 + + + + + + + True + True + True + True + gtk-save + True + GTK_RELIEF_NORMAL + True + -5 + + + + + + 0 + False + True + GTK_PACK_END + + + + + + + diff --git a/glchess.desktop.in b/glchess.desktop.in new file mode 100644 index 0000000..0924a0d --- /dev/null +++ b/glchess.desktop.in @@ -0,0 +1,15 @@ +[Desktop Entry] +_Name=Chess +_Comment=Play the classic two-player boardgame of glChess +Version=1.0 +Encoding=UTF-8 +Exec=glchess +Terminal=false +Type=Application +Categories=GNOME;Application;Game;BoardGame; +StartupNotify=true +Icon=glchess +MimeType=application/x-chess-pgn; +GenericName=3D Chess Game +GenericName[pt_BR]=Jogo de Xadrez 3D +GenericName[de]=3D Schach diff --git a/help/C/Makefile.am b/help/C/Makefile.am new file mode 100644 index 0000000..41efdda --- /dev/null +++ b/help/C/Makefile.am @@ -0,0 +1,7 @@ +docname = glchess +lang = C +helpdir = $(datadir)/gnome/help/$(docname)/$(lang) +help_DATA = glchess.xml legal.xml +EXTRA_DIST = $(help_DATA) + +EXTRA_DIST += $(helpfig_DATA) diff --git a/help/C/glchess-C.omf b/help/C/glchess-C.omf new file mode 100644 index 0000000..d746b94 --- /dev/null +++ b/help/C/glchess-C.omf @@ -0,0 +1,24 @@ + + + + + glChess Manual + + + 2006-10-14 + + + + + glChess + + + user's guide + + + + + + + + diff --git a/help/C/glchess.xml b/help/C/glchess.xml new file mode 100644 index 0000000..8fff30b --- /dev/null +++ b/help/C/glchess.xml @@ -0,0 +1,15 @@ + + +
+ + glChess documentation + April 17th, 2004 + + + TODO! + +
diff --git a/help/C/legal.xml b/help/C/legal.xml new file mode 100644 index 0000000..ac97e1d --- /dev/null +++ b/help/C/legal.xml @@ -0,0 +1,76 @@ + + + Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free Documentation + License (GFDL), Version 1.1 or any later version published + by the Free Software Foundation with no Invariant Sections, + no Front-Cover Texts, and no Back-Cover Texts. You can find + a copy of the GFDL at this link or in the file COPYING-DOCS + distributed with this manual. + + This manual is part of a collection of GNOME manuals + distributed under the GFDL. If you want to distribute this + manual separately from the collection, you can do so by + adding a copy of the license to the manual, as described in + section 6 of the license. + + + + Many of the names used by companies to distinguish their + products and services are claimed as trademarks. Where those + names appear in any GNOME documentation, and the members of + the GNOME Documentation Project are made aware of those + trademarks, then the names are in capital letters or initial + capital letters. + + + + DOCUMENT AND MODIFIED VERSIONS OF THE DOCUMENT ARE PROVIDED + UNDER THE TERMS OF THE GNU FREE DOCUMENTATION LICENSE + WITH THE FURTHER UNDERSTANDING THAT: + + + + DOCUMENT IS PROVIDED ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR + IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES + THAT THE DOCUMENT OR MODIFIED VERSION OF THE + DOCUMENT IS FREE OF DEFECTS MERCHANTABLE, FIT FOR + A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE + RISK AS TO THE QUALITY, ACCURACY, AND PERFORMANCE + OF THE DOCUMENT OR MODIFIED VERSION OF THE + DOCUMENT IS WITH YOU. SHOULD ANY DOCUMENT OR + MODIFIED VERSION PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL WRITER, AUTHOR OR ANY + CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY + SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS + LICENSE. NO USE OF ANY DOCUMENT OR MODIFIED + VERSION OF THE DOCUMENT IS AUTHORIZED HEREUNDER + EXCEPT UNDER THIS DISCLAIMER; AND + + + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL + THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), + CONTRACT, OR OTHERWISE, SHALL THE AUTHOR, + INITIAL WRITER, ANY CONTRIBUTOR, OR ANY + DISTRIBUTOR OF THE DOCUMENT OR MODIFIED VERSION + OF THE DOCUMENT, OR ANY SUPPLIER OF ANY OF SUCH + PARTIES, BE LIABLE TO ANY PERSON FOR ANY + DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR + CONSEQUENTIAL DAMAGES OF ANY CHARACTER + INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS + OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR + MALFUNCTION, OR ANY AND ALL OTHER DAMAGES OR + LOSSES ARISING OUT OF OR RELATING TO USE OF THE + DOCUMENT AND MODIFIED VERSIONS OF THE DOCUMENT, + EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF + THE POSSIBILITY OF SUCH DAMAGES. + + + + + + diff --git a/help/Makefile.am b/help/Makefile.am new file mode 100644 index 0000000..42ffacc --- /dev/null +++ b/help/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = C diff --git a/mime/glchess.xml b/mime/glchess.xml new file mode 100644 index 0000000..4c7a987 --- /dev/null +++ b/mime/glchess.xml @@ -0,0 +1,8 @@ + + + + + PGN chess game + + + \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..649edec --- /dev/null +++ b/setup.py @@ -0,0 +1,63 @@ +import os +import glob + +from distutils.core import setup + +DESCRIPTION = """glChess is an open source 3D chess interface for the Gnome desktop. +It is designed to be used by both beginner and experienced players. +Games can be played between a combination of local players, players connected via a LAN and artificial intelligences. +""" + +CLASSIFIERS = ['License :: OSI-Approved Open Source :: GNU General Public License (GPL)', + 'Intended Audience :: by End-User Class :: End Users/Desktop', + 'Development Status :: 4 - Beta', + 'Topic :: Desktop Environment :: Gnome', + 'Topic :: Games/Entertainment :: Board Games', + 'Programming Language :: Python', + 'Operating System :: Grouping and Descriptive Categories :: All POSIX (Linux/BSD/UNIX-like OSes)', + 'Operating System :: Modern (Vendor-Supported) Desktop Operating Systems :: Linux', + 'User Interface :: Graphical :: Gnome', + 'User Interface :: Graphical :: OpenGL', + 'User Interface :: Toolkits/Libraries :: GTK+', + 'Translations :: English', 'Translations :: German', 'Translations :: Italian'] + +DATA_FILES = [] + +# MIME files +DATA_FILES.append(('share/mime/packages', ['mime/glchess.xml'])) + +# UI files +DATA_FILES.append(('share/games/glchess/gui', ['glchess.svg'] + glob.glob('lib/glchess/gtkui/*.glade'))) + +# Config files +DATA_FILES.append(('share/games/glchess/', ['ai.xml'])) + +# Texture files +TEXTURES = [] +for file in ['board.png', 'piece.png']: + TEXTURES.append('textures/' + file) +DATA_FILES.append(('share/games/glchess/textures', TEXTURES)) + +DATA_FILES.append(('share/applications', ['glchess.desktop'])) +DATA_FILES.append(('share/pixmaps', ['glchess.svg'])) + +# Language files +#for poFile in glob.glob('po/*.po'): +# language = poFile[3:-3] +# DATA_FILES.append(('share/locale/' + language + '/LC_MESSAGES', poFile)) +#print DATA_FILES + +setup(name = 'glchess', + version = '1.0RC1', + classifiers = CLASSIFIERS, + description = '3D Chess Interface', + long_description = DESCRIPTION, + author = 'Robert Ancell', + author_email = 'bob27@users.sourceforge.net', + license = 'GPL', + url = 'http://glchess.sourceforge.net', + download_url = 'http://sourceforge.net/project/showfiles.php?group_id=6348', + package_dir = {'': 'lib'}, + packages = ['glchess', 'glchess.chess', 'glchess.scene', 'glchess.scene.cairo', 'glchess.scene.opengl', 'glchess.ui', 'glchess.gtkui', 'glchess.network'], + data_files = DATA_FILES, + scripts = ['glchess']) diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..0262e4d --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = lib diff --git a/src/glchess b/src/glchess new file mode 100755 index 0000000..78a7400 --- /dev/null +++ b/src/glchess @@ -0,0 +1,10 @@ +#! /usr/bin/env python + +try: + from glchess.glchess import start_game +except ImportError: + import sys + sys.path.append('/usr/share/glchess/') + from glchess.glchess import start_game + +start_game() diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am new file mode 100644 index 0000000..e0f769b --- /dev/null +++ b/src/lib/Makefile.am @@ -0,0 +1,12 @@ +SUBDIRS = chess gtkui network scene ui + +glchessdir = $(pythondir)/glchess +glchess_PYTHON = \ + ai.py \ + cecp.py \ + defaults.py \ + game.py \ + glchess.py \ + __init__.py \ + main.py \ + uci.py diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib/__init__.py @@ -0,0 +1 @@ + diff --git a/src/lib/ai.py b/src/lib/ai.py new file mode 100644 index 0000000..d8bc784 --- /dev/null +++ b/src/lib/ai.py @@ -0,0 +1,335 @@ +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +import os +import sys +import select +import xml.dom.minidom + +import game +import cecp +import uci + +from defaults import * + +CECP = 'CECP' +UCI = 'UCI' + +class Option: + value = '' + + pass + +class Profile: + """ + """ + name = '' + protocol = '' + executable = '' + path = '' + arguments = None + options = None + + def __init__(self): + self.arguments = [] + self.options = [] + + def detect(self): + """ + """ + try: + path = os.environ['PATH'].split(os.pathsep) + except KeyError: + path = [] + + for directory in path: + b = directory + os.sep + self.executable + if os.path.isfile(b): + self.path = b + return + self.path = None + +def _getXMLText(node): + """ + """ + if len(node.childNodes) == 0: + return '' + if len(node.childNodes) > 1 or node.childNodes[0].nodeType != node.TEXT_NODE: + raise ValueError + return node.childNodes[0].nodeValue + +def loadProfiles(): + """ + """ + profiles = [] + + fileNames = [os.path.expanduser('~/.glchess/ai.xml'), os.path.join(BASE_DIR,'ai.xml'), 'ai.xml'] + document = None + for f in fileNames: + try: + document = xml.dom.minidom.parse(f) + except IOError: + pass + except xml.parsers.expat.ExpatError: + print 'AI configuration at ' + f + ' is invalid, ignoring' + else: + print 'Loading AI configuration from ' + f + break + if document is None: + print 'WARNING: No AI configuration' + return profiles + + elements = document.getElementsByTagName('aiconfig') + if len(elements) == 0: + return profiles + + for e in elements: + for p in e.getElementsByTagName('ai'): + try: + protocolName = p.attributes['type'].nodeValue + except KeyError: + assert(False) + if protocolName == 'cecp': + protocol = CECP + elif protocolName == 'uci': + protocol = UCI + else: + assert(False), 'Uknown AI type: ' + repr(protocolName) + + n = p.getElementsByTagName('name') + assert(len(n) > 0) + name = _getXMLText(n[0]) + + n = p.getElementsByTagName('binary') + assert(len(n) > 0) + executable = _getXMLText(n[0]) + + arguments = [executable] + n = p.getElementsByTagName('argument') + for x in n: + args.append(_getXMLText(x)) + + options = [] + n = p.getElementsByTagName('option') + for x in n: + option = Option() + option.value = _getXMLText(x) + try: + attribute = x.attributes['name'] + except KeyError: + pass + else: + option.name = _getXMLText(attribute) + options.append(option) + + profile = Profile() + profile.name = name + profile.protocol = protocol + profile.executable = executable + profile.arguments = arguments + profile.options = options + profiles.append(profile) + + return profiles + +class CECPConnection(cecp.Connection): + """ + """ + player = None + + def __init__(self, player): + """ + """ + self.player = player + cecp.Connection.__init__(self) + + def onOutgoingData(self, data): + """Called by cecp.Connection""" + self.player.logText(data, 'output') + self.player.sendToEngine(data) + + def onMove(self, move): + """Called by cecp.Connection""" + self.player.moving = True + self.player.move(move) + + def logText(self, text, style): + """Called by cecp.Connection""" + self.player.logText(text, style) + +class UCIConnection(uci.StateMachine): + """ + """ + player = None + + def __init__(self, player): + """ + """ + self.player = player + uci.StateMachine.__init__(self) + + def onOutgoingData(self, data): + """Called by uci.StateMachine""" + self.player.logText(data, 'output') + self.player.sendToEngine(data) + + def logText(self, text, style): + """Called by uci.StateMachine""" + self.player.logText(text, style) + + def onMove(self, move): + """Called by uci.StateMachine""" + self.player.move(move) + +class Player(game.ChessPlayer): + """ + """ + + # The profile we are using + __profile = None + + __pipeFromEngine = None + __pipeToEngine = None + + # PID of the subprocess containing the engine + __pid = None + + __connection = None + + moving = False + + def __init__(self, name, profile): + """Constructor for an AI player. + + 'name' is the name of the player (string). + 'profile' is the profile to use for the AI (Profile). + """ + self.__profile = profile + + game.ChessPlayer.__init__(self, name) + + # Create pipes for communication with AI process + self.__pipeToEngine = os.pipe() + self.__pipeFromEngine = os.pipe() + + # Fork sub-process for engine + self.__pid = os.fork() + + # This is the forked process, replace it with the engine + if self.__pid == 0: + self.__startEngine(profile.path, profile.arguments) + + # Close the ends of the pipe we are not using + os.close(self.__pipeFromEngine[1]); + os.close(self.__pipeToEngine[0]); + + if profile.protocol == CECP: + self.connection = CECPConnection(self) + elif profile.protocol == UCI: + self.connection = UCIConnection(self) + else: + assert(False) + + self.connection.start() + self.connection.configure(profile.options) + + # Methods to extend + + def logText(self, text, style): + """ + """ + pass + + # Public methods + + def getProfile(self): + """ + """ + return self.__profile + + def fileno(self): + """Returns the file descriptor for communicating with the engine (integer)""" + return self.__pipeFromEngine[0] + + def read(self): + """Read an process data from the engine. + + This is non-blocking. + """ + while True: + # Check if data is available + (rlist, _, _) = select.select([self.__pipeFromEngine[0]], [], [], 0) + if len(rlist) == 0: + return + + # Read a chunk and process + data = os.read(self.__pipeFromEngine[0], 256) + if data == '': + return + self.connection.registerIncomingData(data) + + def sendToEngine(self, data): + """ + """ + try: + os.write(self.__pipeToEngine[1], data) + except OSError, e: + print 'Failed to write to engine: ' + str(e) + + def quit(self): + """Disconnect the AI""" + # Wait for the pipe to close + # There must be a better way of doing this! + count = 0 + while True: + select.select([], [], [], 0.1) + try: + os.write(self.__pipeToEngine[1], '\nquit\n') # FIXME: CECP specific + except OSError: + return + count += 1 + if count > 5: + break + + print 'Killing AI ' + str(self.__pid) + os.kill(self.__pid, 9) + + # Extended methods + + def onPlayerMoved(self, player, move): + """Called by game.ChessPlayer""" + isSelf = player is self and self.moving + self.moving = False + self.connection.reportMove(move.canMove, isSelf) + + def readyToMove(self): + """Called by game.ChessPlayer""" + self.connection.requestMove() + + def onGameEnded(self, winningPlayer = None): + """Called by game.ChessPlayer""" + self.quit() + + # Private methods + + def __startEngine(self, executable, arguments): + """ + """ + # Connect stdin and stdout to the pipes to the main process + os.dup2(self.__pipeFromEngine[1], sys.stdout.fileno()) + os.dup2(self.__pipeToEngine[0], sys.stdin.fileno()) + + # Close the ends of the pipe we are not using + os.close(self.__pipeFromEngine[0]); + os.close(self.__pipeToEngine[1]); + + # Make the process nice so it doesn't hog the CPU + os.nice(15) + + # Start the AI + try: + os.execv(executable, arguments) + except OSError: + select.select([], [], [], None) + os._exit(os.EX_UNAVAILABLE) diff --git a/src/lib/cecp.py b/src/lib/cecp.py new file mode 100644 index 0000000..0b32cd5 --- /dev/null +++ b/src/lib/cecp.py @@ -0,0 +1,218 @@ +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +class CECPProtocol: + """CECP protocol en/decoder. + + + """ + + # Data being accumulated to be parsed + __buffer = '' + + NEWLINE = '\n' + MOVE_PREFIXS = ['My move is: ', 'my move is ', 'move '] + INVALID_MOVE_PREFIX = 'Illegal move: ' + RESIGN_PREFIX = 'tellics resign' + DRAW_PREFIX = '1/2-1/2' + + def __init__(self): + """ + """ + # Go to simple interface mode + self.onOutgoingData('xboard\n') + + # Methods to extend + + def onOutgoingData(self, data): + """Called when there is data to send to the CECP engine. + + 'data' is the data to give to the AI (string). + """ + print 'OUT: ' + repr(data) + + def onUnknownLine(self, line): + """Called when an unknown line is received from the CECP AI. + + 'line' is the line that has not been decoded (string). There is + no newline on the end of the string. + """ + print 'Unknown CECP line: ' + line + + def onMove(self, move): + """Called when the AI makes a move. + + 'move' is the move the AI has decided to make (string). + """ + print 'CECP move: ' + move + + def onIllegalMove(self, move): + """Called when the AI rejects a move. + + 'move' is the move the AI rejected (string). + """ + print 'CECP illegal move: ' + move + + def onResign(self): + """Called when the AI resigns""" + print 'CECP AI resigns' + + # Public methods + + def sendSetSearchDepth(self, searchDepth): + """Set the search depth for the AI. + + 'searchDepth' is the number of moves to look ahead (integer). + """ + # This is the CECP specified method + self.onOutgoingData('sd %i\n' % int(searchDepth)) + + # GNUchess uses this instead + self.onOutgoingData('depth %i\n' % int(searchDepth)) + + def sendSetPondering(self, aiPonders): + """Enable/disable AI pondering. + + 'aiPonders' is a flag to show if the AI thinks during opponent moves (True) or not (False). + """ + if aiPonders: + self.onOutgoingData('hard\n') + else: + self.onOutgoingData('easy\n') + + def sendMove(self, move): + """Move for the current player. + + 'move' is the move the current player has made (string). + """ + self.onOutgoingData(move + '\n') + + def sendWait(self): + """Stop the AI from automatically moving""" + self.onOutgoingData('force\n') + + def sendMovePrompt(self): + """Get the AI to move for the current player""" + self.onOutgoingData('go\n') + + def sendQuit(self): + """Quit the engine""" + # Send 'quit' starting with a newline in case there are some characters already sent + self.onOutgoingData('\nquit\n') + + def registerIncomingData(self, data): + """ + """ + self.__buffer += data + self.__parseData() + + # Private methods + + def __parseData(self): + while True: + index = self.__buffer.find(self.NEWLINE) + if index < 0: + return + + line = self.__buffer[:index] + self.__buffer = self.__buffer[index+1:] + + self.__parseLine(line) + + def __parseLine(self, line): + for prefix in self.MOVE_PREFIXS: + if line.startswith(prefix): + move = line[len(prefix):] + self.logText(line + '\n', 'move') + self.onMove(move.strip()) + return + + if line.startswith(self.INVALID_MOVE_PREFIX): + self.onIllegalMove(line[len(self.INVALID_MOVE_PREFIX):]) + + elif line.startswith(self.RESIGN_PREFIX): + self.onResign() + + elif line.startswith(self.DRAW_PREFIX): + print 'AI calls a draw' + + else: + self.onUnknownLine(line) + + self.logText(line + '\n', 'input') + +import select + +class Connection(CECPProtocol): + """ + """ + + def __init__(self): + """ + """ + # Start protocol + CECPProtocol.__init__(self) + + # Methods to extend + + def logText(self, text, style): + """FIXME: define style + """ + pass + + def onMove(self, move): + """Called when the AI makes a move. + + 'move' is the move the AI made (string). + """ + print 'AI moves: ' + move + + # Public methods + + def start(self): + """ + """ + pass + + def configure(self, options): + """ + """ + for option in options: + self.onOutgoingData(option.value + '\n') + + def requestMove(self): + """Request the AI moves for the current player""" + # Prompt the AI to move + self.sendMovePrompt() + + def reportMove(self, move, isSelf): + """Report the move the current player has made. + + 'move' is the move to report (string). + 'isSelf' is a flag to say if the move is the move this AI made (True). + """ + # Don't report the move we made + if isSelf: + return + + # Stop the AI from automatically moving + self.sendWait() + + # Report the move + self.sendMove(move) + + # Private methods + + def onUnknownLine(self, line): + """Called by CECPProtocol""" + pass#print 'Unknown CECP line: ' + line + + def onIllegalMove(self, move): + """Called by CECPProtocol""" + print 'CECP illegal move: ' + move + +if __name__ == '__main__': + c = CECPConnection('gnuchess') + while True: + c.read() diff --git a/src/lib/chess/Makefile.am b/src/lib/chess/Makefile.am new file mode 100644 index 0000000..a316995 --- /dev/null +++ b/src/lib/chess/Makefile.am @@ -0,0 +1,7 @@ +glchessdir = $(pythondir)/glchess/chess +glchess_PYTHON = \ + board.py \ + __init__.py \ + lan.py \ + pgn.py \ + san.py diff --git a/src/lib/chess/__init__.py b/src/lib/chess/__init__.py new file mode 100644 index 0000000..57ebfd5 --- /dev/null +++ b/src/lib/chess/__init__.py @@ -0,0 +1,4 @@ +import board +import pgn +import lan +import san diff --git a/src/lib/chess/board.py b/src/lib/chess/board.py new file mode 100644 index 0000000..58f922d --- /dev/null +++ b/src/lib/chess/board.py @@ -0,0 +1,1025 @@ +"""Module implementing the chess rules. + +To use create an instance of the chess board: +>>> b = board.ChessBoard() + +Board locations can be represented in two forms: +o A 2-tuple containing the file and rank as integers (see below). +o A string with the location in SAN format. + +e.g. The black king is on the square (4,7) or 'e8'. + +The chess board with rank and file numbers: + + Black Pieces + + +---+---+---+---+---+---+---+---+ +7 ||||||||| + +---+---+---+---+---+---+---+---+ +6 |

|

|

|

|

|

|

|

| + +---+---+---+---+---+---+---+---+ +5 | | . | | . | | . | | . | + +---+---+---+---+---+---+---+---+ +4 | . | | . | | . | | . | | + +---+---+---+---+---+---+---+---+ +3 | | . | | . | | . | | . | + +---+---+---+---+---+---+---+---+ +2 | . | | . | | . | | . | | + +---+---+---+---+---+---+---+---+ +1 |-P-|-P-|-P-|-P-|-P-|-P-|-P-|-P-| + +---+---+---+---+---+---+---+---+ +0 |-R-|-N-|-B-|-Q-|-K-|-B-|-N-|-R-| + +---+---+---+---+---+---+---+---+ + 0 1 2 3 4 5 6 7 + + White Pieces + +Pieces are moved by: +>>> result = b.movePiece(board.WHITE, 'd1', 'd3') +If the result is not MOVE_RESULT_ILLEGAL then the internal +state is updated and the next player can move. +If the result is MOVE_RESULT_ILLEGAL then the request is ignored. + +A move can be checked if it is legal first by: +>>> result = b.testMove(board.WHITE, 'd1', 'd3') +The returns the same values as movePiece() except the internal state +is never updated. + +The location of pieces can be checked using: +>>> piece = b.getPiece('e1') +>>> pieces = b.getAlivePieces() +>>> casualties = b.getDeadPieces() +The locations are always in the 2-tuple format. +These methods return references to the ChessPiece objects on the board. + +The history of the game can be retrieved by passing a move number to +the get*() methods. This number is the move count from the game start. +It also supports negative indexing: +0 = board before game starts +1 = board after white's first move +2 = board after black's first move +-1 = The last move +e.g. +To get the white pieces after whites second move. +>>> pieces = b.getAlivePieces(3) + +The ChessPiece objects are static per board. Thus references can be compared +between move 0 and move N. Note promoted pieces are a new piece object. + +When any piece is moved onPieceMoved() method is called. If the ChessBoard +class is extended this signal can be picked up by the user. If movePiece() +or testMove() is called while in this method an exception is raised. +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +__all__ = ['ChessPiece', 'ChessBoard'] + +# The two players +WHITE = 'White' +BLACK = 'Black' + +# The piece types +PAWN = 'P' +ROOK = 'R' +KNIGHT = 'N' +BISHOP = 'B' +QUEEN = 'Q' +KING = 'K' + +# Move results +MOVE_RESULT_ILLEGAL = 'Illegal move' +MOVE_RESULT_OPPONENT_CHECK = 'Opponent put into check' +MOVE_RESULT_OPPONENT_CHECKMATE = 'Opponent put into checkmate' +MOVE_RESULT_ALLOWED = 'Valid move' + +class ChessPiece: + """An object representing a chess piece""" + + # The colour of the piece + __colour = None + + # The type of the piece (pawn, knight ...) + __type = None + + def __init__(self, colour, type): + """Constructor for a chess piece. + + 'colour' is the piece colour (WHITE or BLACK). + 'type' is the piece type (PAWN, ROOK, KNIGHT, BISHOP, QUEEN or KING). + """ + self.__colour = colour + self.__type = type + + def getColour(self): + """Get the colour of this piece. + + Returns WHITE or BLACK. + """ + return self.__colour + + def getType(self): + """Get the type of this piece. + + Returns PAWN, ROOK, KNIGHT, BISHOP, QUEEN or KING. + """ + return self.__type + + def __str__(self): + """Returns a string representation of this piece""" + return self.__colour + ' ' + self.__type + +class ChessPlayerState: + """ + """ + + # Flags to show if still able to castle + canShortCastle = True + canLongCastle = True + + def __init__(self, state = None): + """ + """ + if state is None: + return + + # Copy the exisiting state + self.canShortCastle = state.canShortCastle + self.canLongCastle = state.canLongCastle + +class ChessBoardState: + """ + """ + # The move number + moveNumber = 0 + + # A dictionary of piece by location + squares = None + + # The dead pieces in the order they were killed + casualties = None + + # The move that got us to this state + lastMove = None + + # If the last move was a pawn martch The location where it can be taken by en-passant + enPassantSquare = None + + # The state of the players + whiteState = None + blackState = None + + def __init__(self, lastState = None): + """Constuctor for storing the state of a chess board. + + 'lastState' is the previous board state + or a dictionary containing the initial state of the board + or None to start an empty board. + + Example: + + pawn = ChessPiece(WHITE, PAWN) + ChessBoardState({'a2'': pawn, ...}) + + Note if a dictionary is provided the casualties will only record the pieces + killed from this point onwards. + """ + # Start empty + if lastState is None: + self.moveNumber = 0 + self.squares = {} + self.casualties = [] + self.whiteState = ChessPlayerState() + self.blackState = ChessPlayerState() + + # Use provided initial pieces + elif type(lastState) is dict: + self.moveNumber = 0 + self.squares = {} + self.casualties = [] + for coord, piece in lastState.iteritems(): + self.squares[coord] = piece + self.whiteState = ChessPlayerState() + self.blackState = ChessPlayerState() + + # Copy exisiting state + elif isinstance(lastState, ChessBoardState): + self.moveNumber = lastState.moveNumber + 1 + self.squares = lastState.squares.copy() + self.casualties = lastState.casualties[:] + self.lastMove = lastState.lastMove + self.enPassantSquare = lastState.enPassantSquare + self.whiteState = ChessPlayerState(lastState.whiteState) + self.blackState = ChessPlayerState(lastState.blackState) + + else: + raise TypeError('ChessBoardState(oldState) or ChessBoardState({(0,0):pawn, ...})') + + def getPiece(self, location): + """Get the piece at a given location. + + 'location' is the location in LAN format (string). + + Return the piece at this location or None if there is no piece there. + """ + assert(type(location) is str and len(location) == 2) + try: + return self.squares[location] + except KeyError: + return None + + def __range(self, start, end): + """ + """ + startNum = ord(start) + endNum = ord(end) + rangeString = '' + if startNum > endNum: + step = -1 + a = startNum + b = endNum - 1 + else: + step = 1 + a = startNum + b = endNum + 1 + for i in xrange(a, b, step): + rangeString += chr(i) + return rangeString + + def __checkOrtho(self, rankRange, fileRange): + """Check if the space between two squares is empty. + + 'start' is the first square in the form (file,rank). + 'end' is the last square in the form (file,rank). + + Return True if the squares between these two are empty and the move is a + valid orthogonal move. + """ + if len(rankRange) == 1: + for file in fileRange[1:-1]: + coord = rankRange[0] + file + if self.squares.has_key(coord): + return False + + elif len(fileRange) == 1: + for rank in rankRange[1:-1]: + coord = rank + fileRange[0] + if self.squares.has_key(coord): + return False + + else: + return False + + return True + + def __checkDiag(self, rankRange, fileRange): + # For diagonal moves change in co-ordinates must be identical + if len(rankRange) != len(fileRange): + return False + + # Check the squares between the start and end moves + for i in xrange(1, len(rankRange) - 1): + coord = rankRange[i] + fileRange[i] + if self.squares.has_key(coord): + return False + + return True + + def inCheck(self, colour): + """Test if the player with the given colour is in check. + + 'colour' is the colour of the player to check. + + Return True if they are in check (or checkmate) or False otherwise. + """ + # Find the location of this players king(s) + for kingCoord, king in self.squares.iteritems(): + + # Not our king + if king.getType() != KING or king.getColour() != colour: + continue + + # See if any enemy pieces can take this king + for enemyCoord, enemyPiece in self.squares.iteritems(): + # Ignore friendly pieces + if enemyPiece.getColour() == colour: + continue + + # See if this piece can take the king + (result, moves) = self.movePiece(enemyPiece.getColour(), enemyCoord, kingCoord, + testCheck = False, testCheckMate = False, + applyMove = False) + if result is not MOVE_RESULT_ILLEGAL: + return True + + return False + + def inCheckMate(self, colour): + """Test if the player with the given colour is in checkmate. + + 'colour' is the colour of the player to check. + + Return True if they are in checkmate or False otherwise. + """ + # If can move any of their pieces then not in checkmate + for coord, piece in self.squares.iteritems(): + # Only check pieces of the given colour + if piece.getColour() != colour: + continue + + # See if this piece can be moved anywhere + for rank in 'abcdefgh': + for file in '12345678': + (result, moves) = self.movePiece(colour, coord, rank + file, + testCheckMate = False, applyMove = False) + if result is not MOVE_RESULT_ILLEGAL: + return False + return True + + def movePiece(self, colour, start, end, promotionType = QUEEN, testCheck = True, testCheckMate = True, allowSuicide = False, applyMove = True): + """Move a piece. + + 'colour' is the colour of the player moving. + 'start' is a the location to move from in LAN format (string). + 'end' is a the location to move to in LAN format (string). + 'promotionType' is the type of piece to promote to if required. + 'testCheck' is a flag to control if the opponent will be in check after this move. + 'testCheckMate' is a flag to control if the opponent will be in checkmate after this move. + 'allowSuicide' if True means a move is considered valid even + if it would put the moving player in check. + 'applyMove' is a flag to control if the move is applied to the board (True) or just tested (False). + + Return a tuple containing the result of this move and the pieces moved in the form (result, moves). + The moves are a list containing tuples of the form (piece, start, end). If a piece was removed + 'end' is None. If the result is successful the pieces on the board are modified. + """ + assert(promotionType is not KING) + assert(type(start) is str and len(start) == 2) + assert(type(end) is str and len(end) == 2) + + # A list of pieces that have been moved + moves = [] + + # Get the piece to move + try: + piece = self.squares[start] + except KeyError: + return (MOVE_RESULT_ILLEGAL, None) + if piece.getColour() is not colour: + return (MOVE_RESULT_ILLEGAL, None) + + # Get the players + if piece.getColour() is WHITE: + enemyColour = BLACK + playerState = self.whiteState + elif piece.getColour() is BLACK: + enemyColour = WHITE + playerState = self.blackState + else: + assert(False) + + # Copy the player state before it is changed + originalPlayerState = ChessPlayerState(playerState) + + # Check if moving onto another piece (must be enemy) + try: + target = self.squares[end] + if target.getColour() == piece.getColour(): + return (MOVE_RESULT_ILLEGAL, None) + except KeyError: + target = None + victim = target + + # Get the range of rank and files being moved over + rankRange = self.__range(start[0], end[0]) + fileRange = self.__range(start[1], end[1]) + assert(len(rankRange) >= 1) + assert(len(fileRange) >= 1) + + # Get the rank relative to this colour's start rank + if piece.getColour() == BLACK: + baseFile = '8' + else: + baseFile = '1' + + # The new en-passant square + enPassantSquare = None + + # Check move is valid: + + # King can move one square or castle + if piece.getType() is KING: + # Castling: + shortCastle = ('e' + baseFile, 'c' + baseFile) + longCastle = ('e' + baseFile, 'g' + baseFile) + if (playerState.canShortCastle and (start, end) == shortCastle) or (playerState.canLongCastle and (start, end) == longCastle): + if end[0] == 'c': + rookLocation = 'a' + baseFile + rookEndLocation = 'd' + baseFile + kingRanks = 'cd' + else: + rookLocation = 'h' + baseFile + rookEndLocation = 'f' + baseFile + kingRanks = 'fg' + + # Check rook is still there + try: + rook = self.squares[rookLocation] + except KeyError: + return (MOVE_RESULT_ILLEGAL, None) + if rook is None or rook.getType() is not ROOK or rook.getColour() != piece.getColour(): + return (MOVE_RESULT_ILLEGAL, None) + + # Check no pieces between the rook and king + for rank in kingRanks: + if self.squares.has_key(rank + start[1]): + return (MOVE_RESULT_ILLEGAL, None) + + # Test if in check on any of the squares the king moves + # through by filling these squares with cloned kings + for rank in kingRanks: + self.squares[rank + start[1]] = piece + inCheck = self.inCheck(piece.getColour()) + for rank in kingRanks: + self.squares.pop(rank + start[1]) + + if inCheck: + return (MOVE_RESULT_ILLEGAL, None) + + # Move rook and record so can be undone + moves.append((rook, rookLocation, rookEndLocation)) + + # Otherwise can only move one square + else: + if len(rankRange) > 2 or len(fileRange) > 2: + return (MOVE_RESULT_ILLEGAL, None) + + # Can no longer castle if moved the king + playerState.canShortCastle = False + playerState.canLongCastle = False + + moves.append((piece, start, end)) + + # Queen moves orthogonal or diagonal + elif piece.getType() is QUEEN: + if (not self.__checkOrtho(rankRange, fileRange)) and (not self.__checkDiag(rankRange, fileRange)): + return (MOVE_RESULT_ILLEGAL, None) + moves.append((piece, start, end)) + + # Rooks move orthogonal + elif piece.getType() is ROOK: + if not self.__checkOrtho(rankRange, fileRange): + return (MOVE_RESULT_ILLEGAL, None) + + # Can no longer castle once have move the required rook + if start == 'a' + baseFile: + playerState.canLongCastle = False + elif start == 'h' + baseFile: + playerState.canShortCastle = False + moves.append((piece, start, end)) + + # Bishops move diagonal + elif piece.getType() is BISHOP: + if not self.__checkDiag(rankRange, fileRange): + return (MOVE_RESULT_ILLEGAL, None) + moves.append((piece, start, end)) + + # Knights can move through other pieces + elif piece.getType() is KNIGHT: + if (len(rankRange) - 1) * (len(fileRange) - 1) != 2: + return (MOVE_RESULT_ILLEGAL, None) + moves.append((piece, start, end)) + + # On base rank pawns move on or two squares forwards. + # Pawns take other pieces diagonally (1 square). + # Pawns can take other pawns moving two ranks using 'en passant'. + # Pawns are promoted on reaching the other side of the board. + elif piece.getType() is PAWN: + # Pawns must move forwards + if colour is WHITE: + if end[1] < start[1]: + return (MOVE_RESULT_ILLEGAL, None) + elif start[1] < end[1]: + return (MOVE_RESULT_ILLEGAL, None) + + # Calculate the files that pawns start on and move over on marches + if baseFile == '1': + pawnFile = '2' + marchFile = '3' + farFile = '8' + else: + pawnFile = '7' + marchFile = '6' + farFile = '1' + + # Moving one square forwards with nothing in the way + if len(rankRange) == 1 and len(fileRange) == 2 and victim is None: + pass + + # Moving two squares forward from start rank (march) + elif len(rankRange) == 1 and start[1] == pawnFile and len(fileRange) == 3 and victim is None: + # If two steps check nothing inbetween + if not self.__checkOrtho(rankRange, fileRange): + return (MOVE_RESULT_ILLEGAL, None) + + # The square we moved over can be attacked by en-passant + enPassantSquare = start[0] + marchFile + + # Moving diagonally forwards to take another piece + elif len(rankRange) == 2 and len(fileRange) == 2: + # We either need a victim or be attacking the en-passant square + if victim is None: + if end != self.enPassantSquare: + return (MOVE_RESULT_ILLEGAL, None) + + # Kill the pawn that moved + moves.append((self.lastMove[0], self.lastMove[2], None)) + + else: + return (MOVE_RESULT_ILLEGAL, None) + + # Promote pawns when they hit the far rank + if end[1] == farFile: + # Delete the current piece and create a new piece + moves.append((piece, start, None)) + moves.append((ChessPiece(piece.getColour(), promotionType), None, end)) + else: + moves.append((piece, start, end)) + + # Unknown piece + else: + assert(False) + + # Store this move + oldLastMove = self.lastMove + self.lastMove = (piece, start, end) + oldEnPassantSquare = self.enPassantSquare + self.enPassantSquare = enPassantSquare + + # Delete a victim + if victim is not None: + moves.append((victim, end, None)) + + # Move the pieces: + + # Remove the moving pieces from the board + for (p, s, e) in moves: + if s is not None: + self.squares.pop(s) + + # Put pieces in their new locations + for (p, s, e) in moves: + if e is not None: + self.squares[e] = p + + # Test for check and checkmate + result = MOVE_RESULT_ALLOWED + if testCheck: + # Cannot move into check, if would be then undo move + if self.inCheck(piece.getColour()): + applyMove = False + result = MOVE_RESULT_ILLEGAL + # Test if the oponent is in check + else: + if self.inCheck(enemyColour): + if testCheckMate and self.inCheckMate(enemyColour): + result = MOVE_RESULT_OPPONENT_CHECKMATE + else: + result = MOVE_RESULT_OPPONENT_CHECK + + # Undo the moves if only a test + if applyMove is False: + # Empty any squares moved into + for (p, s, e) in moves: + if e is not None: + self.squares.pop(e) + + # Put pieces back into their original locatons + for (p, s, e) in moves: + if s is not None: + self.squares[s] = p + + # Undo player state + if piece.getColour() == WHITE: + self.whiteState = originalPlayerState + else: + self.blackState = originalPlayerState + + # Undo stored move and en-passant location + self.lastMove = oldLastMove + self.enPassantSquare = oldEnPassantSquare + + else: + # Remember the casualties + if victim is not None: + self.casualties.append(victim) + + return (result, moves) + + def __str__(self): + """Covert the board state to a string""" + out = '' + blackSquare = False + for file in '87654321': + out += ' +---+---+---+---+---+---+---+---+\n' + out += ' ' + file + ' |' + blackSquare = not blackSquare + + for rank in 'abcdefgh': + blackSquare = not blackSquare + try: + piece = self.squares[rank + file] + except: + piece = None + if piece is None: + # Checkerboard + if blackSquare: + out += ' . ' + else: + out += ' ' + else: + s = piece.getType() + if piece.getColour() is WHITE: + s = '-' + s + '-' + elif piece.getColour() is BLACK: + s = '<' + s + '>' + else: + assert(False) + out += s + + out += '|' + + out += '\n' + + out += " +---+---+---+---+---+---+---+---+\n" + out += " a b c d e f g h" + + return out + +class ChessBoard: + """An object representing a chess board. + + This class contains a chess board and all its previous states. + """ + # Pieces on the chess board + __pieces = None + + # A list of board states + __boardStates = None + + # Flag to stop methods being called from inside a callback. + __inCallback = True + + def __init__(self): + """Constructor for a chess board""" + self.__pieces = [] + self.__boardStates = [] + self.__resetBoard() + + def onPieceMoved(self, piece, start, end): + """Called when a piece is moved on the chess board. + + 'piece' is the piece being moved. + 'start' is the start location of the piece (tuple (file,rank) or None if the piece is being created. + 'end' is the end location of the piece (tuple (file,rank) or None if the piece is being created. + """ + pass + + # Public methods + + def getPiece(self, location, moveNumber = -1): + """Get the piece at a given location. + + 'location' is the board location to check in LAN format (string). + 'moveNumber' is the move to get the pieces from (integer). + + Return the piece (ChessPiece) at this location or None if there is no piece there. + Raises an IndexError exception if moveNumber is invalid. + """ + return self.__boardStates[moveNumber].getPiece(location) + + def getAlivePieces(self, moveNumber = -1): + """Get the alive pieces on the board. + + 'moveNumber' is the move to get the pieces from (integer). + + Returns a dictionary of the alive pieces (ChessPiece) keyed by location. + Raises an IndexError exception if moveNumber is invalid. + """ + state = self.__boardStates[moveNumber] + return state.squares.copy() + + def getDeadPieces(self, moveNumber = -1): + """Get the dead pieces from the game. + + 'moveNumber' is the move to get the pieces from (integer). + + Returns a list of the pieces (ChessPiece) in the order they were killed. + Raises an IndexError exception if moveNumber is invalid. + """ + state = self.__boardStates[moveNumber] + return state.casualties[:] + + def testMove(self, colour, start, end, promotionType = QUEEN, allowSuicide = False): + """Test if a move is allowed. + + 'colour' is the colour of the player moving. + 'start' is a the location to move from in LAN format (string). + 'end' is a the location to move to in LAN format (string). + 'allowSuicide' if True means a move is considered valid even + if it would put the moving player in check. This is + provided for SAN move calculation. + + Return the move result (MOVE_RESULT_*) + """ + assert(self.__inCallback is False) + + state = ChessBoardState(self.__boardStates[-1]) + (result, moves) = state.movePiece(colour, start, end, promotionType = promotionType, allowSuicide = allowSuicide, applyMove = False) + return result + + def movePiece(self, colour, start, end, promotionType = QUEEN): + """Move a piece. + + 'colour' is the colour of the player moving. + 'start' is a the location to move from in LAN format (string). + 'end' is a the location to move to in LAN format (string). + + Return the result of the move (MOVE_RESULT_*). + """ + assert(self.__inCallback is False) + + state = ChessBoardState(self.__boardStates[-1]) + (result, moves) = state.movePiece(colour, start, end, promotionType = promotionType) + if result is MOVE_RESULT_ILLEGAL: + return result + + # Notify the child class of the moves + for (piece, start, end) in moves: + self.__onPieceMoved(piece, start, end) + + # Push the board state + self.__boardStates.append(state) + return result[0] + + def __str__(self): + """Returns a representation of the current board state""" + return str(self.__boardStates[-1]) + + # Private methods + + def __onPieceMoved(self, piece, start, end): + """ + """ + self.__inCallback = True + self.onPieceMoved(piece, start, end) + self.__inCallback = False + + def __addPiece(self, state, colour, pieceType, location): + """Add a piece into the board. + + 'state' is the board state to add the piece into. + 'colour' is the colour of the piece. + 'pieceType' is the type of piece to add. + 'location' is the start location of the piece in LAN format (string). + """ + # Create the piece + piece = ChessPiece(colour, pieceType) + self.__pieces.append(piece) + + # Put the piece in it's initial location + assert(state.squares.has_key(location) is False) + assert(type(location) == str) + state.squares[location] = piece + + # Notify a child class the piece creation + self.__onPieceMoved(piece, None, location) + + def __resetBoard(self): + """Set up the chess board. + + Any exisiting states are deleted. + The user will be notified of the piece deletions. + """ + # Delete any existing pieces + for piece in self.__pieces: + self.__onPieceMoved(piece, piece.getLocation(), None) # FIXME: getLocation() not defined + self.__pieces = [] + + # Make the board + initialState = ChessBoardState() + self.__boardStates = [initialState] + + # Populate the board + secondRank = [('a', ROOK), ('b', KNIGHT), ('c', BISHOP), ('d', QUEEN), + ('e', KING), ('f', BISHOP), ('g', KNIGHT), ('h', ROOK)] + for (rank, piece) in secondRank: + # Add a second rank and pawn for each piece + self.__addPiece(initialState, WHITE, piece, rank + '1') + self.__addPiece(initialState, WHITE, PAWN, rank + '2') + self.__addPiece(initialState, BLACK, piece, rank + '8') + self.__addPiece(initialState, BLACK, PAWN, rank + '7') + +if __name__ == '__main__': + p = ChessPiece(WHITE, QUEEN) + print p + print repr(p) + + def test_moves(name, colour, start, whitePieces, blackPieces, validResults): + print name + ':' + board = {} + for coord, piece in whitePieces.iteritems(): + board[coord] = ChessPiece(WHITE, piece) + for coord, piece in blackPieces.iteritems(): + board[coord] = ChessPiece(BLACK, piece) + s = ChessBoardState(board) + resultMatrix = {} + for rank in 'abcdefgh': + for file in '12345678': + end = rank + file + try: + expected = validResults[end] + except: + expected = MOVE_RESULT_ILLEGAL + x = ChessBoardState(s) + (result, moves) = x.movePiece(colour, start, end) + resultMatrix[end] = result + if result != expected: + print 'Unexpected result: ' + str(start) + '-' + str(end) + ' is a ' + str(result) + ', should be ' + str(expected) + + out = '' + for file in '87654321': + out += ' +---+---+---+---+---+---+---+---+\n' + out += ' ' + file + ' |' + + for rank in 'abcdefgh': + coord = rank + file + try: + result = resultMatrix[coord] + except: + result = None + + if result is MOVE_RESULT_ILLEGAL: + p = 'X' + elif result is MOVE_RESULT_OPPONENT_CHECK: + p = '+' + elif result is MOVE_RESULT_OPPONENT_CHECKMATE: + p = '#' + else: + p = ' ' + + piece = s.getPiece(rank + file) + if piece is not None: + p = piece.getType() + + piece = s.getPiece(rank + file) + + if piece is None: + box = ' ' + p + ' ' + else: + if piece.getColour() is BLACK: + box = '=' + p + '=' + elif piece.getColour() is WHITE: + box = '-' + p + '-' + + out += box + '|' + + out += '\n' + + out += " +---+---+---+---+---+---+---+---+\n" + out += " a b c d e f g h\n" + print out + + c = ChessBoard() + + result = """ +---+---+---+---+---+---+---+---+ + 8 ||||||||| + +---+---+---+---+---+---+---+---+ + 7 |

|

|

|

|

|

|

|

| + +---+---+---+---+---+---+---+---+ + 6 | | . | | . | | . | | . | + +---+---+---+---+---+---+---+---+ + 5 | . | | . | | . | | . | | + +---+---+---+---+---+---+---+---+ + 4 | | . | | . | | . | | . | + +---+---+---+---+---+---+---+---+ + 3 | . | | . | | . | | . | | + +---+---+---+---+---+---+---+---+ + 2 |-P-|-P-|-P-|-P-|-P-|-P-|-P-|-P-| + +---+---+---+---+---+---+---+---+ + 1 |-R-|-N-|-B-|-Q-|-K-|-B-|-N-|-R-| + +---+---+---+---+---+---+---+---+ + a b c d e f g h""" + + if str(c) != result: + print 'Got:' + print str(c) + + print + print 'Expected:' + print result + print str(c) + + # Test pawn moves + test_moves('Pawn', WHITE, 'e4', {'e4': PAWN}, {}, {'e5': MOVE_RESULT_ALLOWED}) + test_moves('Pawn on base rank', WHITE, 'e2', {'e2': PAWN}, {}, {'e3': MOVE_RESULT_ALLOWED, 'e4': MOVE_RESULT_ALLOWED}) + + # Test rook moves + test_moves('Rook', WHITE, 'e4', {'e4': ROOK}, {}, + {'a4': MOVE_RESULT_ALLOWED, 'b4': MOVE_RESULT_ALLOWED, 'c4': MOVE_RESULT_ALLOWED, + 'd4': MOVE_RESULT_ALLOWED, 'f4': MOVE_RESULT_ALLOWED, 'g4': MOVE_RESULT_ALLOWED, + 'h4': MOVE_RESULT_ALLOWED, 'e1': MOVE_RESULT_ALLOWED, 'e2': MOVE_RESULT_ALLOWED, + 'e3': MOVE_RESULT_ALLOWED, 'e5': MOVE_RESULT_ALLOWED, 'e6': MOVE_RESULT_ALLOWED, + 'e7': MOVE_RESULT_ALLOWED, 'e8': MOVE_RESULT_ALLOWED}) + + # Test knight moves + test_moves('Knight', WHITE, 'e4', {'e4': KNIGHT}, {}, + {'d6': MOVE_RESULT_ALLOWED, 'f6': MOVE_RESULT_ALLOWED, 'g5': MOVE_RESULT_ALLOWED, + 'g3': MOVE_RESULT_ALLOWED, 'f2': MOVE_RESULT_ALLOWED, 'd2': MOVE_RESULT_ALLOWED, + 'c3': MOVE_RESULT_ALLOWED, 'c5': MOVE_RESULT_ALLOWED}) + + # Test bishop moves + test_moves('Bishop', WHITE, 'e4', {'e4': BISHOP}, {}, + {'a8': MOVE_RESULT_ALLOWED, 'b7': MOVE_RESULT_ALLOWED, 'c6': MOVE_RESULT_ALLOWED, + 'd5': MOVE_RESULT_ALLOWED, 'f3': MOVE_RESULT_ALLOWED, 'g2': MOVE_RESULT_ALLOWED, + 'h1': MOVE_RESULT_ALLOWED, 'b1': MOVE_RESULT_ALLOWED, 'c2': MOVE_RESULT_ALLOWED, + 'd3': MOVE_RESULT_ALLOWED, 'f5': MOVE_RESULT_ALLOWED, 'g6': MOVE_RESULT_ALLOWED, + 'h7': MOVE_RESULT_ALLOWED}) + + # Test queen moves + test_moves('Queen', WHITE, 'e4', {'e4': QUEEN}, {}, + {'a8': MOVE_RESULT_ALLOWED, 'b7': MOVE_RESULT_ALLOWED, 'c6': MOVE_RESULT_ALLOWED, + 'd5': MOVE_RESULT_ALLOWED, 'f3': MOVE_RESULT_ALLOWED, 'g2': MOVE_RESULT_ALLOWED, + 'h1': MOVE_RESULT_ALLOWED, 'b1': MOVE_RESULT_ALLOWED, 'c2': MOVE_RESULT_ALLOWED, + 'd3': MOVE_RESULT_ALLOWED, 'f5': MOVE_RESULT_ALLOWED, 'g6': MOVE_RESULT_ALLOWED, + 'h7': MOVE_RESULT_ALLOWED, 'a4': MOVE_RESULT_ALLOWED, 'b4': MOVE_RESULT_ALLOWED, + 'c4': MOVE_RESULT_ALLOWED, 'd4': MOVE_RESULT_ALLOWED, 'f4': MOVE_RESULT_ALLOWED, + 'g4': MOVE_RESULT_ALLOWED, 'h4': MOVE_RESULT_ALLOWED, 'e1': MOVE_RESULT_ALLOWED, + 'e2': MOVE_RESULT_ALLOWED, 'e3': MOVE_RESULT_ALLOWED, 'e5': MOVE_RESULT_ALLOWED, + 'e6': MOVE_RESULT_ALLOWED, 'e7': MOVE_RESULT_ALLOWED, 'e8': MOVE_RESULT_ALLOWED}) + + # Test king moves + test_moves('King', WHITE, 'e4', {'e4': KING}, {}, + {'d5': MOVE_RESULT_ALLOWED, 'e5': MOVE_RESULT_ALLOWED, 'f5': MOVE_RESULT_ALLOWED, + 'd4': MOVE_RESULT_ALLOWED, 'f4': MOVE_RESULT_ALLOWED, 'd3': MOVE_RESULT_ALLOWED, + 'e3': MOVE_RESULT_ALLOWED, 'f3': MOVE_RESULT_ALLOWED}) + + # Test pieces blocking moves + test_moves('Blocking', WHITE, 'd4', + {'d4': QUEEN, 'e4': PAWN, 'd6': KNIGHT, 'd2': ROOK, 'f6': BISHOP, 'e3': BISHOP, + 'b4':PAWN, 'b2': PAWN, 'a7': PAWN}, + {'d8': KNIGHT, 'c4': PAWN}, + {'b6': MOVE_RESULT_ALLOWED, 'c5': MOVE_RESULT_ALLOWED, 'd5': MOVE_RESULT_ALLOWED, + 'e5': MOVE_RESULT_ALLOWED, 'c4': MOVE_RESULT_ALLOWED, 'c3': MOVE_RESULT_ALLOWED, + 'd3': MOVE_RESULT_ALLOWED}) + + # Test moving in/out of check + test_moves('Moving into check', WHITE, 'e4', {'e4': KING}, {'e6': ROOK}, + {'d5': MOVE_RESULT_ALLOWED, 'f5': MOVE_RESULT_ALLOWED, + 'd4': MOVE_RESULT_ALLOWED, 'f4': MOVE_RESULT_ALLOWED, + 'd3': MOVE_RESULT_ALLOWED, 'f3': MOVE_RESULT_ALLOWED}) + test_moves('Held in check', WHITE, 'e4', {'e4': KING}, {'f6': ROOK}, + {'d5': MOVE_RESULT_ALLOWED, 'e5': MOVE_RESULT_ALLOWED, 'd4': MOVE_RESULT_ALLOWED, + 'd3': MOVE_RESULT_ALLOWED, 'e3': MOVE_RESULT_ALLOWED}) + + # Test putting opponent in check + test_moves('Putting opponent in check', WHITE, 'd3', {'d3': BISHOP}, {'d7': KING, 'd6': ROOK}, + {'a6': MOVE_RESULT_ALLOWED, 'b5': MOVE_RESULT_OPPONENT_CHECK, 'c4': MOVE_RESULT_ALLOWED, + 'e2': MOVE_RESULT_ALLOWED, 'f1': MOVE_RESULT_ALLOWED, 'b1': MOVE_RESULT_ALLOWED, + 'c2': MOVE_RESULT_ALLOWED, 'e4': MOVE_RESULT_ALLOWED, 'f5': MOVE_RESULT_OPPONENT_CHECK, + 'g6': MOVE_RESULT_ALLOWED, 'h7': MOVE_RESULT_ALLOWED}) + + # Test putting opponent into checkmate + test_moves('Putting opponent into checkmate', WHITE, 'c1', {'c1': BISHOP, 'g1': ROOK, 'a7': ROOK}, {'h8': KING}, + {'b2': MOVE_RESULT_OPPONENT_CHECKMATE, 'a3': MOVE_RESULT_ALLOWED, + 'd2': MOVE_RESULT_ALLOWED, 'e3': MOVE_RESULT_ALLOWED, 'f4': MOVE_RESULT_ALLOWED, + 'g5': MOVE_RESULT_ALLOWED, 'h6': MOVE_RESULT_ALLOWED}) + #FIXME + + # Test putting own player in check by putting oppononent in check (i.e. can't move) + test_moves('Cannot put opponent in check if we would go into check', + WHITE, 'd3', {'d2': KING, 'd3': BISHOP}, {'d7': KING, 'd6': ROOK}, {}) + + # Test castling + test_moves('Castle1', WHITE, 'e1', {'e1': KING, 'a1': ROOK}, {}, + {'d2': MOVE_RESULT_ALLOWED, 'e2': MOVE_RESULT_ALLOWED, 'f2': MOVE_RESULT_ALLOWED, + 'd1': MOVE_RESULT_ALLOWED, 'f1': MOVE_RESULT_ALLOWED, 'c1': MOVE_RESULT_ALLOWED}) + test_moves('Castle2', BLACK, 'e8', {}, {'e8': KING, 'h8': ROOK}, + {'d7': MOVE_RESULT_ALLOWED, 'e7': MOVE_RESULT_ALLOWED, 'f7': MOVE_RESULT_ALLOWED, + 'd8': MOVE_RESULT_ALLOWED, 'f8': MOVE_RESULT_ALLOWED, 'g8': MOVE_RESULT_ALLOWED}) + + # Test castling while in check + test_moves('Castle in check1', BLACK, 'e8', {'f1': ROOK}, {'e8': KING, 'h8': ROOK}, + {'d7': MOVE_RESULT_ALLOWED, 'e7': MOVE_RESULT_ALLOWED, 'd8': MOVE_RESULT_ALLOWED}) + test_moves('Castle in check2', BLACK, 'e8', {'e1': ROOK}, {'e8': KING, 'h8': ROOK}, + {'d7': MOVE_RESULT_ALLOWED, 'd8': MOVE_RESULT_ALLOWED, + 'f7': MOVE_RESULT_ALLOWED, 'f8': MOVE_RESULT_ALLOWED}) + test_moves('Castle in check3', BLACK, 'e8', {'h1': ROOK}, {'e8': KING, 'h8': ROOK}, + {'d7': MOVE_RESULT_ALLOWED, 'e7': MOVE_RESULT_ALLOWED, 'f7': MOVE_RESULT_ALLOWED, + 'd8': MOVE_RESULT_ALLOWED, 'f8': MOVE_RESULT_ALLOWED, 'g8': MOVE_RESULT_ALLOWED}) + + # Test en-passant + #FIXME + \ No newline at end of file diff --git a/src/lib/chess/lan.py b/src/lib/chess/lan.py new file mode 100644 index 0000000..a6e5a9b --- /dev/null +++ b/src/lib/chess/lan.py @@ -0,0 +1,178 @@ +""" +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +import board + +CHECK = '+' +CHECKMATE = '#' + +# Notation for takes +MOVE = '-' +TAKE = 'x' + +# Castling moves +CASTLE_SHORT = 'O-O' +CASTLE_LONG = 'O-O-O' + +# Characters used to describe pieces +_typeToLAN = {board.PAWN: 'P', + board.KNIGHT: 'N', + board.BISHOP: 'B', + board.ROOK: 'R', + board.QUEEN: 'Q', + board.KING: 'K'} +_lanToType = {} +for (pieceType, character) in _typeToLAN.iteritems(): + _lanToType[character] = pieceType + +class DecodeError(Exception): + """ + """ + pass + +def _checkLocation(location): + """ + """ + if len(location) != 2: + raise DecodeError('Invalid length location') + if location[0] < 'a' or location[0] > 'h': + raise DecodeError('Invalid rank') + if location[1] < '0' or location[1] > '8': + raise DecodeError('Invalid file') + return location + +def decode(colour, move): + """Decode a long algebraic format move. + + 'colour' is the colour of the player making the move (board.WHITE or board.BLACK). + 'move' is the move description (string). + + Returns a tuple containing (start, end, piece, moveType, promotionType, result) + 'start' is the location being moved from (string, e.g. 'a1', 'h8'). + 'end' is the location being moved to (string, e.g. 'a1', 'h8'). + 'piece' is the piece being moved (board.PAWN, board.ROOK, ... or None if not specified). + 'moveType' is a flag to show if this move takes an oppoenent piece (MOVE, TAKE or None if not specified). + 'promotionType' is the piece type to promote to (board.ROOK, board.KNIGHT, ... or None if not specified). + 'check' is the result after the move (CHECK, CHECKMATE or None if not specified). + + Raises DecodeError if the move is unable to be decoded. + """ + pieceType = None + promotionType = None + moveType = None + result = None + + # FIXME: Get 'result' from the end of the move description + if colour is board.WHITE: + baseFile = '1' + else: + baseFile = '8' + if move == CASTLE_SHORT: + return ('e' + baseFile, 'g' + baseFile, None, None, None, None) + elif move == CASTLE_LONG: + return ('e' + baseFile, 'c' + baseFile, None, None, None, None) + + # First character can be the piece types + if len(move) < 1: + raise DecodeError('Too short') + try: + pieceType = _lanToType[move[0]] + except KeyError: + pieceType = None + else: + move = move[1:] + + if len(move) < 2: + raise DecodeError('Too short') + start = _checkLocation(move[:2]) + move = move[2:] + + if len(move) < 1: + raise DecodeError('Too short') + if move[0] == MOVE or move[0] == TAKE: + moveType = move[0] + move = move[1:] + + if len(move) < 2: + raise DecodeError('Too short') + end = _checkLocation(move[:2]) + move = move[2:] + + # Look for promotion type, note this can be in upper or lower case + if len(move) > 0: + if move[0] == '=': + if len(move) < 2: + raise DecodeError('Too short') + try: + promotionType = _lanToType[move[1].upper()] + except KeyError: + raise DecodeError('Unknown promotion type') + move = move[2:] + else: + try: + promotionType = _lanToType[move[0].upper()] + except KeyError: + pass + else: + move = move[1:] + + if len(move) > 0: + if move[0] == CHECK or move[0] == CHECKMATE: + result = move[0] + move = move[1:] + + if len(move) != 0: + raise DecodeError('Extra characters') + + return (start, end, pieceType, moveType, promotionType, result) + +def encode(colour, start, end, piece = None, moveType = None, promotionType = None, result = None): + """Encode a long algebraic format move. + + 'start' is the location being moved from (string, e.g. 'a1', 'h8'). + 'end' is the location being moved to (string, e.g. 'a1', 'h8'). + 'piece' is the piece being moved (board.PAWN, board.ROOK, ... or None if not specified). + 'moveType' is a flag to show if this move takes an oppoenent piece (MOVE, TAKE or None if not specified). + 'promotionType' is the piece type to promote to (board.ROOK, board.KNIGHT, ... or None if not specified). + 'check' is the result after the move (CHECK, CHECKMATE or None if not specified). + + Returns a string describing this move. + """ + try: + _checkLocation(start) + _checkLocation(end) + except DecodeError: + raise TypeError("Invalid values for 'start' and 'end'") + + string = '' + + # Report the piece being moved + if piece is not None: + string += _typeToLAN[piece] + + # Report the source location + string += start + + # Report if this is a move or a take + if moveType != None: + string += moveType + + # Report the target location + string += end + + # Report the promotion type + # FIXME: Only report if a pawn promotion + if promotionType != None: + if False: # FIXME: What to name this flag? + string += '=' + string += _typeToLAN[promotionType].lower() + + # Report the check result + if result is not None: + string += result + + return string diff --git a/src/lib/chess/pgn.py b/src/lib/chess/pgn.py new file mode 100644 index 0000000..944b700 --- /dev/null +++ b/src/lib/chess/pgn.py @@ -0,0 +1,686 @@ +""" +Implement a PGN reader/writer. + +See http://www.chessclub.com/help/PGN-spec +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +""" +; Example PGN file + +[Event "F/S Return Match"] +[Site "Belgrade, Serbia JUG"] +[Date "1992.11.04"] +[Round "29"] +[White "Fischer, Robert J."] +[Black "Spassky, Boris V."] +[Result "1/2-1/2"] + +1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 +O-O 9. h3 Nb8 10. d4 Nbd7 11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. +Nb1 h6 16. Bh4 c5 17. dxe5 Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. +Nc4 Nxc4 22. Bxc4 Nb6 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 +27. Qe3 Qg5 28. Qxg5 hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. +f3 Bc8 34. Kf2 Bf5 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 +40. Rd6 Kc5 41. Ra6 Nf2 42. g4 Bd3 43. Re6 1/2-1/2 +""" + +# Comments are bounded by ';' to '\n' or '{' to '}' +# Lines starting with '%' are ignored and are used as an extension mechanism +# Strings are bounded by '"' and '"' and quotes inside the strings are escaped with '\"' + +class Error(Exception): + """PGN exception class""" + + __errorType = 'Unknown' + + def __init__(self, error = None): + self.__errorType = error + Exception.__init__(self) + + def __str__(self): + return repr(self.__errorType) + +class PGNToken: + """ + """ + + # Token types + LINE_COMMENT = 'Line comment' + COMMENT = 'Comment' + PERIOD = 'Period' + TAG_START = 'Tag start' + TAG_END = 'Tag end' + STRING = 'String' + SYMBOL = 'Symbol' + RAV_START = 'RAV start' + RAV_END = 'RAV end' + XML_START = 'XML start' + XML_END = 'XML end' + NAG = 'NAG' + type = None + + SYMBOL_START_CHARACTERS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + '*' + SYMBOL_CONTINUATION_CHARACTERS = SYMBOL_START_CHARACTERS + '_+#=:-' + '/' # Not in spec but required from game draw and imcomplete + NAG_CONTINUATION_CHARACTERS = '0123456789' + + GAME_TERMINATE_INCOMPLETE = '*' + GAME_TERMINATE_WHITE_WIN = '1-0' + GAME_TERMINATE_BLACK_WIN = '0-1' + GAME_TERMINATE_DRAW = '1/2-1/2' + + data = None + + lineNumber = -1 + characterNumber = -1 + + def __init__(self, lineNumber, characterNumber, tokenType, data = None): + """ + """ + self.type = tokenType + self.data = data + self.lineNumber = lineNumber + self.characterNumber = characterNumber + + def __str__(self): + string = self.type + if self.data is not None: + string += ': ' + self.data + return string + +class PGNParser: + """ + """ + + __inComment = False + __comment = '' + __startOffset = -1 + + def __extractPGNString(self, data): + #"""Extract a PGN string. + + #'data' is the data to extract the string from (string). It must start with a quote character '"'. + + #Return a tuple containing the first PGN string and the number of characters of data it required. + #e.g. '"Mike \"Dog\" Smith"' -> ('Mike "Dog" Smith', 20). + #If no string is found a Error is raised. + #""" + if data[0] != '"': + raise Error('PGN string does not start with "') + + offset = 1 + escaped = False + while True: + try: + c = data[offset] + escaped = (c == '\\') + if c == '"' and escaped is False: + pgnString = data[1:offset] + pgnString.replace('\\"', '"') + return (pgnString, offset + 1) + except IndexError: + raise Error('Unterminated PGN string') + offset += 1 + + def parseLine(self, line, lineNumber): + """TODO + + Return an array of tokens extracted from the line. + """ + tokens = [] + inSymbol = False + inNAG = False + symbol = '' + nag = '' + offset = 0 + while offset < len(line): + c = line[offset] + + if self.__inComment is True: + if c == '}': + tokens.append(PGNToken(lineNumber, self.__startOffset, PGNToken.LINE_COMMENT, self.__comment)) + self.__inComment = False + else: + self.__comment += c + offset += 1 + continue + + if inSymbol: + if PGNToken.SYMBOL_CONTINUATION_CHARACTERS.find(c) >= 0: + symbol += c + offset += 1 + continue + else: + tokens.append(PGNToken(lineNumber, self.__startOffset, PGNToken.SYMBOL, symbol)) + inSymbol = False + elif inNAG: + if PGNToken.NAG_CONTINUATION_CHARACTERS.find(c) >= 0: + symbol += c + offset += 1 + continue + else: + # FIXME: Should be at least one character + tokens.append(PGNToken(lineNumber, self.__startOffset, PGNToken.NAG, nag)) + inNAG = False + + if c.isspace(): + pass + elif c == ';': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.LINE_COMMENT, line[offset:])) + return tokens + elif c == '{': + self.__comment = '' + self.__inComment = True + self.__startOffset = offset + elif c == '.': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.PERIOD)) + elif c == '[': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.TAG_START)) + elif c == ']': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.TAG_END)) + elif c == '"': + (string, newOffset) = self.__extractPGNString(line[offset:]) + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.STRING, string)) + offset += newOffset + continue + elif c == '(': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.RAV_START)) + elif c == ')': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.RAV_END)) + elif c == '<': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.XML_START)) + elif c == '>': + tokens.append(PGNToken(lineNumber, offset+1, PGNToken.XML_END)) + elif c == '$': + inNAG = True + self.__startOffset = offset + elif PGNToken.SYMBOL_START_CHARACTERS.find(c) >= 0: + symbol = c + inSymbol = True + self.__startOffset = offset + else: + raise Error('Unknown character ' + repr(c)) + + offset += 1 + + # Complete any symbols or NAGs + if inSymbol: + tokens.append(PGNToken(lineNumber, self.__startOffset+1, PGNToken.SYMBOL, symbol)) + if inNAG: + # FIXME: Must be 1 or more char.. + tokens.append(PGNToken(lineNumber, self.__startOffset+1, PGNToken.NAG, nag)) + + return tokens + + def endParse(self): + pass + +class PGNGameParser: + """ + """ + + STATE_IDLE = 'IDLE' + STATE_TAG_NAME = 'TAG_NAME' + STATE_TAG_VALUE = 'TAG_VALUE' + STATE_TAG_END = 'TAG_END' + STATE_MOVETEXT = 'MOVETEXT' + STATE_RAV = 'RAV' + STATE_XML = 'XML' + __state = STATE_IDLE + + # The game being assembled + __game = None + + # The tag being assembled + __tagName = None + __tagValue = None + + # The move number being decoded + __expectedMoveNumber = 0 + __lastTokenIsMoveNumber = False + + # The last white move + __whiteMove = None + + # The Recursive Annotation Variation (RAV) stack + __ravDepth = 0 + + def __parseTokenMovetext(self, token): + """ + """ + if token.type is PGNToken.RAV_START: + self.__ravDepth += 1 + # FIXME: Check for RAV errors + return + + elif token.type is PGNToken.RAV_END: + self.__ravDepth -= 1 + # FIXME: Check for RAV errors + return + + # Ignore tokens inside RAV + if self.__ravDepth != 0: + return + + if token.type is PGNToken.PERIOD: + if self.__lastTokenIsMoveNumber is False: + raise Error('Unexpected period on line ' + str(token.lineNumber) + ':' + str(token.characterNumber)) + + elif token.type is PGNToken.SYMBOL: + # See if this is a game terminate + if token.data == PGNToken.GAME_TERMINATE_INCOMPLETE or \ + token.data == PGNToken.GAME_TERMINATE_WHITE_WIN or \ + token.data == PGNToken.GAME_TERMINATE_BLACK_WIN or \ + token.data == PGNToken.GAME_TERMINATE_DRAW: + # Complete any half moves + if self.__whiteMove is not None: + self.__game.addMove(self.__whiteMove, None) + + game = self.__game + self.__game = None + + return game + + # Otherwise it is a move number or a move + else: + # See if this is a move number or a SAN move + try: + moveNumber = int(token.data) + self.__lastTokenIsMoveNumber = True + if moveNumber != self.__expectedMoveNumber: + raise Error('Expected move number ' + str(self.__expectedMoveNumber) + ', got ' + str(moveNumber) + ' on line ' + str(token.lineNumber) + ':' + str(token.characterNumber)) + except ValueError: + self.__lastTokenIsMoveNumber = False + if self.__whiteMove is None: + self.__whiteMove = token.data + else: + self.__game.addMove(self.__whiteMove, token.data) + self.__whiteMove = None + self.__expectedMoveNumber += 1 + + elif token.type is PGNToken.NAG: + pass + + else: + raise Error('Unknown token ' + token.type + ' in movetext on line ' + str(token.lineNumber) + ':' + str(token.characterNumber)) + + def parseToken(self, token): + """TODO + + Return a game object if a game is complete otherwise None. + """ + + # Ignore all comments at any time + if token.type is PGNToken.LINE_COMMENT or token.type is PGNToken.COMMENT: + return None + + if self.__state is self.STATE_IDLE: + if self.__game is None: + self.__game = PGNGame() + + if token.type is PGNToken.TAG_START: + self.__state = self.STATE_TAG_NAME + return + + elif token.type is PGNToken.SYMBOL: + self.__expectedMoveNumber = 1 + self.__whiteMove = None + self.__lastTokenIsMoveNumber = False + self.__ravDepth = 0 + self.__state = self.STATE_MOVETEXT + + else: + raise Error('Unexpected token ' + token.type + ' on line ' + str(token.lineNumber) + ':' + str(token.characterNumber)) + + if self.__state is self.STATE_TAG_NAME: + if token.type is PGNToken.SYMBOL: + self.__tagName = token.data + self.__state = self.STATE_TAG_VALUE + else: + raise Error() + + elif self.__state is self.STATE_TAG_VALUE: + if token.type is PGNToken.STRING: + self.__tagValue = token.data + self.__state = self.STATE_TAG_END + else: + raise Error() + + elif self.__state is self.STATE_TAG_END: + if token.type is PGNToken.TAG_END: + self.__game.setTag(self.__tagName, self.__tagValue) + self.__state = self.STATE_IDLE + else: + raise Error() + + elif self.__state is self.STATE_MOVETEXT: + game = self.__parseTokenMovetext(token) + if game is not None: + self.__state = self.STATE_IDLE + return game + + def complete(self): + """ + """ + pass + # Raise an error if there was a partial game + #raise Error() + +class PGNGame: + """ + """ + + """The required tags in a PGN file (the seven tag roster, STR)""" + PGN_TAG_EVENT = 'Event' + PGN_TAG_SITE = 'Site' + PGN_TAG_DATE = 'Date' + PGN_TAG_ROUND = 'Round' + PGN_TAG_WHITE = 'White' + PGN_TAG_BLACK = 'Black' + PGN_TAG_RESULT = 'Result' + + # The seven tag roster in the required order (REFERENCE) + __strTags = [PGN_TAG_EVENT, PGN_TAG_SITE, PGN_TAG_DATE, PGN_TAG_ROUND, PGN_TAG_WHITE, PGN_TAG_BLACK, PGN_TAG_RESULT] + + # The tags in this game + __tagsByName = None + + __moves = None + + def __init__(self): + # Set the default STR tags + self.__tagsByName = {} + self.setTag(self.PGN_TAG_EVENT, '?') + self.setTag(self.PGN_TAG_SITE, '?') + self.setTag(self.PGN_TAG_DATE, '????.??.??') + self.setTag(self.PGN_TAG_ROUND, '?') + self.setTag(self.PGN_TAG_WHITE, '?') + self.setTag(self.PGN_TAG_BLACK, '?') + self.setTag(self.PGN_TAG_RESULT, '*') + + self.__moves = [] + + def getLines(self): + + lines = [] + + # Get the names of the non STR tags + otherTags = list(set(self.__tagsByName).difference(self.__strTags)) + + # Write seven tag roster and the additional tags + for name in self.__strTags + otherTags: + value = self.__tagsByName[name] + lines.append('['+ name + ' ' + self.__makePGNString(value) + ']') + + lines.append('') + + # Insert numbers in-between moves + tokens = [] + moveNumber = 1 + for m in self.__moves: + tokens.append('%i.' % moveNumber) + moveNumber += 1 + tokens.append(m[0]) + if m[1] is not None: + tokens.append(m[1]) + + # Add result token to the end + tokens.append(self.__tagsByName[self.PGN_TAG_RESULT]) + + # Print moves keeping the line length to less than 256 characters (PGN requirement) + line = '' + for t in tokens: + if line == '': + x = t + else: + x = ' ' + t + if len(line) + len(x) >= 80: #>= 256: + lines.append(line) + line = t + else: + line += x + + lines.append(line) + return lines + + def setTag(self, name, value): + """Set a PGN tag. + + 'name' is the name of the tag to set (string). + 'value' is the value to set the tag to (string) or None to delete the tag. + + Tag names cannot contain whitespace. + + Deleting a tag that does not exist has no effect. + + Deleting a STR tag or setting one to an invalid value will raise an Error exception. + """ + if self.__isValidTagName(name) is False: + raise Error(str(name) + ' is an invalid tag name') + + # If no value delete + if value is None: + # If is a STR tag throw an exception + if self.__strTags.has_key(name): + raise Error(name + ' is a PGN STR tag and cannot be deleted') + + # Delete the tag + try: + self.__strTags.pop(name) + except KeyError: + pass + + # Otherwise set the tag to the new value + else: + # FIXME: Validate if it is a STR tag + + self.__tagsByName[name] = value + + def getTag(self, name): + """Get a PGN tag. + + 'name' is the name of the tag to get (string). + + Return the value of the tag (string) or None if the tag does not exist. + """ + try: + return self.__tagsByName[name] + except KeyError: + return None + + def addMove(self, whiteMove, blackMove): + self.__moves.append((whiteMove, blackMove)) + + def getWhiteMove(self, moveNumber): + return self.__moves[moveNumber - 1][0] + + def getBlackMove(self, moveNumber): + return self.__moves[moveNumber - 1][1] + + def getMoves(self): + moves = [] + for m in self.__moves: + moves.append(m[0]) + if m[1] is not None: + moves.append(m[1]) + return moves + + def __str__(self): + + string = '' + for tag, value in self.__tagsByName.iteritems(): + string += tag + ' = ' + value + '\n' + string += '\n' + + number = 1 + for move in self.__moves: + string += '%3i. ' % number + str(move[0]) + ' ' + str(move[1]) + '\n' + number += 1 + + return string + + # Private methods + def __makePGNString(self, string): + """Make a PGN string. + + 'string' is the string to convert to a PGN string (string). + + All characters are valid and quotes are escaped with '\"'. + + Return the string surrounded with quotes. e.g. 'Mike "Dog" Smith' -> '"Mike \"Dog\" Smith"' + """ + pgnString = string + pgnString.replace('"', '\\"') + return '"' + pgnString + '"' + + def __isValidTagName(self, name): + """Valid a PGN tag name. + + 'name' is the tag name to validate (string). + + Tags can only contain the characters, a-Z A-Z and _. + + Return True if this is a valid tag name otherwise return False. + """ + if name is None or len(name) == 0: + return False + + validCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_' + for c in name: + if validCharacters.find(c) < 0: + return False + return True + +class PGN: + """ + """ + + __games = None + + def __init__(self, fileName = None, maxGames = None): + """Create a PGN reader/writer. + + 'fileName' is the file to load the PGN from or None to generate an empty PGN file. + 'maxGames' is the maximum number of games to load from the file or None + to load the whole file. (int, Only applicable if a filename is supplied). + """ + self.__games = [] + + if fileName is not None: + self.__load(fileName, maxGames) + + def addGame(self): + """Add a new game to the PGN file. + + Returns the PGNGame instance to modify""" + game = PGNGame() + self.__games.append(game) + return game + + def getGame(self, index): + """Get a game from the PGN file. + + 'index' is the game index to get (integer, 0-N). + + Return this PGN game or raise an IndexError if no game with this index. + """ + return self.__games[index] + + def save(self, fileName): + """Save the PGN file. + + 'fileName' is the name of the file to save to. + """ + try: + f = file(fileName, 'w') + except IOError, e: + raise Error('Unable to write to PGN file: ' + str(e)) + # FIXME: Set the newline characters to the correct type? + + # Sign it from glChess + f.write('; PGN saved game generated by glChess\n') + f.write('; http://glchess.sourceforge.net\n') + + for game in self.__games: + f.write('\n') + for line in game.getLines(): + f.write(line + '\n') + + f.close() + + def __getitem__(self, index): + return self.__games[index] + + def __getslice__(self, start, end): + return self.__games[start:end] + + # Private methods + + def __load(self, fileName, maxGames = None): + """ + """ + # Convert the file into PGN tokens + try: + f = file(fileName, 'r') + except IOError, e: + raise Error('Unable to open PGN file: ' + str(e)) + p = PGNParser() + gp = PGNGameParser() + lineNumber = 1 + gameCount = 0 + while True: + # Read a line from the file + line = f.readline() + if line == '': + break + + # Parse the line into tokens + tokens = p.parseLine(line, lineNumber) + + # Decode the tokens into PGN games + for token in tokens: + game = gp.parseToken(token) + + # Store this game and stop if only required to parse a certain number + if game is not None: + self.__games.append(game) + gameCount += 1 + + if maxGames is not None and gameCount >= maxGames: + break + + # YUCK... FIXME + if maxGames is not None and gameCount >= maxGames: + break + + lineNumber += 1 + + # Must be at least one game in the PGN file + if gameCount == 0: + raise Error('Empty PGN file') + + # Tidy up + gp.complete() + p.endParse() + f.close() + +if __name__ == '__main__': + def test(fileName, maxGames = None): + p = PGN(fileName, maxGames) + number = 1 + games = p[:] + for game in games: + print 'Game ' + str(number) + print game + print + number += 1 + + test('example.pgn') + test('rav.pgn') + test('wolga-benko.pgn', 3) + + p = PGN('example.pgn') + p.save('out.pgn') diff --git a/src/lib/chess/san.py b/src/lib/chess/san.py new file mode 100644 index 0000000..16363ee --- /dev/null +++ b/src/lib/chess/san.py @@ -0,0 +1,456 @@ +""" +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +__all__ = ['SANConverter'] + +# Examples of SAN moves: +# +# f4 (pawn move to f4) +# fxg3 (pawn on file f takes opponent on g3) +# Qh5 (queen moves to h5) +# Qh5+ (queen moves to h5 and puts opponent into check) +# Ned4 (knight on file e moves to d4) +# gxh8=Q# (pawn on g7 takes opponent in h8 promotes to queen and puts oponent into checkmate (smooth!)) + +# Notation for takes +SAN_TAKE = 'x' + +# Castling moves +SAN_CASTLE_SHORT = 'O-O' +SAN_CASTLE_LONG = 'O-O-O' + +RANKS = 'abcdefgh' +FILES = '12345678' + +# Suffixes +SAN_PROMOTE = '=' + +class Error(Exception): + """ + """ + + # Properties of the error + __move = '' + __description = '' + + def __init__(self, move, description): + """Constructor for a SAN exception. + + 'move' is the SAN move that generated the exception (string). + 'description' is the description of the exception that occured (string). + """ + self.__move = str(move) + self.__description = str(description) + Exception.__init__(self) + + def __str__(self): + """Convert the SAN exception to a string""" + return 'Error parsing SAN move ' + repr(self.__move) + ': ' + self.__description + +class SANConverter: + """ + + Define file and rank + """ + + # Piece colours + WHITE = 'White' + BLACK = 'Black' + + # SAN piece types + PAWN = 'P' + KNIGHT = 'N' + BISHOP = 'B' + ROOK = 'R' + QUEEN = 'Q' + KING = 'K' + __pieceTypes = PAWN + KNIGHT + BISHOP + ROOK + QUEEN + KING + + # Valid promotion types + __promotionTypes = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN] + + # Move results + CHECK = '+' + CHECKMATE = '#' + + def __init__(self): + """Constructor""" + pass + + # Methods to extend + + def getPiece(self, location): + """Get a piece from the chess board. + + 'location' is the location to get the piece from (string, e.g. 'a1', h8'). + + Return a tuple containing (colour, type) or None if no piece at this location. + """ + return None + + def testMove(self, colour, start, end, promotionType, allowSuicide = False): + """Test if a move is valid. + + 'colour' is the colour of the player making the move (self.WHITE or self.BLACK). + 'start' is the board location to move from (string, e.g. 'a1', 'h8'). + 'end' is the board location to move to (string, e.g. 'a1', 'h8'). + 'promotionType' is the piece type to promote to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]). + 'allowSuicide' is a flag to show if the move should be disallowed (False) or + allowed (True) if it would put the moving player into check. + + Return False if the move is dissallowed or + self.CHECK if the move puts the opponent into check or + self.CHECKMATE if the move puts the opponent into checkmate or + True if the move is allowed and does not put the opponent into check. + """ + pass + + def decodeSAN(self, colour, san): + """Decode a SAN move. + + 'colour' is the colour of the player making the move (self.WHITE or self.BLACK). + 'san' is the SAN description of the move (string). + + Returns the move this SAN describes in the form (start, end, promotionType). + 'start' is the square to move from (string, e.g. 'a1', 'h8'). + 'end' is the square to move to (string, e.g. 'a1', 'h8'). + 'promotionType' is the piece to promote to (self.[KNIGHT|BISHOP|ROOK|QUEEN]). + If the move is invalid then an Error expection is raised. + """ + copy = san[:] + + # Look for check hints + expectedResult = True + if copy[-1] == self.CHECK or copy[-1] == self.CHECKMATE: + expectedResult = copy[-1] + copy = copy[:-1] + + # Extract promotions + promotionType = self.QUEEN + if copy[-2] == SAN_PROMOTE: + promotionType = copy[-1] + copy = copy[:-2] + try: + self.__promotionTypes.index(promotionType) + except ValueError: + raise Error(san, 'Invalid promotion type ' + promotionType) + + # Check for castling moves + if colour is self.WHITE: + baseFile = '1' + else: + baseFile = '8' + # FIXME: Update moveResult and compare against expectedResult + if copy == SAN_CASTLE_SHORT: + return ('e' + baseFile, 'g' + baseFile, expectedResult, promotionType) + elif copy == SAN_CASTLE_LONG: + return ('e' + baseFile, 'c' + baseFile, expectedResult, promotionType) + + # Get the destination (the last two characters before the suffix) + end = copy[-2:] + copy = copy[:-2] + if RANKS.find(end[0]) < 0 or FILES.find(end[1]) < 0: + raise Error(san, 'Invalid destination: ' + end) + + # Check if is a take move (use try in case there are no more characters) + isTake = False + try: + if copy[-1] == SAN_TAKE: + isTake = True + copy = copy[:-1] + except: + pass + + # The first character is the piece type (or pawn if not specified) + pieceType = self.PAWN + if len(copy) > 0: + if self.__pieceTypes.find(copy[0]) >= 0: + pieceType = copy[0] + copy = copy[1:] + + # Get the rank of the source piece (if supplied) + rank = None + if len(copy) > 0: + if RANKS.find(copy[0]) >= 0: + rank = copy[0] + copy = copy[1:] + + # Get the file of the source piece (if supplied) + file = None + if len(copy) > 0: + if FILES.find(copy[0]) >= 0: + file = copy[0] + copy = copy[1:] + + # There should be no more characters + if len(copy) != 0: + raise Error(san, 'Unexpected extra characters: ' + copy) + + # If have both rank and file for source then we have the move completely defined + moveResult = None + move = None + if rank is not None and file is not None: + start = rank + file + moveResult = self.testMove(colour, start, end, promotionType = promotionType) + move = (start, end) + else: + # Try and find a piece that matches the source one + if file is None: + fileRange = FILES + else: + fileRange = file + if rank is None: + rankRange = RANKS + else: + rankRange = rank + + for file in fileRange: + for rank in rankRange: + start = rank + file + + # Only test our pieces + piece = self.getPiece(start) + if piece is None: + continue + if piece[0] != colour or piece[1] != pieceType: + continue + + # If move is valid this is a possible move + # FIXME: Check the crafty case in reverse (i.e. suicidal moves) + result = self.testMove(colour, start, end, promotionType = promotionType) + if result is False: + continue + + # Multiple matches + if moveResult is not None: + raise Error(san, 'Move is ambiguous, at least ' + str(move) + ' and ' + str([start, end]) + ' are possible') + moveResult = result + move = [start, end] + + # Failed to find a match + if moveResult is None: + raise Error(san, 'Not a valid move') + + return (move[0], move[1], expectedResult, promotionType) + + def encode(self, start, end, promotionType = QUEEN): + """Convert glChess co-ordinate move to SAN notation. + + 'start' is the square to move from (string, e.g. 'a1', 'h8'). + 'end' is the square to move to (string, e.g. 'a1', 'h8'). + 'promotionType' is the piece used for pawn promotion (if necesasary). + + Return the move in SAN notation or None if unable to convert. + """ + piece = self.getPiece(start) + if piece is None: + return None + (pieceColour, pieceType) = piece + victim = self.getPiece(end) + + # Test the move is valid + if self.testMove(pieceColour, start, end, promotionType) is False: + return None + + # Check for castling + if pieceType is self.KING: + # Castling + if pieceColour is self.WHITE: + baseFile = '1' + else: + baseFile = '8' + shortCastle = ('e' + baseFile, 'g' + baseFile) + longCastle = ('e' + baseFile, 'c' + baseFile) + # FIXME: Add check result + if (start, end) == shortCastle: + return SAN_CASTLE_SHORT + elif (start, end) == longCastle: + return SAN_CASTLE_LONG + + # Try and describe this piece with the minimum of information + file = '?' + rank = '?' + + # Pawns always explicitly provide rank when taking + if pieceType is self.PAWN and victim is not None: + rank = start[0] + + # First try no rank or file, then just file, then just rank, then both + result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType) + if result is None: + # Try with rank + rank = start[0] + file = '?' + result = self.__isUnique(pieceColour, pieceType, rank + '?', end, promotionType) + + if result is None: + # Try with file + rank = '?' + file = start[1] + result = self.__isUnique(pieceColour, pieceType, '?' + file, end, promotionType) + + if result is None: + # Try with rank and file + result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType) + + # This move is illegal + if result is None: + return None + + # Store the piece that is being moved, note pawns are not marked + san = '' + if pieceType is not self.PAWN: + san += pieceType + + # Disambiguations + if rank != '?': + san += rank + if file != '?': + san += file + + # Mark if taking a piece + if victim is not None: + san += SAN_TAKE + + # Write target co-ordinate + san += end + + # If a pawn promotion record the type + if pieceColour is self.WHITE: + promotionFile = '8' + else: + promotionFile = '1' + if pieceType == self.PAWN and end[1] == promotionFile: + san += SAN_PROMOTE + promotionType + + # Record if this is a check/checkmate move + if result is self.CHECK: + san += self.CHECK + elif result is self.CHECKMATE: + san += self.CHECKMATE + + return san + + def __isUnique(self, colour, pieceType, start, end, promotionType = QUEEN): + """Test if a move is unique. + + 'colour' is the piece colour being moved. (self.WHITE or self.BLACK). + 'pieceType' is the type of the piece being moved (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN|KING]). + 'start' is the start location of the move (tuple (file, rank). rank and file can be None). + 'end' is the end point of the move (tuple (file,rank)). + 'promotionType' is the piece type to promote pawns to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]). + + Return the result of self.testMove() if a unique move is found otherwise None. + """ + lastResult = None + + # Work out what ranges to iterate over + if start[0] == '?': + rankRange = RANKS + else: + rankRange = start[0] + if start[1] == '?': + fileRange = FILES + else: + fileRange = start[1] + + for file in fileRange: + for rank in rankRange: + # Check if there is a piece of this type and colour at this location + p = self.getPiece(rank + file) + if p is None: + continue + if p[1] != pieceType or p[0] != colour: + continue + + # If move is valid this is a possible move + # NOTE: We check moves that would be suicide for us otherwise crafty claims they + # are ambiguous, the PGN specification says we don't need to disambiguate if only + # one non-suicidal move is available. + # (8.2.3.4: Disambiguation) */ + result = self.testMove(colour, rank + file, end, promotionType = promotionType, allowSuicide = True) + if result is not False: + # If multiple matches then not unique (duh!) + if lastResult != None: + return None + lastResult = result + + # Return the result of the move + return lastResult + +if __name__ == '__main__': + + import chess_board + + class TestConverter(SANConverter): + """ + """ + + __colourToSAN = {chess_board.WHITE: SANConverter.WHITE, chess_board.BLACK: SANConverter.BLACK} + __sanToColour = {} + for (a, b) in __colourToSAN.iteritems(): + __sanToColour[b] = a + + __typeToSAN = {chess_board.PAWN: SANConverter.PAWN, + chess_board.KNIGHT: SANConverter.KNIGHT, + chess_board.BISHOP: SANConverter.BISHOP, + chess_board.ROOK: SANConverter.ROOK, + chess_board.QUEEN: SANConverter.QUEEN, + chess_board.KING: SANConverter.KING} + __sanToType = {} + for (a, b) in __typeToSAN.iteritems(): + __sanToType[b] = a + + __board = None + + def __init__(self, board): + self.__board = board + + def testEncode(self, start, end): + print str((start, end)) + ' => ' + str(self.encode(start, end)) + + def testDecode(self, colour, san): + try: + result = self.decodeSAN(colour, san) + print san.ljust(7) + ' => ' + str(result) + except Error, e: + print san.ljust(7) + ' !! ' + str(e) + + def getPiece(self, file, rank): + """Called by SANConverter""" + piece = self.__board.getPiece((file, rank)) + if piece is None: + return None + return (self.__colourToSAN[piece.getColour()], self.__typeToSAN[piece.getType()]) + + def testMove(self, colour, start, end, promotionType, allowSuicide = False): + """Called by SANConverter""" + moveResult = self.__board.testMove(self.__sanToColour[colour], ((start, end)), self.__sanToType[promotionType], allowSuicide) + + return {chess_board.MOVE_RESULT_ILLEGAL: False, + chess_board.MOVE_RESULT_ALLOWED: True, + chess_board.MOVE_RESULT_OPPONENT_CHECK: self.CHECK, + chess_board.MOVE_RESULT_OPPONENT_CHECKMATE: self.CHECKMATE}[moveResult] + + b = chess_board.ChessBoard() + c = TestConverter(b) + + print b + c.testEncode((1,1), (1,2)) + c.testEncode((1,0), (2,2)) + + c.testDecode(c.WHITE, 'c3') # Simple pawn move + c.testDecode(c.WHITE, 'Pc3') # Explicit pawn move + c.testDecode(c.WHITE, 'c4') # Pawn march + c.testDecode(c.WHITE, 'Nc3') # Non-pawn move + c.testDecode(c.WHITE, 'Qd3') # Invalid move + c.testDecode(c.WHITE, 'Qd3=X') # Invalid promotion type + c.testDecode(c.WHITE, 'x3') # Invalid destination + c.testDecode(c.WHITE, 'ic3') # Extra characters + # TODO: Ambiguous move + print b + \ No newline at end of file diff --git a/src/lib/defaults.py b/src/lib/defaults.py new file mode 100644 index 0000000..f32d88d --- /dev/null +++ b/src/lib/defaults.py @@ -0,0 +1,47 @@ +import os, os.path +import gettext +#DOMAIN = 'glchess' +DOMAIN = 'gnome-games' +gettext.bindtextdomain(DOMAIN) +gettext.textdomain(DOMAIN) +from gettext import gettext as _ +import gtk.glade +gtk.glade.bindtextdomain (DOMAIN) +gtk.glade.textdomain (DOMAIN) + +VERSION = "2.17.1" +APPNAME = _("glChess") + +# grab the proper subdirectory, assuming we're in +# lib/python/site-packages/glchess/ +# special case our standard debian install, which puts +# all the python libraries into /usr/share/glchess +if __file__.find('/usr/share/glchess')==0: + usr='/usr' +elif __file__.find('/usr/local/share/glchess')==0: + usr='/usr/local' +else: + usr=os.path.split(os.path.split(os.path.split(os.path.split(os.path.split(__file__)[0])[0])[0])[0])[0] + # add share/glchess + # this assumes the user only specified a general build + # prefix. If they specified data and lib prefixes, we're + # screwed. See the following email for details: + # http://mail.python.org/pipermail/python-list/2004-May/220700.html + +if usr: + APP_DATA_DIR = os.path.join(usr,'share') + ICON_DIR = os.path.join(APP_DATA_DIR,'pixmaps') + IMAGE_DIR = os.path.join(ICON_DIR,'glchess') + GLADE_DIR = os.path.join(APP_DATA_DIR,'glchess') + BASE_DIR = os.path.join(APP_DATA_DIR,'glchess') +else: + ICON_DIR = '../../textures' + IMAGE_DIR = '../../textures' + GLADE_DIR = '../../glade' + BASE_DIR = '../../data' + +DATA_DIR = os.path.expanduser('~/.gnome2/glchess/') + +def initialize_games_dir (): + if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR) + diff --git a/src/lib/game.py b/src/lib/game.py new file mode 100644 index 0000000..9890939 --- /dev/null +++ b/src/lib/game.py @@ -0,0 +1,526 @@ +""" +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +import chess.board +import chess.san + +class ChessMove: + """ + """ + # The move number (game starts at 0) + number = 0 + + # The player and piece that moved + player = None + piece = None + + # The start and end position of the move + start = None + end = None + + # The move in CAN and SAN format + canMove = '' + sanMove = '' + +class ChessPlayer: + """ + """ + # The name of the player + __name = None + __type = None + + # The game this player is in + __game = None + + # Flag to show if this player is able to move + __readyToMove = False + + def __init__(self, name): + """Constructor for a chess player. + + 'name' is the name of the player. + """ + self.__name = str(name) + + # Methods to extend + + def onPieceMoved(self, piece, start, end): + """Called when a chess piece is moved. + + 'piece' is the piece that has been moved (chess.board.ChessPiece). + 'start' is the location the piece in LAN format (string) or None if the piece has been created. + 'end' is the location the piece has moved to in LAN format (string) or None if the piece has been deleted. + """ + pass + + def onPlayerMoved(self, player, move): + """Called when a player has moved. + + 'player' is the player that has moved (ChessPlayer). + 'move' is the record for this move (ChessMove). + """ + pass + + def onGameEnded(self, winningPlayer = None): + """Called when a chess game has ended. + + 'winningPlayer' is the player that won or None if the game was a draw. + """ + pass + + # Public methods + + def getName(self): + """Get the name of this player. + + Returns the player name (string). + """ + return self.__name + + def readyToMove(self): + """ + """ + return self.__readyToMove + + def canMove(self, start, end, promotionType = chess.board.QUEEN): + """ + """ + return self.__game.canMove(self, start, end, promotionType) + + def move(self, move): + """ + """ + self.__game.move(self, move) + + # Private methods + + def _setGame(self, game): + """ + """ + self.__game = game + + def _setReadyToMove(self, readyToMove): + self.__readyToMove = readyToMove + if readyToMove is True: + self.readyToMove() + +class ChessGameBoard(chess.board.ChessBoard): + """ + """ + + # Reference to the game + __game = None + + def __init__(self, game): + """ + """ + self.__game = game + chess.board.ChessBoard.__init__(self) + + def onPieceMoved(self, piece, start, end): + """Called by chess.board.ChessBoard""" + self.__game._onPieceMoved(piece, start, end) + +class ChessGameSANConverter(chess.san.SANConverter): + """ + """ + + __colourToSAN = {chess.board.WHITE: chess.san.SANConverter.WHITE, + chess.board.BLACK: chess.san.SANConverter.BLACK} + __sanToColour = {} + for (a, b) in __colourToSAN.iteritems(): + __sanToColour[b] = a + + __typeToSAN = {chess.board.PAWN: chess.san.SANConverter.PAWN, + chess.board.KNIGHT: chess.san.SANConverter.KNIGHT, + chess.board.BISHOP: chess.san.SANConverter.BISHOP, + chess.board.ROOK: chess.san.SANConverter.ROOK, + chess.board.QUEEN: chess.san.SANConverter.QUEEN, + chess.board.KING: chess.san.SANConverter.KING} + __sanToType = {} + for (a, b) in __typeToSAN.iteritems(): + __sanToType[b] = a + + __resultToSAN = {chess.board.MOVE_RESULT_ILLEGAL: False, + chess.board.MOVE_RESULT_ALLOWED: True, + chess.board.MOVE_RESULT_OPPONENT_CHECK: chess.san.SANConverter.CHECK, + chess.board.MOVE_RESULT_OPPONENT_CHECKMATE: chess.san.SANConverter.CHECKMATE} + + __board = None + + def __init__(self, board): + self.__board = board + chess.san.SANConverter.__init__(self) + + def decodeSAN(self, colour, move): + (start, end, result, promotionType) = chess.san.SANConverter.decodeSAN(self, self.__colourToSAN[colour], move) + return (start, end, self.__sanToType[promotionType]) + + def encodeSAN(self, start, end, promotionType): + if promotionType is None: + promotion = self.QUEEN + else: + promotion = self.__typeToSAN[promotionType] + return chess.san.SANConverter.encode(self, start, end, promotion) + + def getPiece(self, location): + """Called by chess.san.SANConverter""" + piece = self.__board.getPiece(location) + if piece is None: + return None + return (self.__colourToSAN[piece.getColour()], self.__typeToSAN[piece.getType()]) + + def testMove(self, colour, start, end, promotionType, allowSuicide = False): + """Called by chess.san.SANConverter""" + moveResult = self.__board.testMove(self.__sanToColour[colour], start, end, self.__sanToType[promotionType], allowSuicide) + + return self.__resultToSAN[moveResult] + +class ChessGame: + """ + """ + # The players and spectators in the game + __players = None + __whitePlayer = None + __blackPlayer = None + __spectators = None + + # The board to move on + __board = None + + # SAN en/decoders + __sanConverter = None + + # The game state (started and player to move) + __started = False + __currentPlayer = None + + # Flag to show if calling a player and the queued up moves + __notifyingPlayer = False + __queuedMoves = None + + __moves = None + + def __init__(self): + """Game constructor""" + self.__players = [] + self.__spectators = [] + self.__board = ChessGameBoard(self) + self.__sanConverter = ChessGameSANConverter(self.__board) + self.__moves = [] + + def getAlivePieces(self, moveNumber = -1): + """Get the alive pieces on the board. + + 'moveNumber' is the move to get the pieces from (integer). + + Returns a dictionary of the alive pieces (board.ChessPiece) keyed by location. + Raises an IndexError exception if moveNumber is invalid. + """ + return self.__board.getAlivePieces(moveNumber) + + def getDeadPieces(self, moveNumber = -1): + """Get the dead pieces from the game. + + 'moveNumber' is the move to get the pieces from (integer). + + Returns a list of the pieces (board.ChessPiece) in the order they were killed. + Raises an IndexError exception if moveNumber is invalid. + """ + return self.__board.getDeadPieces(moveNumber) + + def setWhite(self, player): + """Set the white player in the game. + + 'player' is the player to use as white. + + If the game has started or there is a white player an exception is thrown. + """ + assert(self.__started is False) + assert(self.__whitePlayer is None) + self.__whitePlayer = player + self.__connectPlayer(player) + + def getWhite(self): + """Returns the current white player (player.Player)""" + return self.__whitePlayer + + def setBlack(self, player): + """Set the black player in the game. + + 'player' is the player to use as black. + + If the game has started or there is a black player an exception is thrown. + """ + assert(self.__started is False) + assert(self.__blackPlayer is None) + self.__blackPlayer = player + self.__connectPlayer(player) + + def getBlack(self): + """Returns the current white player (player.Player)""" + return self.__blackPlayer + + def addSpectator(self, player): + """Add a spectator to the game. + + 'player' is the player spectating. + + This can be called after the game has started. + """ + self.__spectators.append(player) + self.__connectPlayer(player) + + def start(self, moves = []): + """Start the game. + + 'moves' is a list of moves to start with. + + If there is no white or black player then an exception is raised. + """ + assert(self.__whitePlayer is not None and self.__blackPlayer is not None) + + # Disabled for now + #import network + #self.x = network.GameReporter('Test Game', 12345) + #print 'Reporting' + + # Set initial state + self.__queuedMoves = [] + self.__currentPlayer = self.__whitePlayer + + # Load starting moves + try: + for move in moves: + self.move(self.__currentPlayer, move) + except chess.san.Error, e: + print e + + self.__started = True + + # Get the next player to move + self.__currentPlayer._setReadyToMove(True) + + def getSquareOwner(self, coord): + """TODO + """ + piece = self.__board.getPiece(coord) + if piece is None: + return None + + colour = piece.getColour() + if colour is chess.board.WHITE: + return self.__whitePlayer + elif colour is chess.board.BLACK: + return self.__blackPlayer + else: + return None + + def canMove(self, player, start, end, promotionType): + """Test if a player can move. + + 'player' is the player making the move. + 'start' is the location to move from in LAN format (string). + 'end' is the location to move from in LAN format (string). + 'promotionType' is the piece type to promote pawns to. FIXME: Make this a property of the player + + Return True if can move, otherwise False. + """ + if player is not self.__currentPlayer: + return False + + if player is self.__whitePlayer: + colour = chess.board.WHITE + elif player is self.__blackPlayer: + colour = chess.board.BLACK + else: + assert(False) + + moveResult = self.__board.testMove(colour, start, end, promotionType = promotionType) + + return moveResult is not chess.board.MOVE_RESULT_ILLEGAL + + def move(self, player, move): + """Get a player to make a move. + + 'player' is the player making the move. + 'move' is the move to make in SAN or LAN format (string). + """ + self.__queuedMoves.append((player, move)) + + # Don't process if still finishing the last move + if self.__notifyingPlayer: + return + + while True: + try: + (movingPlayer, move) = self.__queuedMoves.pop(0) + except IndexError: + return + + if movingPlayer is not self.__currentPlayer: + print 'Player attempted to move out of turn' + else: + self.__notifyingPlayer = True + self._move(movingPlayer, move) + self.__notifyingPlayer = False + + def _move(self, player, move): + """ + """ + if self.__currentPlayer is self.__whitePlayer: + nextPlayer = self.__blackPlayer + colour = chess.board.WHITE + else: + nextPlayer = self.__whitePlayer + colour = chess.board.BLACK + + # If move is SAN process it as such + try: + (start, end, _, _, promotionType, _) = chess.lan.decode(colour, move) + except chess.lan.DecodeError, e: + try: + (start, end, promotionType) = self.__sanConverter.decodeSAN(colour, move) + except chess.san.Error, e: + print 'Invalid move: ' + move + return + + # Only use promotion type if a pawn move to far file + piece = self.__board.getPiece(start) + promotion = None + if piece is not None and piece.getType() is chess.board.PAWN: + if colour is chess.board.WHITE: + if end[1] == '8': + promotion = promotionType + else: + if end[1] == '1': + promotion = promotionType + + # Re-encode for storing and reporting + sanMove = self.__sanConverter.encodeSAN(start, end, promotionType) + canMove = chess.lan.encode(colour, start, end, promotionType = promotion) + moveResult = self.__board.movePiece(colour, start, end, promotionType) + + if moveResult is chess.board.MOVE_RESULT_ILLEGAL: + print 'Illegal move: ' + str(move) + return + + move = ChessMove() + if len(self.__moves) == 0: + move.number = 1 + else: + move.number = self.__moves[-1].number + 1 + move.player = self.__currentPlayer + move.start = start + move.end = end + move.canMove = canMove + move.sanMove = sanMove + self.__moves.append(move) + + # This player has now moved + self.__currentPlayer._setReadyToMove(False) + + # Inform other players of the result + for player in self.__players: + player.onPlayerMoved(self.__currentPlayer, move) + + # Notify the next player they can move + self.__currentPlayer = nextPlayer + if self.__started is True: + nextPlayer._setReadyToMove(True) + + def getMoves(self): + """ + """ + return self.__moves[:] + + def end(self): + """End the game""" + # Inform players + for player in self.__players: + player.onGameEnded() + + # Private methods: + + def __connectPlayer(self, player): + """Add a player into the game. + + 'player' is the player to add. + + The player will be notified of the current state of the board. + """ + self.__players.append(player) + player._setGame(self) + + # Notify the player of the current state + # FIXME: Make the board iteratable... + for file in '12345678': + for rank in 'abcdefgh': + coord = rank + file + piece = self.__board.getPiece(coord) + if piece is None: + continue + + # These are moves from nowhere to their current location + player.onPieceMoved(piece, None, coord) + + def _onPieceMoved(self, piece, start, end): + """Called by the chess board""" + + # Notify all players of creations and deletions + # NOTE: Normal moves are done above since the SAN moves are calculated before the move... + # FIXME: Change this so the SAN moves are done afterwards... + for player in self.__players: + player.onPieceMoved(piece, start, end) + +class NetworkChessGame(ChessGame): + """ + """ + + def move(self, player, move): + """Get a player to make a move. + + 'player' is the player making the move. + 'move' is the move to make. It can be of the form: + A coordinate move in the form ((file0, rank0), (file1, rank1), promotionType) ((int, int), (int, int), chess.board.PIECE_TYPE) or + A SAN move (string). + """ + # Send to the server + + +if __name__ == '__main__': + game = ChessGame() + + import pgn + + p = pgn.PGN('black.pgn') + g = p.getGame(0) + + class PGNPlayer(ChessPlayer): + __moveNumber = 1 + + __isWhite = True + + def __init__(self, isWhite): + self.__isWhite = isWhite + + def readyToMove(self): + if self.__isWhite: + move = g.getWhiteMove(self.__moveNumber) + else: + move = g.getBlackMove(self.__moveNumber) + self.__moveNumber += 1 + self.move(move) + + white = PGNPlayer(True) + black = PGNPlayer(False) + + game.setWhite(white) + game.setBlack(black) + + game.start() + \ No newline at end of file diff --git a/src/lib/glchess.py b/src/lib/glchess.py new file mode 100644 index 0000000..d9276c9 --- /dev/null +++ b/src/lib/glchess.py @@ -0,0 +1,4 @@ +def start_game (): + import main + app = main.Application() + app.start() diff --git a/src/lib/gtkui/Makefile.am b/src/lib/gtkui/Makefile.am new file mode 100644 index 0000000..0bea3f4 --- /dev/null +++ b/src/lib/gtkui/Makefile.am @@ -0,0 +1,5 @@ +glchessdir = $(pythondir)/glchess/gtkui +glchess_PYTHON = \ + dialogs.py \ + gtkui.py \ + __init__.py diff --git a/src/lib/gtkui/__init__.py b/src/lib/gtkui/__init__.py new file mode 100644 index 0000000..10b9be6 --- /dev/null +++ b/src/lib/gtkui/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +from gtkui import GtkView, GtkUI diff --git a/src/lib/gtkui/dialogs.py b/src/lib/gtkui/dialogs.py new file mode 100644 index 0000000..b3ce282 --- /dev/null +++ b/src/lib/gtkui/dialogs.py @@ -0,0 +1,540 @@ +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +import os + +import gobject +import gtk +import gtk.glade +import gtk.gdk + +import gtkui + +class GtkServerList: + __gui = None + + def __init__(self, gui): + self.__gui = gui + + self.__servers = [] + view = gui.get_widget('server_list') + if view is not None: + store = gtk.ListStore(str, gobject.TYPE_PYOBJECT) + view.set_model(store) + + cell = gtk.CellRendererText() + column = gtk.TreeViewColumn('name', cell) + column.add_attribute(cell, 'text', 0) + view.append_column(column) + + def add(self, name, game): + """ + """ + view = self.__gui.get_widget('server_list') + if view is None: + return + model = view.get_model() + iter = model.append() + model.set(iter, 0, name) + model.set(iter, 1, game) + + def getSelected(self): + """ + """ + view = self.__gui.get_widget('server_list') + if view is None: + return None + selection = view.get_selection() + (model, iter) = selection.get_selected() + + if iter is None: + return None + else: + return model.get_value(iter, 1) + + def remove(self, game): + """ + """ + view = self.__gui.get_widget('server_list') + if view is None: + return + model = view.get_model() + + iter = model.get_iter_first() + while iter is not None: + if model.get_value(iter, 1) is game: + break + iter = model.iter_next(iter) + + if iter is not None: + model.remove(iter) + +class GtkNewGameDialog: + """ + """ + # The main UI and the ??? + __mainUI = None + __gui = None + + __moves = None + + def __init__(self, mainUI, aiModel, gameName = None, + whiteName = None, blackName = None, + whiteAI = None, blackAI = None, moves = None): + """Constructor for a new game dialog. + + 'mainUI' is the main UI. + 'aiModel' is the AI models to use. + 'gameName' is the name of the game (string) or None if unknown. + 'whiteName' is the name of the white player (string) or None if unknown. + 'blackName' is the name of the white player (string) or None if unknown. + 'whiteAI' is the type of AI the white player is (string) or None if no AI. + 'blackAI' is the type of AI the black player is (string) or None if no AI. + 'moves' is a list of moves (strings) that the have already been made. + """ + self.__mainUI = mainUI + self.__moves = moves + + # Load the UI + self.__gui = gtkui.loadGladeFile('new_game.glade', 'new_game_dialog', domain = 'glchess') + self.__gui.signal_autoconnect(self) + + # Make all the AI combo boxes use one list of AI types + for name in ['black_type_combo', 'white_type_combo']: + widget = self.__gui.get_widget(name) + if widget is None: + continue + + widget.set_model(aiModel) + + cell = gtk.CellRendererPixbuf() + widget.pack_start(cell, False) + widget.add_attribute(cell, 'pixbuf', 1) + + cell = gtk.CellRendererText() + widget.pack_start(cell, False) + widget.add_attribute(cell, 'text', 2) + + widget.set_active(0) + + # Use the supplied properties + if moves: + self.__getWidget('new_game_dialog').set_title('Restore game (%i moves)' % len(moves)) + if gameName: + self.__getWidget('game_name_entry').set_text(gameName) + + if whiteName: + self.__getWidget('white_name_entry').set_text(whiteName) + if blackName: + self.__getWidget('black_name_entry').set_text(blackName) + + # Configure AIs + if whiteAI: + self.__getWidget('white_type_combo').set_active_iter(self.__getAIIter(aiModel, whiteAI)) + if blackAI: + self.__getWidget('black_type_combo').set_active_iter(self.__getAIIter(aiModel, blackAI)) + + # Show the dialog + self.__getWidget('new_game_dialog').show() + self.__testReady() + + # Private methods + + def __getAIIter(self, model, name): + """Get an AI engine. + + 'name' is the name of the AI engine to find. + + Return the iter for this AI or None if no AI of this name. + """ + # FIXME: I'm sure there is a more efficient way of doing this... + iter = model.get_iter_first() + while True: + if name == model.get_value(iter, 0): + return iter + + iter = model.iter_next(iter) + if iter is None: + return None + + def __getWidget(self, name): + """ + """ + return self.__gui.get_widget(name) + + def __getAIType(self, comboBox): + """ + """ + model = comboBox.get_model() + iter = comboBox.get_active_iter() + if iter is None: + return None + + data = model.get(iter, 0) + return data[0] + + def __getWhitePlayer(self): + """ + """ + name = self.__getWidget('white_name_entry').get_text() + if len(name) == 0: + return (None, None) + aiType = self.__getAIType(self.__getWidget('white_type_combo')) + return (name, aiType) + + def __getBlackPlayer(self): + """ + """ + name = self.__getWidget('black_name_entry').get_text() + if len(name) == 0: + return (None, None) + aiType = self.__getAIType(self.__getWidget('black_type_combo')) + return (name, aiType) + + def __testReady(self): + ready = True + + # Must have a name for the game + name = self.__getWidget('game_name_entry').get_text() + if len(name) == 0: + ready = False + + # Must have two valid players + white = self.__getWhitePlayer() + black = self.__getBlackPlayer() + if white is None or black is None: + ready = False + + # Can only click OK if have enough information + self.__getWidget('start_button').set_sensitive(ready) + + def __startGame(self): + # FIXME: Game properties + gameName = self.__getWidget('game_name_entry').get_text() + allowSpectators = True + + # Get the players + white = self.__getWhitePlayer() + black = self.__getBlackPlayer() + assert(white is not None) + assert(black is not None) + + # Inform the child class + self.__mainUI.onGameStart(gameName, allowSpectators, white[0], white[1], black[0], black[1], self.__moves) + + # Gtk+ signal handlers + + def _on_properties_changed(self, widget, *data): + """Gtk+ callback""" + self.__testReady() + + def _on_response(self, widget, response_id, data = None): + """Gtk+ callback""" + if response_id == gtk.RESPONSE_OK: + self.__startGame() + self.__getWidget('new_game_dialog').destroy() + +class GtkLoadGameDialog: + """ + """ + __mainUI = None + __gui = None + + def __init__(self, mainUI): + """ + """ + self.__mainUI = mainUI + + # Load the UI + self.__gui = gtkui.loadGladeFile('load_game.glade', domain = 'glchess') + self.__gui.signal_autoconnect(self) + + fileChooser = self.__gui.get_widget('filechooserwidget') + + # Filter out non PGN files by default + pgnFilter = gtk.FileFilter() + pgnFilter.set_name('PGN files') + pgnFilter.add_pattern('*.pgn') + fileChooser.add_filter(pgnFilter) + + allFilter = gtk.FileFilter() + allFilter.set_name('All files') + allFilter.add_pattern('*') + fileChooser.add_filter(allFilter) + + dialog = self.__gui.get_widget('game_load_dialog') + dialog.show() + + def __getFilename(self): + """Get the currently selected filename. + + Returns the filename (string) or None if none selected. + """ + return self.__gui.get_widget('filechooserwidget').get_filename() + + def _on_file_changed(self, widget, data = None): + """Gtk+ callback""" + name = self.__getFilename() + if name is None: + isFile = False + else: + isFile = os.path.isfile(name) + self.__gui.get_widget('open_button').set_sensitive(isFile) + self.__gui.get_widget('properties_button').set_sensitive(isFile) + + def _on_load_game(self, widget, data = None): + """Gtk+ callback""" + self.__mainUI.loadGame(self.__getFilename(), False) + self._on_close(widget, data) + + def _on_configure_game(self, widget, data = None): + """Gtk+ callback""" + self.__mainUI.loadGame(self.__getFilename(), True) + self._on_close(widget, data) + + def _on_close(self, widget, data = None): + """Gtk+ callback""" + self.__gui.get_widget('game_load_dialog').destroy() + +class GtkJoinGameDialog: + """ + """ + # The main UI and the ??? + __mainUI = None + __gui = None + + __serverList = None + + __moves = None + + def __init__(self, mainUI, aiModel, gameName = None, + localName = None, localAI = None, moves = None): + """Constructor for a join game dialog. + + 'mainUI' is the main UI. + 'aiModel' is the AI models to use. + 'gameName' is the name of the game (string) or None if unknown. + 'localName' is the name of the local player (string) or None if unknown. + 'localAI' is the type of AI the local player is (string) or None if no AI. + 'moves' is a list of moves (strings) that the have already been made. + """ + self.__mainUI = mainUI + self.__moves = moves + + # Load the UI + self.__gui = gtkui.loadGladeFile('new_game.glade', 'join_game_dialog', domain = 'glchess') + + # Make all the AI combo boxes use one list of AI types + combo = self.__gui.get_widget('local_type_combo') + combo.set_model(aiModel) + cell = gtk.CellRendererPixbuf() + combo.pack_start(cell, False) + combo.add_attribute(cell, 'pixbuf', 1) + cell = gtk.CellRendererText() + combo.pack_start(cell, False) + combo.add_attribute(cell, 'text', 2) + combo.set_active(0) + + # Use the supplied properties + if moves: + self.__getWidget('join_game_dialog').set_title('Restore game (%i moves)' % len(moves)) + if gameName: + self.__getWidget('game_name_entry').set_text(gameName) + + # Configure local player + if localName: + self.__getWidget('local_name_entry').set_text(whiteName) + if localAI: + # FIXME + self.__getWidget('local_ai_type_combo').set_active_iter(self.__getAIIter(aiModel, localAI)) + + # ... + self.__serverList = GtkServerList(self.__gui) + view = self.__getWidget('server_list') + if view is not None: + selection = view.get_selection() + selection.connect('changed', self._on_properties_changed) + + # Show the dialog + self.__gui.signal_autoconnect(self) + self.__getWidget('join_game_dialog').show() + self.__testReady() + + def addNetworkGame(self, name, game): + """ + """ + self.__serverList.add(name, game) + # FIXME: Update? + + def removeNetworkGame(self, game): + """ + """ + self.__serverList.remove(game) + # FIXME: Update? + + # Private methods + + def __getAIIter(self, model, name): + """Get an AI engine. + + 'name' is the name of the AI engine to find. + + Return the iter for this AI or None if no AI of this name. + """ + # FIXME: I'm sure there is a more efficient way of doing this... + iter = model.get_iter_first() + while True: + if name == model.get_value(iter, 0): + return iter + + iter = model.iter_next(iter) + if iter is None: + return None + + def __getWidget(self, name): + """ + """ + return self.__gui.get_widget(name) + + def __getAIType(self, comboBox): + """ + """ + model = comboBox.get_model() + iter = comboBox.get_active_iter() + if iter is None: + return None + + data = model.get(iter, 0) + return data[0] + + def __getLocalPlayer(self): + """ + """ + name = self.__getWidget('local_name_entry').get_text() + if len(name) == 0: + return (None, None) + aiType = self.__getAIType(self.__getWidget('local_type_combo')) + return (name, aiType) + + def __testReady(self): + ready = True + # Must have a selected server + if self.__serverList.getSelected() is None: + ready = False + + # Must have a valid local player + player = self.__getLocalPlayer() + if player is None: + ready = False + + # FIXME: Some games do not allow spectators + + # Can only click OK if have enough information + self.__getWidget('join_button').set_sensitive(ready) + + def __startGame(self): + player = self.__getLocalPlayer() + assert(player is not None) + + # Joining a server + server = self.__serverList.getSelected() + assert(server is not None) + self.__mainUI.onGameJoin(player[0], player[1], server) + + # Gtk+ signal handlers + + def _on_find_servers_button_clicked(self, widget, data=None): + """Gtk+ callback""" + host = self.__getWidget('server_entry').get_text() + if host == '': + self.__mainUI.onNetworkServerSearch() + else: + self.__mainUI.onNetworkServerSearch(host) + + def _on_search_server_entry_changed(self, widget, data=None): + """Gtk+ callback""" + # FIXME: Change colour back to default + pass + + def _on_properties_changed(self, widget, *data): + """Gtk+ callback""" + self.__testReady() + + def _on_response(self, widget, response_id, data = None): + """Gtk+ callback""" + if response_id == gtk.RESPONSE_OK: + self.__startGame() + self.__getWidget('join_game_dialog').destroy() + +class GtkSaveGameDialog: + """ + """ + # The main UI + __mainUI = None + + # The view that is being saved + __view = None + + # The GUI + __gui = None + + def __init__(self, mainUI, view): + """ + """ + self.__mainUI = mainUI + self.__view = view + + # Load the UI + self.__gui = gtkui.loadGladeFile('save_game.glade', domain = 'glchess') + self.__gui.signal_autoconnect(self) + + # Filter out non PGN files by default + dialog = self.__gui.get_widget('dialog') + pgnFilter = gtk.FileFilter() + pgnFilter.set_name('PGN files') + pgnFilter.add_pattern('*.pgn') + dialog.add_filter(pgnFilter) + + allFilter = gtk.FileFilter() + allFilter.set_name('All files') + allFilter.add_pattern('*') + dialog.add_filter(allFilter) + + def _on_save(self, widget, data = None): + """Gtk+ callback""" + dialog = self.__gui.get_widget('dialog') + + # Append .pgn to the end if not provided + fname = dialog.get_filename() + if fname[-4:].lower() != '.pgn': + fname += '.pgn' + + self.__mainUI._saveView(self.__view, fname) + dialog.destroy() + + def _on_close(self, widget, data = None): + """Gtk+ callback""" + dialog = self.__gui.get_widget('dialog') + dialog.destroy() + self.__mainUI._saveView(self.__view, None) + +class GtkErrorDialog: + """ + """ + __gui = None + + def __init__(self, title, contents): + """ + """ + self.__gui = gtkui.loadGladeFile('error_dialog.glade', domain = 'glchess') + self.__gui.signal_autoconnect(self) + + self.__gui.get_widget('title_label').set_markup('' + title + '') + self.__gui.get_widget('content_label').set_text(contents) + + dialog = self.__gui.get_widget('dialog').show_all() + + def _on_close(self, widget, data = None): + """Gtk+ callback""" + dialog = self.__gui.get_widget('dialog').destroy() diff --git a/src/lib/gtkui/gtkui.py b/src/lib/gtkui/gtkui.py new file mode 100644 index 0000000..e9ae627 --- /dev/null +++ b/src/lib/gtkui/gtkui.py @@ -0,0 +1,931 @@ +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +__all__ = ['GtkView', 'GtkUI'] + +# TODO: Extend base UI classes? + +import os +import sys +import traceback +import time +import gettext +import ConfigParser + +import gobject +import gtk +import gtk.glade +import gtk.gdk +import pango +import gnome, gnome.ui + +from glchess.defaults import * + +# Optionally use OpenGL support +try: + import gtk.gtkgl +except ImportError: + pass + +# Stop PyGTK from catching exceptions +os.environ['PYGTK_FATAL_EXCEPTIONS'] = '1' + +import glchess.ui +import dialogs + +def loadGladeFile(name, root = None, domain = None): + return gtk.glade.XML(os.path.join(GLADE_DIR, name), root, domain = domain) + +class GtkViewArea(gtk.DrawingArea): + """Custom widget to render an OpenGL scene""" + # The view this widget is rendering + view = None + + renderGL = False + + # Pixmaps to use for double buffering + pixmap = None + dynamicPixmap = None + + # TODO... + __glDrawable = None + + def __init__(self, view): + """ + """ + gtk.DrawingArea.__init__(self) + + self.view = view + + gnome.program_init('glchess',VERSION, + properties={gnome.PARAM_APP_DATADIR:APP_DATA_DIR} + ) + + + # Allow notification of button presses + self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.BUTTON_MOTION_MASK) + + # Make openGL drawable + if hasattr(gtk, 'gtkgl'): + gtk.gtkgl.widget_set_gl_capability(self, self.view.ui.notebook.glConfig)# FIXME:, share_list=glContext) + + # Connect signals + self.connect('realize', self.__init) + self.connect('configure_event', self.__configure) + self.connect('expose_event', self.__expose) + self.connect('button_press_event', self.__button_press) + self.connect('button_release_event', self.__button_release) + + # Public methods + + def redraw(self): + """Request this widget is redrawn""" + #FIXME: Check this is valid + self.window.invalidate_rect(self.allocation, False) + + def setRenderGL(self, renderGL): + """Enable OpenGL rendering""" + if not hasattr(gtk, 'gtkgl'): + renderGL = False + + if self.renderGL == renderGL: + return + self.renderGL = renderGL + self.redraw() + + # Private methods + + def __startGL(self): + """Get the OpenGL context""" + if not self.renderGL: + return + + assert(self.__glDrawable is None) + + # Obtain a reference to the OpenGL drawable + # and rendering context. + glDrawable = gtk.gtkgl.widget_get_gl_drawable(self) + glContext = gtk.gtkgl.widget_get_gl_context(self) + + # OpenGL begin. + if not glDrawable.gl_begin(glContext): + return + + self.__glDrawable = glDrawable + + import OpenGL + if not self.view.ui.openGLInfoPrinted: + print 'Using OpenGL:' + print 'VENDOR=' + OpenGL.GL.glGetString(OpenGL.GL.GL_VENDOR) + print 'RENDERER=' + OpenGL.GL.glGetString(OpenGL.GL.GL_RENDERER) + print 'VERSION=' + OpenGL.GL.glGetString(OpenGL.GL.GL_VERSION) + print 'EXTENSIONS=' + OpenGL.GL.glGetString(OpenGL.GL.GL_EXTENSIONS) + self.view.ui.openGLInfoPrinted = True + + def __endGL(self): + """Free the OpenGL context""" + if not self.renderGL: + return + + assert(self.__glDrawable is not None) + self.__glDrawable.gl_end() + self.__glDrawable = None + + def __init(self, widget): + """Gtk+ signal""" + if self.view.feedback is not None: + self.view.feedback.reshape(widget.allocation.width, widget.allocation.height) + + def __configure(self, widget, event): + """Gtk+ signal""" + self.pixmap = gtk.gdk.Pixmap(widget.window, event.width, event.height) + self.dynamicPixmap = gtk.gdk.Pixmap(widget.window, event.width, event.height) + self.__startGL() + if self.view.feedback is not None: + self.view.feedback.reshape(event.width, event.height) + self.__endGL() + + def __expose(self, widget, event): + """Gtk+ signal""" + if self.renderGL: + self.__startGL() + + # Get the scene rendered + try: + if self.view.feedback is not None: + self.view.feedback.renderGL() + except GLerror, e: + print 'Rendering Error: ' + str(e) + traceback.print_exc(file = sys.stdout) + + # Paint this + if self.__glDrawable.is_double_buffered(): + self.__glDrawable.swap_buffers() + else: + glFlush() + + self.__endGL() + + else: + context = self.pixmap.cairo_create() + if self.view.feedback is not None: + self.view.feedback.renderCairoStatic(context) + + # Copy the background to render the dynamic elements on top + self.dynamicPixmap.draw_drawable(widget.get_style().white_gc, self.pixmap, 0, 0, 0, 0, -1, -1) + context = self.dynamicPixmap.cairo_create() + + # Set a clip region for the expose event + context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) + context.clip() + + # Render the dynamic elements + if self.view.feedback is not None: + self.view.feedback.renderCairoDynamic(context) + + # Draw the window + widget.window.draw_drawable(widget.get_style().white_gc, self.dynamicPixmap, + event.area.x, event.area.y, + event.area.x, event.area.y, event.area.width, event.area.height) + + def __button_press(self, widget, event): + """Gtk+ signal""" + self.__startGL() + if self.view.feedback is not None: + self.view.feedback.select(event.x, event.y) + self.__endGL() + + def __button_release(self, widget, event): + """Gtk+ signal""" + self.__startGL() + if self.view.feedback is not None: + self.view.feedback.deselect(event.x, event.y) + self.__endGL() + +class GtkView(glchess.ui.ViewController): + """ + """ + # The UI this view belongs to + ui = None + + # The widget to render the scene to + widget = None + + # A Gtk+ tree model to store the move history + moveModel = None + selectedMove = -1 + + def __init__(self, ui, feedback, isActive = True): + """Constructor for a view. + + 'feedback' is the feedback object for this view (extends ui.ViewFeedback). + 'isActive' is a flag showing if this view can be controlled by the user (True) or not (False). + """ + self.ui = ui + self.feedback = feedback + self.isActive = isActive + self.widget = GtkViewArea(self) + + # Make a model for navigation + model = gtk.ListStore(int, str) + iter = model.append() + model.set(iter, 0, 0, 1, gettext.gettext('Game Start')) + self.moveModel = model + + self.widget.show_all() + + # Extended methods + + def render(self): + """Extends glchess.ui.ViewController""" + self.widget.redraw() + + def addMove(self, move): + """Extends glchess.ui.ViewController""" + # FIXME: Make a '@ui' player who watches for these itself? + iter = self.moveModel.append() + string = '%2i. ' % ((move.number - 1) / 2 + 1) + if move.number % 2 == 0: + string += '... ' + string += move.sanMove + self.moveModel.set(iter, 0, move.number, 1, string) + + # If is the current view and tracking the game select this + if self.selectedMove == -1: + self.ui._updateViewButtons() + + def close(self): + """Extends glchess.ui.ViewController""" + self.ui._removeView(self) + + # Public methods + + def _getModel(self): + """ + """ + return (self.moveModel, self.selectedMove) + + def _setMoveNumber(self, moveNumber): + """Set the move number this view requests. + + 'moveNumber' is the move number to use (integer). + """ + self.selectedMove = moveNumber + if self.feedback is not None: + self.feedback.setMoveNumber(moveNumber) + +class GtkGameNotebook(gtk.Notebook): + """ + """ + + glConfig = None + + defaultView = None + viewsByWidget = None + + def __init__(self, ui): + """ + """ + self.ui = ui + self.viewsByWidget = {} + + gtk.Notebook.__init__(self) + self.set_show_border(False) + + # Make the tabs scrollable so the area is not resized + self.set_scrollable(True) + + # Configure openGL + try: + gtk.gdkgl + except AttributeError: + self.glConfig = None + else: + display_mode = (gtk.gdkgl.MODE_RGB | gtk.gdkgl.MODE_DEPTH | gtk.gdkgl.MODE_DOUBLE) + try: + self.glConfig = gtk.gdkgl.Config(mode = display_mode) + except gtk.gdkgl.NoMatches: + display_mode &= ~gtk.gdkgl.MODE_DOUBLE + self.glConfig = gtk.gdkgl.Config(mode = display_mode) + + self.set_show_tabs(False) + + def setDefault(self, feedback): + """ + """ + assert(self.defaultView is None) + self.defaultView = GtkView(self.ui, feedback) + page = self.append_page(self.defaultView.widget) + self.set_current_page(page) + + self.__updateTabVisibleState() + + return self.defaultView + + def addView(self, title, feedback): + """ + """ + view = GtkView(self.ui, feedback) + self.viewsByWidget[view.widget] = view + page = self.append_page(view.widget) + self.set_tab_label_text(view.widget, title) + self.set_current_page(page) + + self.__updateTabVisibleState() + + return view + + def getView(self, pageNumber = None): + """Get the view at a given page number. + + 'pageNumber' is the page to check or None to get the selected page. + + Return the view (GtkView) on this page or None if no view here. + """ + if pageNumber is None: + # If splashscreen present then there is no view + if len(self.viewsByWidget) > 0: + num = self.get_current_page() + if num < 0: + return None + widget = self.get_nth_page(num) + else: + return None + else: + widget = self.get_nth_page(pageNumber) + + return self.viewsByWidget[widget] + + def removeView(self, view): + """Remove a view from the notebook. + + 'view' is the view to remove. + """ + self.remove_page(self.page_num(view.widget)) + self.viewsByWidget.pop(view.widget) + self.__updateTabVisibleState() + + def __updateTabVisibleState(self): + """ + """ + # Only show tabs if there is more than one game + self.set_show_tabs(len(self.viewsByWidget) > 1) + + # Show/hide the default view + if len(self.viewsByWidget) == 0: + self.defaultView.widget.show() + else: + self.defaultView.widget.hide() + +class AIWindow: + """ + """ + + notebook = None + defaultPage = None + + # We keep track of the number of pages as there is a bug + # in GtkNotebook (Gnome bug #331785). + pageCount = 0 + + def __init__(self, notebook): + """ + """ + self.notebook = notebook + self.defaultPage = notebook.get_nth_page(0) + + def addView(self, title, executable, description): + """ + """ + # Hide the default page + self.defaultPage.hide() + self.notebook.set_show_tabs(True) + + self.pageCount += 1 + return AIView(self, title, executable, description) + +class AIView: + """ + """ + + __gui = None + + def __init__(self, window, title, executable, description): + """ + """ + self.window = window + self.__gui = loadGladeFile('ai.glade', 'ai_table', domain = 'glchess') + self.__gui.get_widget('executable_label').set_text(executable) + self.__gui.get_widget('game_label').set_text(description) + + # Add into the notebook + self.root = self.__gui.get_widget('ai_table') + notebook = window.notebook + notebook.append_page(self.root, gtk.Label(title)) + + # Create styles for the buffer + buffer = self.__gui.get_widget('comms_textview').get_buffer() + buffer.create_tag('input', family='Monospace') + buffer.create_tag('output', family='Monospace', weight = pango.WEIGHT_BOLD) + buffer.create_tag('move', family='Monospace', foreground = 'blue') + buffer.create_tag('info', family='Monospace', foreground = 'green') + buffer.create_tag('error', family='Monospace', foreground = 'red') + + def addText(self, text, style): + """FIXME: Define style + """ + buffer = self.__gui.get_widget('comms_textview').get_buffer() + buffer.insert_with_tags_by_name(buffer.get_end_iter(), text, style) + + def close(self): + """ + """ + self.window.pageCount -= 1 + self.window.notebook.remove_page(self.window.notebook.page_num(self.root)) + + # Show the default page + if self.window.pageCount == 0: + self.window.defaultPage.show() + self.window.notebook.set_show_tabs(False) + +class GtkUI(glchess.ui.UI): + """ + """ + # The Gtk+ GUI + _gui = None + + # The time stored for animation + __lastTime = None + __animationTimer = None + + # The notebook containing games + notebook = None + + # The Gtk+ list model of the available player types + __playerModel = None + + # The about dialog open + __aboutDialog = None + + # Dictionary of save game dialogs keyed by view + __saveGameDialogs = None + + # Dictionary of configuration options + __config = None + __applyingConfig = False + + __renderGL = False + openGLInfoPrinted = False + + # TODO + __joinGameDialogs = None + __networkGames = None + + __defaultWhiteAI = None + __defaultBlackAI = None + + def __init__(self): + """Constructor for a GTK+ glChess GUI""" + self.__networkGames = {} + self.__saveGameDialogs = {} + self.__joinGameDialogs = [] + + self._gui = loadGladeFile('glchess.glade', domain = 'glchess') + self._gui.signal_autoconnect(self) + + # Make a notebook for the games + self.notebook = GtkGameNotebook(self) + self.notebook.connect_after('switch-page', self._on_view_changed) + self.__getWidget('game_viewport').add(self.notebook) + self.notebook.show_all() + + # Create the model for the player types + self.__playerModel = gtk.ListStore(gobject.TYPE_PYOBJECT, gtk.gdk.Pixbuf, str) + iconTheme = gtk.icon_theme_get_default() + try: + icon = iconTheme.load_icon('stock_people', 24, gtk.ICON_LOOKUP_USE_BUILTIN) + except gobject.GError: + icon = None + iter = self.__playerModel.append() + self.__playerModel.set(iter, 0, None, 1, icon, 2, gettext.gettext('Human')) + # FIXME: Add spectators for network games + + self.__aiWindow = AIWindow(self._gui.get_widget('ai_notebook')) + + combo = self.__getWidget('history_combo') + cell = gtk.CellRendererText() + combo.pack_start(cell, False) + combo.add_attribute(cell, 'text', 1) + + self.defaultViewController = self.notebook.setDefault(None) + + # Disable OpenGL support + if not hasattr(gtk, 'gtkgl'): + self._gui.get_widget('menu_view_3d').set_sensitive(False) + + # Load UI preferences + self.__config = {} + self.__loadConfig() + + self.defaultViewController.widget.setRenderGL(self.__renderGL) + + # Public methods + + def watchFileDescriptor(self, fd): + """Extends ui.UI""" + gobject.io_add_watch(fd, gobject.IO_IN, self.__readData) + + def __readData(self, fd, condition): + return self.onReadFileDescriptor(fd) + + def addAIEngine(self, name): + """Register an AI engine. + + 'name' is the name of the engine. + TODO: difficulty etc etc + """ + iconTheme = gtk.icon_theme_get_default() + try: + icon = iconTheme.load_icon("stock_notebook", 24, gtk.ICON_LOOKUP_USE_BUILTIN) + except gobject.GError: + icon = None + iter = self.__playerModel.append() + self.__playerModel.set(iter, 0, name, 1, icon, 2, name) + + # Get the human to play against this AI + if self.__defaultBlackAI is None: + self.__defaultBlackAI = name + + def setDefaultView(self, feedback): + """Extends ui.UI""" + self.defaultViewController.feedback = feedback + return self.defaultViewController + + def addView(self, title, feedback): + """Extends ui.UI""" + view = self.notebook.addView(title, feedback) + view.widget.setRenderGL(self.__renderGL) + return view + + def addAIWindow(self, title, executable, description): + """ + """ + return self.__aiWindow.addView(title, executable, description) + + def run(self): + """Run the UI. + + This method will not return. + """ + gtk.main() + + # Extended methods + + def reportError(self, title, error): + """Extends glchess.ui.UI""" + dialogs.GtkErrorDialog(title, error) + + def reportGameLoaded(self, gameName = None, + whiteName = None, blackName = None, + whiteAI = None, blackAI = None, moves = None): + """Extends glchess.ui.UI""" + dialogs.GtkNewGameDialog(self, self.__playerModel, gameName = gameName, + whiteName = whiteName, whiteAI = whiteAI, + blackName = blackName, blackAI = blackAI, moves = moves) + + def addNetworkGame(self, name, game): + """Extends glchess.ui.UI""" + self.__networkGames[game] = name + + # Update the open dialogs + for dialog in self.__joinGameDialogs: + dialog.addNetworkGame(name, game) + + def removeNetworkGame(self, game): + """Extends glchess.ui.UI""" + self.__networkGames.pop(game) + + # Update the open dialogs + for dialog in self.__joinGameDialogs: + dialog.removeNetworkGame(game) + + # Protected methods + + def _saveView(self, view, path): + """ + """ + self.__saveGameDialogs.pop(view) + if path is None: + return + + if view.feedback is not None: + view.feedback.save(path) + + def _removeView(self, view): + """Remove a view from the UI. + + 'view' is the view to remove. + """ + self.notebook.removeView(view) + self._updateViewButtons() + + # Private methods + + def __loadConfig(self): + """ + """ + # Set defaults + self.__config = {'show_toolbar': 'True', + 'show_history': 'True', + 'show_3d': 'False'} + + name = os.path.expanduser('~/.glchess/gtkui.ini') + cp = ConfigParser.ConfigParser() + cp.read(name) + + try: + items = cp.items('ui') + except ConfigParser.NoSectionError: + self.__saveConfig() + else: + for (name, value) in items: + self.__config[name] = value + + self.__applyConfig() + + def __setConfig(self, name, value): + """ + """ + if self.__applyingConfig: + return + + self.__config[name] = value + self.__applyConfig() + self.__saveConfig() + + def __saveConfig(self): + """ + """ + name = os.path.expanduser('~/.glchess/gtkui.ini') + try: + f = file(name, 'w') + except IOError, e: + print 'Unable to save config: ' + str(e) + return + + cp = ConfigParser.ConfigParser() + cp.add_section('ui') + for (name, value) in self.__config.iteritems(): + cp.set('ui', name, value) + cp.write(f) + + def __applyConfig(self): + """ + """ + # Stop recursion + self.__applyingConfig = True + + # Show/hide the toolbar + toolbar = self.__getWidget('toolbar') + menu = self.__getWidget('menu_view_toolbar') + if self.__config['show_toolbar'] == 'True': + menu.set_active(True) + toolbar.show() + else: + menu.set_active(False) + toolbar.hide() + + # Show/hide the history + box = self.__getWidget('navigation_box') + menu = self.__getWidget('menu_view_history') + if self.__config['show_history'] == 'True': + menu.set_active(True) + box.show() + else: + menu.set_active(False) + box.hide() + + # Enable/disable OpenGL rendering + self.__renderGL = self.__config['show_3d'] == 'True' + menuItem = self.__getWidget('menu_view_3d') + menuItem.set_active(self.__renderGL) + self.notebook.defaultView.widget.setRenderGL(self.__renderGL) + for view in self.notebook.viewsByWidget.itervalues(): + view.widget.setRenderGL(self.__renderGL) + + self.__applyingConfig = False + + def startAnimation(self): + """Start the animation callback""" + if self.__animationTimer is None: + self.__lastTime = time.time() + self.__animationTimer = gobject.timeout_add(10, self.__animate) + + def __animate(self): + # Get the timestep, if it is less than zero or more than a second + # then the system clock was probably changed. + now = time.time() + step = now - self.__lastTime + if step < 0.0: + step = 0.0 + elif step > 1.0: + step = 1.0 + self.__lastTime = now + + # Animate! + animating = self.onAnimate(step) + if not animating: + self.__animationTimer = None + + # Keep/delete timer + return animating + + def __getWidget(self, name): + return self._gui.get_widget(name) + + def _on_show_toolbar_clicked(self, widget, data = None): + """Gtk+ callback""" + if widget.get_active(): + value = 'True' + else: + value = 'False' + self.__setConfig('show_toolbar', value) + + def _on_show_history_clicked(self, widget, data = None): + """Gtk+ callback""" + if widget.get_active(): + value = 'True' + else: + value = 'False' + self.__setConfig('show_history', value) + + def _on_toggle_3d_clicked(self, widget, data = None): + """Gtk+ callback""" + if widget.get_active(): + value = 'True' + else: + value = 'False' + self.__setConfig('show_3d', value) + + def _on_show_ai_stats_clicked(self, widget, data = None): + """Gtk+ callback""" + window = self._gui.get_widget('ai_window') + if widget.get_active(): + window.show() + else: + window.hide() + + def _on_history_combo_changed(self, widget, data = None): + """Gtk+ callback""" + model = widget.get_model() + iter = widget.get_active_iter() + if iter is None: + return + + # Get the move number + moveNumber = model.get_value(iter, 0) + + string = 'Show move number: ' + str(moveNumber) + if moveNumber == len(model) - 1: + string += ' (latest)' + moveNumber = -1 + + view = self.notebook.getView() + if view is not None: + view._setMoveNumber(moveNumber) + + def __selectMoveNumber(self, moveNumber): + """FIXME + """ + combo = self.__getWidget('history_combo') + + # Limit moves to the maximum value + maxNumber = len(combo.get_model()) + + # Allow negative indexing + if moveNumber < 0: + moveNumber = maxNumber + moveNumber + if moveNumber < 0: + moveNumber = 0 + if moveNumber >= maxNumber: + moveNumber = maxNumber - 1 + + combo.set_active(moveNumber) + + def __selectMoveNumberRelative(self, offset): + """FIXME + """ + combo = self.__getWidget('history_combo') + selected = combo.get_active() + maxNumber = len(combo.get_model()) + new = selected + offset + if new < 0: + new = 0 + elif new >= maxNumber: + new = maxNumber - 1 + self.__selectMoveNumber(new) + + def _on_history_start_clicked(self, widget, data = None): + """Gtk+ callback""" + self.__selectMoveNumber(0) + + def _on_history_previous_clicked(self, widget, data = None): + """Gtk+ callback""" + self.__selectMoveNumberRelative(-1) + + def _on_history_next_clicked(self, widget, data = None): + """Gtk+ callback""" + self.__selectMoveNumberRelative(1) + + def _on_history_latest_clicked(self, widget, data = None): + """Gtk+ callback""" + self.__selectMoveNumber(-1) + + def _on_view_changed(self, widget, page, pageNum, data = None): + """Gtk+ callback""" + self._updateViewButtons() + + def _updateViewButtons(self): + """ + """ + view = self.notebook.getView() + enableWidgets = (view is not None) and view.isActive + self.__getWidget('end_game_button').set_sensitive(enableWidgets) + self.__getWidget('save_game_button').set_sensitive(enableWidgets) + self.__getWidget('menu_save_item').set_sensitive(enableWidgets) + self.__getWidget('menu_end_game_item').set_sensitive(enableWidgets) + + combo = self.__getWidget('history_combo') + if view is None: + combo.set_model(None) + else: + (model, selected) = view._getModel() + combo.set_model(model) + if selected < 0: + selected = len(model) + selected + combo.set_active(selected) + self.__getWidget('navigation_box').set_sensitive(enableWidgets) + + def _on_new_game_button_clicked(self, widget, data = None): + """Gtk+ callback""" + + dialogs.GtkNewGameDialog(self, self.__playerModel, whiteAI = self.__defaultWhiteAI, blackAI = self.__defaultBlackAI) + + def _on_join_game_button_clicked(self, widget, data = None): + """Gtk+ callback""" + # Create the dialog + dialog = dialogs.GtkJoinGameDialog(self, self.__playerModel) + self.__joinGameDialogs.append(dialog) + # FIXME: Remove from this list when they dissapear + + # Add the detected games into the dialog + for (game, name) in self.__networkGames.iteritems(): + dialog.addNetworkGame(name, game) + + def _on_open_game_button_clicked(self, widget, data = None): + """Gtk+ callback""" + dialogs.GtkLoadGameDialog(self) + + def _on_save_game_button_clicked(self, widget, data = None): + """Gtk+ callback""" + view = self.notebook.getView() + if not self.__saveGameDialogs.has_key(view): + self.__saveGameDialogs[view] = dialogs.GtkSaveGameDialog(self, view) + + def _on_end_game_button_clicked(self, widget, data = None): + """Gtk+ callback""" + view = self.notebook.getView() + assert(view is not None) + if view.feedback is not None: + view.feedback.close() + + def _on_help_clicked(self, widget, data = None): + """Gtk+ callback""" + gnome.help_display('glchess') + + + def _on_about_clicked(self, widget, data = None): + """Gtk+ callback""" + if self.__aboutDialog is None: + self.__aboutDialog = loadGladeFile('about.glade', domain = 'glchess') + self.__aboutDialog.signal_autoconnect(self) + + def _on_glchess_about_dialog_close(self, widget, data = None): + """Gtk+ callback""" + self.__aboutDialog = None + + def _on_ai_window_delete_event(self, widget, data = None): + """Gtk+ callback""" + self._gui.get_widget('menu_view_ai').set_active(False) + + # Stop the event - the window will be closed by the menu event + return True + + def _on_quit(self, widget, data = None): + """Gtk+ callback""" + self.onQuit() + +if __name__ == '__main__': + ui = GtkUI() + ui.run() diff --git a/src/lib/main.py b/src/lib/main.py new file mode 100644 index 0000000..b0fa31d --- /dev/null +++ b/src/lib/main.py @@ -0,0 +1,992 @@ +""" +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +__all__ = ['Application'] + +import sys +import os +import gettext +import traceback + +import ui +import gtkui +import scene.cairo +import scene.opengl +import scene.human +import game +import chess.board +import chess.lan +import ai + +#import dbus.glib +#import network + +import chess.pgn + +class Config: + """ + """ + __directory = None + + def __init__(self): + """Constructor for a confgiuration object""" + self.__directory = os.path.expanduser('~/.glchess') + + # Create the directory if it does not exist + if not os.path.exists(self.__directory): + os.mkdir(self.__directory) + else: + assert(os.path.isdir(self.__directory)) + + def getAutosavePath(self): + """Get the path to the autosave file""" + return self.__directory + '/autosave.pgn' + +class MovePlayer(game.ChessPlayer): + """This class provides a pseudo-player to watch for piece movements""" + # The game to control + __game = None + + # A dictionary of pieces added into the scene + __pieces = None + + def __init__(self, chessGame): + """Constructor for a move player. + + 'chessGame' is the game to make changes to (ChessGame). + """ + self.__game = chessGame + game.ChessPlayer.__init__(self, '@move') + + # Extended methods + + def onPieceMoved(self, piece, start, end): + """Called by chess.board.ChessPlayer""" + self.__game.scene._movePiece(piece, start, end) + self.__game.cairoScene._movePiece(piece, start, end) + + def onPlayerMoved(self, player, move): + """Called by chess.board.ChessPlayer""" + self.__game._onPlayerMoved(player, move) + +class HumanPlayer(game.ChessPlayer): + """ + """ + __game = None + + def __init__(self, chessGame, name): + """Constructor. + + 'chessGame' is the game this player is in (game.ChessGame). + 'name' is the name of this player (string). + """ + game.ChessPlayer.__init__(self, name) + self.__game = chessGame + + def readyToMove(self): + # FIXME: ??? + self.__game.scene.setHumanPlayer(self) + self.__game.cairoScene.setHumanPlayer(self) + +class AIPlayer(ai.Player): + """ + """ + + def __init__(self, application, name, profile, description): + """ + """ + self.window = application.ui.addAIWindow(profile.name, profile.path, description) + ai.Player.__init__(self, name, profile) + + def logText(self, text, style): + """Called by ai.Player""" + self.window.addText(text, style) + +class SceneCairo(scene.cairo.Scene, scene.human.SceneHumanInput): + """ + """ + # The game this scene is rendering + __game = None + + # TODO + __moveNumber = -1 + __pieceModels = None + + # The current human player or None if not a player in play + __humanPlayer = None + + def __init__(self, chessGame): + """ + """ + self.__game = chessGame + self.__pieceModels = {} + + # Call parent constructors + scene.human.SceneHumanInput.__init__(self) + scene.cairo.Scene.__init__(self) + + def setHumanPlayer(self, player): + """TODO + """ + self.__humanPlayer = player + + # Animate the board + if player is self.__game.getWhite(): + self.setBoardRotation(0.0) + elif player is self.__game.getBlack(): + self.setBoardRotation(180.0) + else: + assert(False), 'Human player is not white or black' + + def setMoveNumber(self, moveNumber): + """Set the move number to watch. + + 'moveNumber' is the move to watch (integer). + """ + if self.__moveNumber == moveNumber: + return + self.__moveNumber = moveNumber + + # Lock the scene if not tracking the game + self.enableHumanInput(moveNumber == -1) + + # Get the state of this scene + piecesByLocation = self.__game.getAlivePieces(moveNumber) + + # Remove any models not present + requiredPieces = piecesByLocation.values() + for (piece, model) in self.__pieceModels.items(): + try: + requiredPieces.index(piece) + except ValueError: + self.__pieceModels.pop(piece) + self.removeChessPiece(model) + + # Move the models in the scene + for (location, piece) in piecesByLocation.iteritems(): + self.__movePiece(piece, location) + + def _movePiece(self, piece, start, end): + """TODO + """ + # Only allow then watching the active game + if self.__moveNumber == -1: + self.__movePiece(piece, end) + + def __movePiece(self, piece, location): + """ + """ + # Get the model for this piece creating one if it doesn't exist + try: + model = self.__pieceModels[piece] + except KeyError: + # No need to create if didn't exist anyway + if location is None: + return + + # Make the new model + pieceName = {chess.board.PAWN: 'pawn', chess.board.ROOK: 'rook', chess.board.KNIGHT: 'knight', + chess.board.BISHOP: 'bishop', chess.board.QUEEN: 'queen', chess.board.KING: 'king'}[piece.getType()] + chessSet = {chess.board.WHITE: 'white', chess.board.BLACK: 'black'}[piece.getColour()] + model = self.addChessPiece(chessSet, pieceName, location) + self.__pieceModels[piece] = model + + # Delete or move the model + if location is None: + self.__pieceModels.pop(piece) + self.removeChessPiece(model) + else: + model.move(location) + + # Extended methods + + def onRedraw(self): + """Called by scene.cairo.Scene""" + if self.__game.view.activeScene is self and self.__game.view is not None: + self.__game.view.controller.render() + + def startAnimation(self): + """Called by scene.cairo.Scene""" + self.__game.application.ui.startAnimation() + + def playerIsHuman(self): + """Called by scene.human.SceneHumanInput""" + return self.__humanPlayer is not None + + def squareIsFriendly(self, coord): + """Called by scene.human.SceneHumanInput""" + owner = self.__game.getSquareOwner(coord) + if owner is None: + return False + return owner is self.__humanPlayer + + def canMove(self, start, end): + """Called by scene.human.SceneHumanInput""" + if self.__humanPlayer is None: + return False + + return self.__humanPlayer.canMove(start, end) # FIXME: Promotion type + + def moveHuman(self, start, end): + """Called by scene.human.SceneHumanInput""" + player = self.__humanPlayer + self.__humanPlayer = None + if player is self.__game.getWhite(): + colour = chess.board.WHITE + else: + colour = chess.board.BLACK + move = chess.lan.encode(colour, start, end, promotionType = chess.board.QUEEN) # FIXME: Promotion type + player.move(move) + +class SceneOpenGL(scene.opengl.Scene, scene.human.SceneHumanInput): + """ + """ + # The game this scene is rendering + __game = None + + # TODO + __moveNumber = -1 + __pieceModels = None + + # The current human player or None if not a player in play + __humanPlayer = None + + def __init__(self, chessGame): + """Constructor for a glChess scene. + + 'chessGame' is the game the scene is rendering (game.ChessGame). + """ + self.__game = chessGame + self.__pieceModels = {} + + # Call parent constructors + scene.human.SceneHumanInput.__init__(self) + scene.opengl.Scene.__init__(self) + + def setHumanPlayer(self, player): + """TODO + """ + self.__humanPlayer = player + + # Animate the board + if player is self.__game.getWhite(): + self.setBoardRotation(0.0) + elif player is self.__game.getBlack(): + self.setBoardRotation(180.0) + else: + assert(False), 'Human player is not white or black' + + def setMoveNumber(self, moveNumber): + """Set the move number to watch. + + 'moveNumber' is the move to watch (integer). + """ + if self.__moveNumber == moveNumber: + return + self.__moveNumber = moveNumber + + # Lock the scene if not tracking the game + self.enableHumanInput(moveNumber == -1) + + # Get the state of this scene + piecesByLocation = self.__game.getAlivePieces(moveNumber) + + # Remove any models not present + requiredPieces = piecesByLocation.values() + for (piece, model) in self.__pieceModels.items(): + try: + requiredPieces.index(piece) + except ValueError: + self.__pieceModels.pop(piece) + self.removeChessPiece(model) + + # Move the models in the scene + for (location, piece) in piecesByLocation.iteritems(): + self.__movePiece(piece, location) + + def _movePiece(self, piece, start, end): + """TODO + """ + # Ignore if not watching the active game + if self.__moveNumber != -1: + return + + self.__movePiece(piece, end) + + def __movePiece(self, piece, location): + """ + """ + # Get the model for this piece creating one if it doesn't exist + try: + model = self.__pieceModels[piece] + except KeyError: + # No need to create if didn't exist anyway + if location is None: + return + + # Make the new model + pieceName = {chess.board.PAWN: 'pawn', chess.board.ROOK: 'rook', chess.board.KNIGHT: 'knight', + chess.board.BISHOP: 'bishop', chess.board.QUEEN: 'queen', chess.board.KING: 'king'}[piece.getType()] + chessSet = {chess.board.WHITE: 'white', chess.board.BLACK: 'black'}[piece.getColour()] + model = self.addChessPiece(chessSet, pieceName, location) + self.__pieceModels[piece] = model + + # Delete or move the model + if location is None: + self.__pieceModels.pop(piece) + self.removeChessPiece(model) + else: + model.move(location) + + # Extended methods + + def onRedraw(self): + """Called by scene.opengl.Scene""" + if self.__game.view.activeScene is self and self.__game.view is not None: + self.__game.view.controller.render() + + def startAnimation(self): + """Called by scene.opengl.Scene""" + self.__game.application.ui.startAnimation() + + def playerIsHuman(self): + """Called by scene.human.SceneHumanInput""" + return self.__humanPlayer is not None + + def squareIsFriendly(self, coord): + """Called by scene.human.SceneHumanInput""" + owner = self.__game.getSquareOwner(coord) + if owner is None: + return False + return owner is self.__humanPlayer + + def canMove(self, start, end): + """Called by scene.human.SceneHumanInput""" + if self.__humanPlayer is None: + return False + + return self.__humanPlayer.canMove(start, end) # FIXME: Promotion type + + def moveHuman(self, start, end): + """Called by scene.human.SceneHumanInput""" + player = self.__humanPlayer + self.__humanPlayer = None + if player is self.__game.getWhite(): + colour = chess.board.WHITE + else: + colour = chess.board.BLACK + move = chess.lan.encode(colour, start, end, promotionType = chess.board.QUEEN) # FIXME: Promotion type + player.move(move) + +class Splashscreen(ui.ViewFeedback): + """ + """ + application = None + scene = None + + def __init__(self, application): + """Constructor. + + 'application' is ??? + """ + self.application = application + self.cairoScene = scene.cairo.Scene() + self.scene = scene.opengl.Scene() + + def renderGL(self): + """Called by ui.ViewFeedback""" + self.scene.render() + + def renderCairoStatic(self, context): + """Called by ui.ViewFeedback""" + return self.cairoScene.renderStatic(context) + + def renderCairoDynamic(self, context): + """Called by ui.ViewFeedback""" + self.cairoScene.renderDynamic(context) + + def reshape(self, width, height): + """Called by ui.View""" + self.scene.reshape(width, height) + self.cairoScene.reshape(width, height) + +class View(ui.ViewFeedback): + """ + """ + # The game this view is rendering + game = None + + # TEMP: The scene to render (switches between OpenGL and Cairo). + activeScene = None + + # The controller object for this view + controller = None + + def __init__(self, game): + """Constructor. + + 'game' is ??? + """ + self.game = game + + def renderGL(self): + """Called by ui.ViewFeedback""" + self.activeScene = self.game.scene + self.activeScene.render() + + def renderCairoStatic(self, context): + """Called by ui.ViewFeedback""" + self.activeScene = self.game.cairoScene + return self.activeScene.renderStatic(context) + + def renderCairoDynamic(self, context): + """Called by ui.ViewFeedback""" + self.activeScene.renderDynamic(context) + + def reshape(self, width, height): + """Called by ui.ViewFeedback""" + self.game.scene.reshape(width, height) + self.game.cairoScene.reshape(width, height) + + def select(self, x, y): + """Called by ui.ViewFeedback""" + self.activeScene.select(x, y) + + def deselect(self, x, y): + """Called by ui.ViewFeedback""" + self.activeScene.deselect(x, y) + + def setMoveNumber(self, moveNumber): + """Called by ui.ViewFeedback""" + self.game.scene.setMoveNumber(moveNumber) + self.game.cairoScene.setMoveNumber(moveNumber) + + def save(self, filename): + """Called by ui.ViewFeedback""" + try: + f = file(filename, 'w') + except IOError, e: + self.reportError('Unable to save PGN file ' + filename, str(e)) + return + + pgnGame = chess.pgn.PGNGame() + self.game.toPGN(pgnGame) + + lines = pgnGame.getLines() + for line in lines: + f.write(line + '\n') + f.write('\n') + f.close() + + def close(self): + """Called by ui.ViewFeedback""" + # The user requests the game to end, for now we just do it + self.game.remove() + +class ChessGame(game.ChessGame): + """ + """ + # Link back to the main application + application = None + + # The name of the game + __name = None + + # The scene for this game + scene = None + + # The view watching this scene + view = None + + # The players in the game + __movePlayer = None + __aiPlayers = None + __humanPlayers = None + + def __init__(self, application, name): + """Constructor for a chess game. + + 'application' is a reference to the glChess application. + 'name' is the name of the game (string). + """ + self.application = application + self.__name = name + self.__aiPlayers = [] + self.__humanPlayers = [] + + # Call parent constructor + game.ChessGame.__init__(self) + + # Create a scene to render to + self.scene = SceneOpenGL(self) + self.cairoScene = SceneCairo(self) + self.view = View(self) + self.view.controller = application.ui.addView(name, self.view) + + # Watch for piece moves with a player + self.__movePlayer = MovePlayer(self) + self.addSpectator(self.__movePlayer) + + def getName(self): + """Returns the name of the game (string)""" + return self.__name + + def addAIPlayer(self, name, profile): + """Create an AI player. + + 'name' is the name of the player to create (string). + 'profile' is the the AI profile to use (ai.Profile). + + Returns an AI player to use (game.ChessPlayer). + """ + description = "'" + name + "' in '" + self.__name + "'" + player = AIPlayer(self.application, name, profile, description) + self.__aiPlayers.append(player) + self.application.watchAIPlayer(player) + return player + + def addHumanPlayer(self, name): + """Create a human player. + + 'name' is the name of the player to create. + + Returns a human player to use (game.ChessPlayer). + """ + player = HumanPlayer(self, name) + self.__humanPlayers.append(player) + return player + + def playerIsHuman(self, player): + """Test if a player is human. + + 'player' is the player to check (game.ChessPlayer). + + Returns True if this is a human player in this game otherwise False. + """ + try: + if self.__humanPlayers.index(player) < 0: + return False + else: + return True + except ValueError: + return False + + def toPGN(self, pgnGame): + """Write the properties of this game into a PGN game. + + 'pgnGame' is the game to write into (pgn.PGNGame). All the tags should be unset. + """ + white = self.getWhite() + black = self.getBlack() + + pgnGame.setTag(pgnGame.PGN_TAG_EVENT, self.getName()) + pgnGame.setTag(pgnGame.PGN_TAG_WHITE, white.getName()) + pgnGame.setTag(pgnGame.PGN_TAG_BLACK, black.getName()) + + if isinstance(white, ai.Player): + pgnGame.setTag('WhiteAI', white.getProfile().name) + if isinstance(black, ai.Player): + pgnGame.setTag('BlackAI', black.getProfile().name) + + moves = self.getMoves() + while len(moves) > 0: + if len(moves) == 1: + pgnGame.addMove(moves[0].sanMove, None) + break + else: + pgnGame.addMove(moves[0].sanMove, moves[1].sanMove) + moves = moves[2:] + + def animate(self, timeStep): + """ + """ + animating1 = self.scene.animate(timeStep) + animating2 = self.cairoScene.animate(timeStep) + return animating1 or animating2 + + def remove(self): + """Remove this game""" + # Remove AI player windows + for player in self.__aiPlayers: + player.window.close() + self.application.unwatchAIPlayer(player) + + # End the game + self.end() + + # Remove the game from the UI + self.application._removeGame(self) + self.view.controller.close() + + # Private methods + + def _onPlayerMoved(self, player, move): + """FIXME: Rename this + """ + self.view.controller.addMove(move) + +class UI(gtkui.GtkUI): + """ + """ + __application = None + + splashscreen = None + + def __init__(self, application): + """ + """ + self.__application = application + gtkui.GtkUI.__init__(self) + + self.splashscreen = Splashscreen(self) + self.setDefaultView(self.splashscreen) + + def onAnimate(self, timeStep): + """Called by UI""" + return self.__application.animate(timeStep) + + def onReadFileDescriptor(self, fd): + """Called by UI""" + try: + player = self.__application.aiPlayers[fd] + except KeyError: + return False + else: + player.read() + return True + + def onGameStart(self, gameName, allowSpectators, whiteName, whiteType, blackName, blackType, moves = None): + """Called by UI""" + g = self.__application.addGame(gameName, whiteName, whiteType, blackName, blackType) + print 'Starting game ' + gameName + ' between ' + whiteName + '(' + str(whiteType) + ') and ' + blackName + '(' + str(blackType) + ')' + if moves: + g.start(moves) + else: + g.start() + + def loadGame(self, path, returnResult): + """Called by ui.UI""" + try: + p = chess.pgn.PGN(path, 1) + except chess.pgn.Error, e: + self.reportError('Unable to open PGN file ' + path, str(e)) + return + + # Use the first game + pgnGame = p[0] + + if returnResult is True: + whiteAI = pgnGame.getTag('WhiteAI') + blackAI = pgnGame.getTag('BlackAI') + msg = '' + if whiteAI and self.__application.getAIProfile(whiteAI) is None: + msg += "AI '" + whiteAI + "' is not installed, white player is now human" + whiteAI = None + if blackAI and self.__application.getAIProfile(blackAI) is None: + if msg != '': + msg += '\n' + msg += "AI '" + blackAI + "' is not installed, black player is now human" + blackAI = None + + self.reportGameLoaded(gameName = pgnGame.getTag(pgnGame.PGN_TAG_EVENT), + whiteName = pgnGame.getTag(pgnGame.PGN_TAG_WHITE), + blackName = pgnGame.getTag(pgnGame.PGN_TAG_BLACK), + whiteAI = whiteAI, blackAI = blackAI, + moves = pgnGame.getMoves()) + + if len(msg) > 0: + self.reportError('Game modified', msg) + else: + g = self.__application.addPGNGame(pgnGame) + moves = pgnGame.getMoves() + if moves: + g.start(moves) + else: + g.start() + + def onGameJoin(self, localName, localType, game): + """Called by UI""" + print 'Joining game ' + str(game) + ' as ' + localName + '(' + str(localType) + ')' + + def onQuit(self): + """Called by UI""" + self.__application.quit() + +#class GameDetector(network.GameDetector): +# """ +# """ +# def __init__(self, app): +# """ +# """ +# self.__app = app +# network.GameDetector.__init__(self) +# +# def onGameDetected(self, game): +# """Called by network.GameDetector""" +# self.__app.ui.addNetworkGame(game.name, game) +# +# def onGameRemoved(self, game): +# """Called by network.GameDetector""" +# self.__app.ui.removeNetworkGame(game) + +class Application: + """ + """ + # The configuration + __config = None + + # The glChess UI + ui = None + + # The AI types + __aiProfiles = None + + # AI players keyed by file descriptor + aiPlayers = None + + # The network game detector + __detector = None + + # The games present + __games = None + + def __init__(self): + """Constructor for glChess application""" + self.__aiProfiles = {} + self.__games = [] + self.aiPlayers = {} + + self.__config = Config() + + self.__detector = None#GameDetector(self) + + self.ui = UI(self) + + def addAIProfile(self, profile): + """Add a new AI profile into glChess. + + 'profile' is the profile to add (ai.Profile). + """ + name = profile.name + assert(self.__aiProfiles.has_key(name) is False) + self.__aiProfiles[name] = profile + self.ui.addAIEngine(name) + + def getAIProfile(self, name): + """Get an installed AI profile. + + 'name' is the name of the profile to get (string). + + Return the profile (ai.Profile) or None if it does not exist. + """ + try: + return self.__aiProfiles[name] + except KeyError: + return None + + def watchAIPlayer(self, player): + """ + """ + self.aiPlayers[player.fileno()] = player + self.ui.watchFileDescriptor(player.fileno()) + + def unwatchAIPlayer(self, player): + """ + """ + self.aiPlayers.pop(player.fileno()) + + def addGame(self, name, whiteName, whiteType, blackName, blackType): + """Add a chess game into glChess. + + 'name' is the name of the game (string). + 'whiteName' is the name of the white player (string). + 'whiteType' is the AI profile to use for white (string) or None if white is human. + 'blackName' is the name of the black player (string). + 'blackType' is the AI profile to use for black (string) or None if black is human. + + Returns the game object. Use game.start() to start the game. + """ + # Create the game + g = ChessGame(self, name) + self.__games.append(g) + + msg = '' + if whiteType is None: + player = g.addHumanPlayer(whiteName) + else: + try: + profile = self.__aiProfiles[whiteType] + player = g.addAIPlayer(whiteName, profile) + except KeyError: + msg += "AI '" + whiteType + "' is not installed, white player is now human" + player = g.addHumanPlayer(whiteName) + g.setWhite(player) + + if blackType is None: + player = g.addHumanPlayer(blackName) + else: + try: + profile = self.__aiProfiles[blackType] + player = g.addAIPlayer(blackName, profile) + except KeyError: + msg += "AI '" + blackType + "' is not installed, black player is now human" + player = g.addHumanPlayer(blackName) + g.setBlack(player) + + if len(msg) > 0: + self.ui.reportError('Game modified', msg) + + return g + + def addPGNGame(self, pgnGame): + """Add a PGN game. + + 'pgnGame' is the game to add (chess.pgn.PGNGame). + + Returns the game object. Use game.start() to start the game. + """ + return self.addGame(pgnGame.getTag(pgnGame.PGN_TAG_EVENT), + pgnGame.getTag(pgnGame.PGN_TAG_WHITE), + pgnGame.getTag('WhiteAI'), + pgnGame.getTag(pgnGame.PGN_TAG_BLACK), + pgnGame.getTag('BlackAI')) + + def addMove(self, view, move): + # TEMP + self.ui.addMove(view, move) + + def start(self): + """Run glChess. + + This method does not return. + """ + print 'This is glChess 1.0RC1' + + # Load AI profiles + profiles = ai.loadProfiles() + + for p in profiles: + p.detect() + if p.path is not None: + print 'Detected AI: ' + p.name + ' at ' + p.path + self.addAIProfile(p) + + nArgs = len(sys.argv) + + # Load existing games + if nArgs == 1: + self.__autoload() + + # Load requested game + # TODO: Merge this with the UI requested load games + elif nArgs == 2: + path = sys.argv[1] + try: + p = chess.pgn.PGN(path, 1) + except chess.pgn.Error, e: + # TODO: Pop-up dialog + print e + else: + # Use the first game + pgnGame = p[0] + g = self.addPGNGame(pgnGame) + moves = pgnGame.getMoves() + if moves: + g.start(moves) + else: + g.start() + else: + print 'Usage: ' + sys.argv[0] + ' [PGN file]' + sys.exit() + + # Start UI (does not return) + try: + self.ui.run() + except: + print 'glChess has crashed. Please report this bug to http://glchess.sourceforge.net' + print 'Debug output:' + print traceback.format_exc() + self.quit() + sys.exit(1) + + def animate(self, timeStep): + """ + """ + animating = False + for g in self.__games: + if g.animate(timeStep): + animating = True + return animating + + def quit(self): + """Quit glChess""" + # Save any open games + self.__autosave() + + # End current games (will delete AIs etc) + for game in self.__games[:]: + game.end() + + # Exit the application + sys.exit() + + # Private methods + + def _removeGame(self, g): + """ + """ + self.__games.remove(g) + + def __autoload(self): + """Restore games from the autosave file""" + path = self.__config.getAutosavePath() + print 'Auto-loading from ' + path + '...' + + try: + p = chess.pgn.PGN(path) + games = p[:] + except chess.pgn.Error, e: + print 'Invalid autoload file ' + path + ': ' + str(e) + games = [] + except IOError, e: + print 'Unable to autoload from ' + path + ': ' + str(e) + games = [] + + # Delete the file once loaded + try: + os.unlink(path) + except OSError: + pass + + # Restore each game + for pgnGame in games: + g = self.addPGNGame(pgnGame) + g.start(pgnGame.getMoves()) + + def __autosave(self): + """Save any open games to the autosave file""" + if len(self.__games) == 0: + return + + fname = self.__config.getAutosavePath() + print 'Auto-saving to ' + fname + '...' + + f = file(fname, 'a') + for g in self.__games: + pgnGame = chess.pgn.PGNGame() + g.toPGN(pgnGame) + + lines = pgnGame.getLines() + for line in lines: + f.write(line + '\n') + f.write('\n') + f.close() + +if __name__ == '__main__': + gettext.textdomain('gnome-games') + app = Application() + app.start() diff --git a/src/lib/network/Makefile.am b/src/lib/network/Makefile.am new file mode 100644 index 0000000..d5d692e --- /dev/null +++ b/src/lib/network/Makefile.am @@ -0,0 +1,5 @@ +glchessdir = $(pythondir)/glchess/network +glchess_PYTHON = \ + announce.py \ + __init__.py \ + protocol.py diff --git a/src/lib/network/__init__.py b/src/lib/network/__init__.py new file mode 100644 index 0000000..59bd0a8 --- /dev/null +++ b/src/lib/network/__init__.py @@ -0,0 +1,2 @@ +from protocol import * +from announce import * diff --git a/src/lib/network/announce.py b/src/lib/network/announce.py new file mode 100644 index 0000000..194ff68 --- /dev/null +++ b/src/lib/network/announce.py @@ -0,0 +1,127 @@ +""" +""" + +#avahi = __import__('avahi') +#dbus = __import__('dbus') +import avahi +import dbus + +class RemoteGame: + """ + """ + + def __init__(self, name, address): + """ + """ + self.name = name + self.address = address + +class GameReporter: + """ + """ + + def __init__(self, name, port): + """Constructor. + + 'name' is the name of the game started (string). + 'port' is the UDP/IP port the game is running on (integer). + """ + self.name = name + self.port = port + + # Connect to the Avahi server + bus = dbus.SystemBus() + self.server = dbus.Interface(bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + + # Register this service + path = self.server.EntryGroupNew() + group = dbus.Interface(bus.get_object(avahi.DBUS_NAME, path), avahi.DBUS_INTERFACE_ENTRY_GROUP) + n = name + index = 1 + while True: + try: + group.AddService(avahi.IF_UNSPEC, avahi.PROTO_INET, 0, n, '_glchess._udp', '', '', port, avahi.string_array_to_txt_array(['hi=gi'])) + except dbus.dbus_bindings.DBusException: + index += 1 + n = name + ' (' + str(index) + ')' + else: + break + group.Commit() + +class GameDetector: + """Class to detect glChess network games. + """ + # The known about games + __games = None + __gamesByName = None + + def __init__(self): + """Constructor""" + self.__games = [] + self.__gamesByName = {} + + # Connect to the Avahi server + bus = dbus.SystemBus() + self.server = dbus.Interface(bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + + # Listen for glChess servers + # FIXME: Can raise a dbus_bindings.DBusException (local name collision) + browser = self.server.ServiceBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_INET, '_glchess._udp', '', 0) + listener = dbus.Interface(bus.get_object(avahi.DBUS_NAME, browser), avahi.DBUS_INTERFACE_SERVICE_BROWSER) + listener.connect_to_signal('ItemNew', self.__serviceDetected) + listener.connect_to_signal('ItemRemove', self.__serviceRemoved) + + # Methods to extend + + def onGameDetected(self, game): + """Called when a game is detected. + + 'game' is the game that has been detected (RemoteGame). + """ + pass + + def onGameRemoved(self, game): + """Called when a game is removed. + + 'game' is the game that has been removed (RemoteGame). + """ + pass + + # Public methods + + def getGames(self): + """Returns a list of games that are known of""" + return self.__games[:] + + # Private methods + + def __serviceDetected(self, interface, protocol, name, stype, domain, flags): + """D-Dbus callback""" + # Get information on this service + self.server.ResolveService(interface, protocol, name, stype, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), + reply_handler = self.__serverResolved, error_handler = self.__resolveError) + + def __serverResolved(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + """D-Dbus callback""" + #print avahi.txt_array_to_string_array(txt) + print 'Game detected: ' + str(address) + ':' + str(port) + ' (' + name + ')' + + assert(not self.__gamesByName.has_key(name)) + + game = RemoteGame(name, (address, port)) + self.__gamesByName[name] = game + self.__games.append(game) + + self.onGameDetected(game) + + def __resolveError(self, error): + """D-Dbus callback""" + print 'Avahi/D-Bus error: ' + repr(error) + + def __serviceRemoved(self, interface, protocol, name, stype, domain, flags): + """D-Dbus callback""" + print 'Game removed: ' + str(name) + game = self.__gamesByName.pop(name) + self.__games.remove(game) + + self.onGameRemoved(game) diff --git a/src/lib/network/protocol.py b/src/lib/network/protocol.py new file mode 100644 index 0000000..4e39514 --- /dev/null +++ b/src/lib/network/protocol.py @@ -0,0 +1,352 @@ +""" +""" + +def decodeMessage(data): + """ + """ + fields = [] + + lines = data.split('\n') + (sequenceNumber, messageType) = lines[0].split(' ') + try: + sequenceNumber = int(sequenceNumber) + except ValueError: + sequenceNumber = None + + fieldName = None + fieldValue = None + for line in lines[1:]: + (name, value) = line.split('=', 1) + if name == '~': + assert(fieldName is not None) + fieldValue += '\n' + value + else: + if fieldName is not None: + fields.append((fieldName, fieldValue)) + + fieldName = name + fieldValue = value + + if fieldName is not None: + fields.append((fieldName, fieldValue)) + + return (sequenceNumber, messageType, fields) + +def encodeMessage(sequenceNumber, messageType, fields): + """ + """ + data = str(sequenceNumber) + ' ' + messageType + '\n' + + for (name, value) in fields: + data += name + '=' + lines = value.split('\n') + data += lines[0] + for line in lines[1:]: + data += '\n~=' + line + data += '\n' + + return data[:-1] + +class OutgoingMessage: + """ + """ + + sequenceNumber = None + data = None + + def __init__(self, sequenceNumber, data): + """ + """ + self.sequenceNumber = sequenceNumber + self.data = data + +class Sender: + """ + """ + + # Sequence number to mark message with + __sequenceNumber = 0 + + # Messages waiting to be sent + __queuedMessages = None + + # Timout in seconds sending a message + TIMEOUT = 1.0 + + # Number of times to send a message until assuming the receiver has died + RETRIES = 3 + + def __init__(self): + """Constructor""" + self.__queuedMessages = [] + + # Methods to extend + + def onOutgoingMessage(self, data): + """Called when a message is generated to be sent. + + 'data' is the raw message to send (string). + """ + pass + + def startTimer(self): + """Start the message retry timer. + + If the timer expires call retryMessage(). + """ + pass + + def stopTimer(): + """Stop the retry timer""" + pass + + # Public methods + + def sendAcknowledge(self, sequenceNumber): + """Send an acknowledge message. + + 'sequenceNumber' is the sequence number of the message being acknowledged (int). + """ + d = encodeMessage('*', 'ACK', [('seq', str(sequenceNumber))]) + self.onOutgoingMessage(d) + + def queueMessage(self, messageType, fields = []): + """Queue a message for sending. + + 'messageType' is the type of message (string). + 'fields' is a list of containing (name, value) pairs for message fields. + """ + # Encode with the message header + self.__sequenceNumber = (self.__sequenceNumber + 1) % 1000 + data = encodeMessage(self.__sequenceNumber, messageType, fields) + + self.__queuedMessages.append(OutgoingMessage(self.__sequenceNumber, data)) + + # Send if no other queued messages + if len(self.__queuedMessages) == 1: + self.startTimer() + self.onOutgoingMessage(data) + + def acknowledgeMessage(self, sequenceNumber): + """Confirm a message has been received at the far end. + + 'sequenceNumber' is the sequence number that has been acknowledged (int). + """ + # If this matches the last message sent then remove it from the message queue + if len(self.__queuedMessages) == 0: + return + if sequenceNumber != self.__queuedMessages[0].sequenceNumber: + return + self.__queuedMessages = self.__queuedMessages[1:] + + # Send the next message + if len(self.__queuedMessages) > 0: + self.startTimer() + self.onOutgoingMessage(self.__queuedMessages[0].data) + else: + self.stopTimer() + + def retryMessage(self): + """Resend the last message""" + try: + data = self.__queuedMessages[0].data + except IndexError: + return + self.startTimer() + self.onOutgoingMessage(data) + +class Receiver: + """ + """ + # Expected sequence number to receive + __expectedSequenceNumber = None + + __processing = False + __queue = None + + def __init__(self): + """Constructor""" + self.__queue = [] + + # Methods to extend + + def processMessage(self, messageType, fields): + """Called when a message is available for processing. + + 'messageType' is the message type (string). + 'fields' is a dictionary of field values keyed by field name. + """ + pass + + # Public methods + + def filterMessage(self, sequenceNumber, messageType, fields): + """Check the header fields on a message. + + 'sequenceNumber' is the sequence number of the incoming message (int). + 'messageType' is the type of message (string). + 'fields' is a dictionary of field values keyed by field name. + + processMessage() is called if this message has the correct sequence number. + """ + # Stop recursion + if self.__processing: + self.__queue.append((sequenceNumber, messageType, fields)) + return + + # Check sequence number matches + if sequenceNumber is not None: + expected = self.__expectedSequenceNumber + if expected is not None and sequenceNumber != expected: + return + self.__expectedSequenceNumber = (sequenceNumber + 1) % 1000 + + # Pass to higher level + self.__processing = True + self.processMessage(messageType, fields) + self.__processing = False + + # Process any messages received while in the callback + while len(self.__queue) > 0: + (sequenceNumber, messageType, fields) = self.__queue[0] + self.__queue = self.__queue[1:] + self.filterMessage(sequenceNumber, messageType, fields) + +class StateMachine(Receiver, Sender): + """ + """ + + def __init__(self): + """Constructor""" + Sender.__init__(self) + Receiver.__init__(self) + + # Methods to extend + + def onOutgoingMessage(self, message): + """Called when a message is generated to send. + + 'message' is the message to send (string). + """ + pass + + def processMessage(self, messageType, fields): + """Called when a message is available for processing. + + 'messageType' is the message type (string). + 'fields' is a dictionary of field values keyed by field name. + """ + pass + + # Public methods + + def sendMessage(self, messageType, fields): + """Send a message. + + 'messageType' is the message type to send (string made up of A-Z). + 'fields' is a list of 2-tuples containing field names and values (strings). + """ + self.queueMessage(messageType, fields) + + def registerIncomingMessage(self, message): + """Register a received message. + + 'message' is the raw received message (string). + """ + (sequenceNumber, messageType, fields) = decodeMessage(message) + fields = dict(fields) + + # FIXME: 'seq' could be an invalid integer + if messageType == 'ACK': + try: + seq = int(fields['seq']) + except (KeyError, ValueError): + return + self.acknowledgeMessage(seq) + return + + # Acknowledge this message + # FIXME: We should check its sequence number first + self.sendAcknowledge(sequenceNumber) + + # Process it + self.filterMessage(sequenceNumber, messageType, fields) + +class Encoder: + """ + """ + + __sequenceNumber = 0 + + def onOutgoingMessage(self, message): + """ + """ + pass + + # Public methods + + def __sendMessage(self, messageType, fields = []): + """ + """ + self.__sequenceNumber += 1 + if self.__sequenceNumber > 999: + self.__sequenceNumber = 0 + d = encodeMessage(messageType, [('seq', str(self.__sequenceNumber)), ('src', self.__sourceAddress), ('dst', self.__destinationAddress)].join(fields)) + self.onOutgoingMessage(d) + + def sendAcknowledge(self): + """ + """ + self.__sendMessage('ACK') + + def sendNotAcknowledge(self, error): + """ + """ + self.__sendMessage('NACK', [('error', error)]) + + def sendJoin(self, name, playerType): + """ + """ + self.__sendMessage('JOIN', [('name', name), ('type', playerType)]) + + def sendLeave(self, name, reason): + """ + """ + self.__sendMessage('LEAVE', [('name', name), ('reason', reason)]) + + def sendMove(self, player, move): + """ + """ + self.__sendMessage('MOVE', [('player', player), ('move', move)]) + + def sendGameAnnounce(self, name, result, white = None, black = None, spectators = [], player = None, moves = []): + """ + """ + fields = [('name', name), ('result', result)] + + if white is not None: + fields.append(('white', white)) + if black is not None: + fields.append(('black', black)) + if player is not None: + fields.append(('player', player)) + for player in spectators: + fields.append(('spectator', player)) + + for move in moves: + fields.append(('move', move)) + + self.__sendMessage('GAME', fields) + +if __name__ == '__main__': + x = encodeMessage('TEST', [('field1', 'value1'), ('long_field', 'This is a long message.\nBlah Blah\n\nAll done\n')]) + print x + (t, f) = decodeMessage(x) + print t + print f + + x = encodeMessage('X', []) + print x + (t, f) = decodeMessage(x) + print t + print f diff --git a/src/lib/scene/Makefile.am b/src/lib/scene/Makefile.am new file mode 100644 index 0000000..5144008 --- /dev/null +++ b/src/lib/scene/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = cairo opengl + +glchessdir = $(pythondir)/glchess/scene +glchess_PYTHON = \ + human.py \ + __init__.py diff --git a/src/lib/scene/__init__.py b/src/lib/scene/__init__.py new file mode 100644 index 0000000..afe4462 --- /dev/null +++ b/src/lib/scene/__init__.py @@ -0,0 +1,127 @@ +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +# Highlight types +HIGHLIGHT_SELECTED = 'selected' +HIGHLIGHT_CAN_MOVE = 'canMove' + +class ChessSet: + """ + """ + + def drawPiece(self, pieceName, state, context = None): + """Draw a piece. + + 'pieceName' is the piece name (string). + 'state' is the piece state (string). + 'context' is a reference to the rendering context being used (user-defined). + """ + pass + +class ChessPiece: + """Abstract class for a glChess chess piece model""" + + def move(self, coord): + """Move this piece to a board location. + + 'coord' is a 2-tuple containing the file and rank of the board location to move to. + The values are: + + Black + (0,7) +-----+ (7,7) + | | + | | + (0,0) +-----+ (7,0) + White + """ + pass + +class Scene: + """Abstract class for glChess scenes + + Extend this class to make a scene + """ + + # Metheds to extend by a higher class + + def onRedraw(self): + """This method is called when the scene needs redrawing""" + pass + + def startAnimation(self): + """Called when the animate() method should be called""" + pass + + # Methods to implement + + def reshape(self, width, height): + """Resize the viewport into the scene. + + 'width' is the width of the viewport in pixels. + 'height' is the width of the viewport in pixels. + """ + pass + + def addChessPiece(self, chessSet, name, coord): + """Add a chess piece model into the scene. + + 'chessSet' is the name of the chess set (string). + 'name' is the name of the piece (string). + 'coord' is the the chess board location of the piece (tuple, (file,rank)). + + Returns a reference to this chess piece or raises an exception. + """ + raise Exception('Not implemented') + + def removeChessPiece(self, piece): + """Remove chess piece. + + 'piece' is a chess piece instance as returned by addChessPiece(). + """ + pass + + def setBoardHighlight(self, coords): + """Highlight a square on the board. + + 'coords' is a dictionary of highlight types keyed by square co-ordinates. + The co-ordinates are a tuple in the form (file,rank). + If None the highlight will be cleared. + """ + pass + + def setBoardRotation(self, angle): + """Set the rotation on the board. + + 'angle' is the angle the board should be drawn at in degress (float, [0.0, 360.0]). + """ + pass + + def animate(self, timeStep): + """Animate the scene. + + 'timeStep' is the time since this method has last been called in seconds (float). + + Returns False once all animation is complete otherwise returns True. Once animation + is complete do not call this method again until startAnimation() is called. + """ + pass + + def render(self, context): + """Manually render the scene. + + 'context' TODO + """ + pass + + def getSquare(self, x, y): + """Find the chess square at a given 2D location. + + 'x' is the number of pixels from the left of the scene to select. + 'y' is the number of pixels from the bottom of the scene to select. + + This requires an OpenGL context. + + Return the co-ordinate in LAN format (string) or None if no square at this point. + """ + return None diff --git a/src/lib/scene/cairo/Makefile.am b/src/lib/scene/cairo/Makefile.am new file mode 100644 index 0000000..f8453f7 --- /dev/null +++ b/src/lib/scene/cairo/Makefile.am @@ -0,0 +1,4 @@ +glchessdir = $(pythondir)/glchess/scene/cairo +glchess_PYTHON = \ + __init__.py \ + pieces.py diff --git a/src/lib/scene/cairo/__init__.py b/src/lib/scene/cairo/__init__.py new file mode 100644 index 0000000..61695f5 --- /dev/null +++ b/src/lib/scene/cairo/__init__.py @@ -0,0 +1,297 @@ +import math + +import glchess.scene + +import pieces + +BACKGROUND_COLOUR = (0.53, 0.63, 0.75) +BORDER_COLOUR = (0.808, 0.361, 0.0)#(0.757, 0.490, 0.067)#(0.36, 0.21, 0.05) +BLACK_SQUARE_COLOURS = {None: (0.8, 0.8, 0.8), glchess.scene.HIGHLIGHT_SELECTED: (0.3, 1.0, 0.3), glchess.scene.HIGHLIGHT_CAN_MOVE: (0.3, 0.3, 1.0)} +WHITE_SQUARE_COLOURS = {None: (1.0, 1.0, 1.0), glchess.scene.HIGHLIGHT_SELECTED: (0.2, 1.0, 0.0), glchess.scene.HIGHLIGHT_CAN_MOVE: (0.2, 0.2, 0.8)} +PIECE_COLOUR = (0.0, 0.0, 0.0) + +class ChessPiece(glchess.scene.ChessPiece): + """ + """ + + __scene = None + name = None + + __targetPos = None + pos = None + + moving = False + + def __init__(self, scene, name, startPos = (0.0, 0.0)): + """ + """ + self.__scene = scene + self.name = name + self.pos = self.__coordToLocation(startPos) + + def __coordToLocation(self, coord): + """ + """ + rank = ord(coord[0]) - ord('a') + file = ord(coord[1]) - ord('1') + + return (float(rank), float(file)) + + def move(self, coord): + """Extends glchess.scene.ChessPiece""" + self.__targetPos = self.__coordToLocation(coord) + self.moving = True + self.__scene._startAnimation() + + def draw(self, state = 'default'): + """ + """ + pass + + def animate(self, timeStep): + """ + + Return True if the piece has moved otherwise False. + """ + if self.__targetPos is None: + return False + + if self.pos == self.__targetPos: + self.__targetPos = None + return False + + # Get distance to target + dx = self.__targetPos[0] - self.pos[0] + dy = self.__targetPos[1] - self.pos[1] + + # Get movement step in each direction + SPEED = 4.0 # FIXME + xStep = timeStep * SPEED + if xStep > abs(dx): + xStep = dx + else: + xStep *= cmp(dx, 0.0) + yStep = timeStep * SPEED + if yStep > abs(dy): + yStep = dy + else: + yStep *= cmp(dy, 0.0) + + # Move the piece + self.pos = (self.pos[0] + xStep, self.pos[1] + yStep) + return True + + def render(self, offset, context): + """ + """ + x = offset[0] + self.pos[0] * self.__scene.squareSize + self.__scene.PIECE_BORDER + y = offset[1] + (7 - self.pos[1]) * self.__scene.squareSize + self.__scene.PIECE_BORDER + pieces.piece(self.name, context, self.__scene.pieceSize, x, y) + context.fill() + +class Scene(glchess.scene.Scene): + """ + """ + __pieces = None + __highlights = None + + __animating = False + __changed = True + + BORDER = 6.0 + PIECE_BORDER = 2.0 + + def __init__(self): + """Constructor for a Cairo scene""" + self.__highlight = {} + self.__pieces = [] + + def onRedraw(self): + """This method is called when the scene needs redrawing""" + pass + + def _startAnimation(self): + """ + """ + self.__changed = True + if self.__animating is False: + self.__animating = True + self.startAnimation() + + def addChessPiece(self, chessSet, name, coord): + """Add a chess piece model into the scene. + + 'chessSet' is the name of the chess set (string). + 'name' is the name of the piece (string). + 'coord' is the the chess board location of the piece in LAN format (string). + + Returns a reference to this chess piece or raises an exception. + """ + name = chessSet + name[0].upper() + name[1:] + piece = ChessPiece(self, name, coord) + self.__pieces.append(piece) + + # Redraw the scene + self.__changed = True + self.onRedraw() + + return piece + + def removeChessPiece(self, piece): + """Remove chess piece. + + 'piece' is a chess piece instance as returned by addChessPiece(). + """ + self.__pieces.remove(piece) + self.__changed = True + self.onRedraw() + + def setBoardHighlight(self, coords): + """Highlight a square on the board. + + 'coords' is a dictionary of highlight types keyed by square co-ordinates. + The co-ordinates are a tuple in the form (file,rank). + If None the highlight will be cleared. + """ + self.__changed = True + if coords is None: + self.__highlight = {} + else: + self.__highlight = coords.copy() + self.onRedraw() + + def reshape(self, width, height): + """Resize the viewport into the scene. + + 'width' is the width of the viewport in pixels. + 'height' is the width of the viewport in pixels. + """ + self.__changed = True + self.width = width + self.height = height + + shortEdge = min(self.width, self.height) + self.squareSize = (shortEdge - 2.0*self.BORDER) / 9.0 + self.pieceSize = self.squareSize - 2.0*self.PIECE_BORDER + + boardWidth = self.squareSize * 9.0 + self.offset = ((self.width - boardWidth) / 2.0, (self.height - boardWidth) / 2.0) + + self.__changed = True + self.onRedraw() + + def setBoardRotation(self, angle): + """Set the rotation on the board. + + 'angle' is the angle the board should be drawn at in degress (float, [0.0, 360.0]). + """ + pass + + def animate(self, timeStep): + """Extends glchess.scene.Scene""" + redraw = False + for piece in self.__pieces: + if piece.animate(timeStep): + piece.moving = True + redraw = True + else: + # Redraw static scene once pieces stop + if piece.moving: + redraw = True + self.__changed = True + piece.moving = False + + # Redraw scene or stop animation + if redraw: + self.__animating = True + self.onRedraw() + else: + self.__animating = False + return self.__animating + + def renderStatic(self, context): + """Render the static elements in a scene. + """ + if self.__changed is False: + return False + self.__changed = False + + # Clear background + context.set_source_rgb(*BACKGROUND_COLOUR) + context.paint() + + # Draw border + context.set_source_rgb(*BORDER_COLOUR) + context.rectangle(self.offset[0], self.offset[1], self.squareSize * 9.0, self.squareSize * 9.0) + context.fill() + + offset = (self.offset[0] + self.squareSize * 0.5, self.offset[1] + self.squareSize * 0.5) + + for i in xrange(8): + for j in xrange(8): + x = offset[0] + i * self.squareSize + y = offset[1] + (7 - j) * self.squareSize + + coord = chr(ord('a') + i) + chr(ord('1') + j) + try: + highlight = self.__highlight[coord] + except KeyError: + highlight = None + + context.rectangle(x, y, self.squareSize, self.squareSize) + if (i + j) % 2 == 0: + colour = BLACK_SQUARE_COLOURS[highlight] + else: + colour = WHITE_SQUARE_COLOURS[highlight] + context.set_source_rgb(*colour) + context.fill() + + context.set_source_rgb(*PIECE_COLOUR) + for piece in self.__pieces: + if piece.moving: + continue + piece.render(offset, context) + + return True + + def renderDynamic(self, context): + """Render the dynamic elements in a scene. + + This requires a Cairo context. + """ + offset = (self.offset[0] + self.squareSize * 0.5, self.offset[1] + self.squareSize * 0.5) + + context.set_source_rgb(*PIECE_COLOUR) + for piece in self.__pieces: + if not piece.moving: + continue + piece.render(offset, context) + + def getSquare(self, x, y): + """Find the chess square at a given 2D location. + + 'x' is the number of pixels from the left of the scene to select. + 'y' is the number of pixels from the bottom of the scene to select. + + Return the co-ordinate in LAN format (string) or None if no square at this point. + """ + offset = (self.offset[0] + self.squareSize * 0.5, self.offset[1] + self.squareSize * 0.5) + + rank = (x - offset[0]) / self.squareSize + if rank < 0 or rank >= 8.0: + return None + rank = int(rank) + + file = (y - offset[1]) / self.squareSize + if file < 0 or file >= 8.0: + return None + file = 7 - int(file) + + # Convert from co-ordinates to LAN format + rank = chr(ord('a') + rank) + file = chr(ord('1') + file) + + return rank + file + + # Private methods + diff --git a/src/lib/scene/cairo/pieces.py b/src/lib/scene/cairo/pieces.py new file mode 100644 index 0000000..baf723f --- /dev/null +++ b/src/lib/scene/cairo/pieces.py @@ -0,0 +1,47 @@ +__author__ = 'Thomas Dybdahl Ahle ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = '' + +import re + +elemExpr = re.compile(r"([a-zA-Z])\s*([0-9\.,\s]*)\s+") +spaceExpr = re.compile(r"[\s,]+") + +def parse2 (cc, name, psize, x, y): + for g1, g2 in parsedPieces[name]: + coords = [(s*psize/size) for s in g2] + for i in range(0,len(coords),2): coords[i] += x + for i in range(1,len(coords),2): coords[i] += y + if g1 == 'm': + cc.move_to(*coords) + elif g1 == 'l': + cc.line_to(*coords) + elif g1 == 'c': + cc.curve_to(*coords) + +def piece (name, cairoContext, s, x, y): + parse2 (cairoContext, name, s, x, y) + #parse (cairoContext, pieces[name], s, x, y) + #cairoContext.text_path(pieces[name]) + +size = 800.0 +pieces = {"blackKing": "M 653.57940,730.65870 L 671.57940,613.65870 C 725.57940,577.65870 797.57940,514.65870 797.57940,397.65870 C 797.57940,325.65870 734.57940,280.65870 662.57940,280.65870 C 590.57940,280.65870 509.57940,334.65870 509.57940,334.65870 C 509.57940,334.65870 554.57940,190.65870 428.57940,154.65870 L 428.57940,118.65870 L 482.57940,118.65870 L 482.57940,64.658690 L 428.57940,64.658690 L 428.57940,10.658690 L 374.57940,10.658690 L 374.57940,64.658690 L 320.57940,64.658690 L 320.57940,118.65870 L 374.57940,118.65870 L 374.57940,154.65870 C 248.57940,190.65870 293.57940,334.65870 293.57940,334.65870 C 293.57940,334.65870 212.57940,280.65870 140.57940,280.65870 C 68.579380,280.65870 5.5793840,325.65870 5.5793840,397.65870 C 5.5793840,514.65870 77.579380,577.65870 131.57940,613.65870 L 149.57940,730.65870 C 158.57940,757.65870 221.57940,793.65870 401.57940,793.65870 C 581.57940,793.65870 644.57940,757.65870 653.57940,730.65870 z M 374.57940,541.65870 C 329.57940,541.65870 212.57940,550.65870 167.57940,568.65870 C 113.57940,541.65870 59.579380,496.65870 59.579380,406.65870 C 59.579380,352.65870 86.579380,334.65870 149.57940,334.65870 C 212.57940,334.65870 356.57940,397.65870 374.57940,541.65870 z M 428.57940,541.65870 C 446.57940,397.65870 590.57940,334.65870 662.57940,334.65870 C 716.57940,334.65870 743.57940,352.65870 743.57940,406.65870 C 743.57940,496.65870 689.57940,541.65870 635.57940,568.65870 C 590.57940,550.65870 473.57940,541.65870 428.57940,541.65870 z M 617.57940,667.65870 L 608.57940,705.90870 C 437.57940,678.90870 365.57940,678.90870 194.57940,705.90870 L 185.57940,667.65870 C 365.57940,640.65870 437.57940,640.65870 617.57940,667.65870 z M 464.57940,514.65870 C 527.57940,514.65870 581.57940,523.65870 635.57940,541.65870 C 707.57940,487.65870 716.57940,442.65870 716.57940,406.65870 C 716.57940,379.65870 698.57940,361.65870 662.57940,361.65870 C 554.57940,361.65670 473.57940,451.65870 464.57940,514.65870 z M 338.57940,514.65870 C 329.57940,451.65870 239.57940,361.65670 140.57940,361.65870 C 104.57940,361.65870 86.579380,388.65870 86.579380,415.65870 C 86.579380,442.65870 95.579380,487.65870 167.57940,541.65870 C 221.57940,523.65870 275.57940,514.65870 338.57940,514.65870 z ", + "blackQueen": "M 617.12310,626.00950 C 617.12310,599.00950 627.77310,557.68550 671.12310,536.00950 C 689.12310,527.00950 689.12310,509.00950 689.12310,500.00950 C 689.12310,471.54950 743.12310,203.00950 743.12310,203.00950 C 779.62510,198.74750 796.96610,170.47750 796.96610,144.34650 C 796.96610,112.98940 772.26810,85.907430 738.52810,85.907430 C 710.73310,85.907430 680.56310,109.18740 679.85110,143.63450 C 679.66510,152.63250 681.03910,169.05250 697.43010,186.15650 L 590.12310,392.00950 L 590.12310,158.00950 C 619.03410,151.95050 636.85210,127.48250 636.85210,99.687430 C 636.85210,69.517430 610.72110,42.199430 577.93710,42.199430 C 544.44210,42.199430 519.27910,70.470430 519.73610,102.06340 C 519.97310,118.45540 527.57510,136.03450 545.12410,149.00950 L 464.12410,383.00950 L 428.12410,131.00950 C 452.74310,117.03040 459.86810,97.075430 459.86810,80.208430 C 459.86810,41.011430 428.74910,20.581430 401.19210,20.818430 C 371.26010,21.076430 342.75310,46.950430 342.75310,77.120430 C 342.75310,107.05240 359.14410,122.73150 374.12410,131.00950 L 338.12410,383.00950 L 257.12410,149.00950 C 275.75910,134.13450 282.17310,119.64340 282.17310,98.976430 C 282.17310,77.833430 264.35910,42.199430 223.25910,42.199430 C 190.47610,42.199430 165.29410,70.944430 165.29410,99.926430 C 165.29410,134.84850 190.23910,153.37750 212.12410,158.01050 L 212.12410,392.01050 L 104.12410,185.01050 C 117.78210,170.95350 120.15710,154.06050 120.15710,145.06050 C 120.15710,114.41440 97.114060,85.807430 59.124060,86.010430 C 33.925060,86.146430 3.4010650,109.06240 3.0420650,145.06050 C 2.8040650,168.81650 20.858060,200.88650 59.124060,203.01050 C 59.124060,203.01050 113.12410,473.01050 113.12410,500.01050 C 113.12410,509.01150 113.12410,527.01050 131.12410,536.01050 C 167.12410,554.01150 185.12410,599.01050 185.12410,626.01050 C 185.12410,662.01150 158.12410,698.01150 158.12410,707.01150 C 158.12410,752.01050 320.12410,779.01150 401.12410,778.99150 C 473.12410,778.97350 644.12410,752.01050 644.12410,707.01050 C 644.12410,698.01050 617.12410,671.01050 617.12410,626.01050 L 617.12310,626.00950 z M 594.55210,537.12950 C 583.21810,553.12950 581.21810,558.46250 576.55210,575.12950 C 487.21810,553.79650 327.21810,547.79650 225.88510,575.79650 C 221.21710,557.79650 219.21710,550.46250 208.55110,537.12950 C 325.21710,508.46250 479.21710,506.46250 594.55110,537.12950 L 594.55210,537.12950 z M 570.55210,663.79650 C 555.88510,674.46250 551.21810,682.46250 542.55210,693.79650 C 434.55210,677.12950 363.88410,674.46250 255.21810,693.12950 C 246.55210,679.79650 241.88610,673.12950 229.21810,663.79650 C 341.88610,634.46250 457.88610,644.46250 570.55210,663.79650 L 570.55210,663.79650 z ", + "blackRook": "M 232.94440,519.29360 L 124.94440,627.29360 L 124.94440,762.29360 L 682.94440,762.29360 L 682.94440,627.29360 L 574.94440,519.29360 L 574.94440,303.29360 L 682.94440,231.29360 L 682.94440,51.293580 L 520.94440,51.293580 L 520.94440,123.29360 L 484.94440,123.29360 L 484.94440,51.293580 L 322.94440,51.293580 L 322.94440,123.29360 L 286.94440,123.29360 L 286.94440,51.293580 L 124.94440,51.293580 L 124.94440,231.29360 L 232.94440,303.29360 L 232.94440,519.29360 z M 268.94440,321.29360 L 268.94440,285.29360 L 538.94440,285.29360 L 538.94440,321.29360 L 268.94440,321.29360 z M 268.94440,537.29360 L 268.94440,501.29360 L 538.94440,501.29360 L 538.94440,537.29360 L 268.94440,537.29360 z ", + "blackBishop": "M 491.69440,453.44430 L 500.69440,482.69430 C 464.69440,455.69430 338.69440,455.69430 302.69440,482.69430 L 311.69440,453.44430 C 332.69440,432.44430 470.69440,432.44430 491.69440,453.44430 z M 509.69440,518.69430 L 518.69440,545.69430 C 470.69440,521.69430 332.69440,521.69430 284.69440,545.69430 L 293.69440,518.69430 C 338.69440,491.69430 464.69440,491.69430 509.69440,518.69430 z M 797.69440,653.69430 C 797.69440,653.69430 752.69440,635.69430 689.69440,626.69430 C 652.95940,621.44630 599.69440,635.69430 554.69440,626.69430 C 518.69440,617.69430 482.69440,599.69430 482.69440,599.69430 L 572.69440,554.69430 L 545.69440,473.69430 C 545.69440,473.69430 608.69440,446.69430 608.69440,365.69430 C 608.69440,302.69430 563.69440,230.69430 500.69440,194.69430 C 455.13040,168.65830 446.69440,149.69430 446.69440,149.69430 C 446.69440,149.69430 482.69440,131.69430 482.69440,86.694330 C 482.69440,50.694330 455.69440,5.6943260 401.69440,5.6943260 C 347.69440,5.6943260 320.69440,50.694330 320.69440,86.694330 C 320.69440,131.69430 356.69440,149.69430 356.69440,149.69430 C 356.69440,149.69430 348.25840,168.65830 302.69440,194.69430 C 239.69440,230.69430 194.69440,302.69430 194.69440,365.69430 C 194.69440,446.69430 257.69440,473.69430 257.69440,473.69430 L 230.69440,554.69430 L 320.69440,599.69430 C 320.69440,599.69430 284.69440,617.69430 248.69440,626.69430 C 204.17340,637.82430 146.99540,621.93730 113.69440,626.69430 C 50.694360,635.69430 5.6943640,653.69430 5.6943640,653.69430 L 50.694360,797.69430 C 113.69440,779.69430 122.69440,779.69430 176.69440,770.69430 C 209.78640,765.17930 291.51040,774.42230 329.69440,761.69430 C 383.69440,743.69430 401.69440,716.69430 401.69440,716.69430 C 401.69440,716.69430 419.69440,743.69430 473.69440,761.69430 C 511.87840,774.42230 598.40740,767.15830 626.69440,770.69430 C 681.01640,777.48430 752.69440,797.69430 752.69440,797.69430 L 797.69440,653.69430 L 797.69440,653.69430 z M 428.69440,392.69430 L 374.69440,392.69430 L 374.69440,356.69430 L 338.69440,356.69430 L 338.69440,302.69430 L 374.69440,302.69430 L 374.69440,266.69430 L 428.69440,266.69430 L 428.69440,302.69430 L 464.69440,302.69430 L 464.69440,356.69430 L 428.69440,356.69430 L 428.69440,392.69430 z ", + "blackKnight": "M 84.310370,730.48460 L 564.28850,729.48460 C 563.97550,600.58860 477.97550,556.58860 485.00550,477.74860 L 587.06050,552.58860 C 611.11150,581.44960 637.05150,594.72560 657.36750,594.91660 C 671.53450,595.04960 633.37050,547.08060 627.37050,536.08060 C 653.37050,535.08060 689.37050,585.08060 718.38750,574.11560 C 739.54850,566.12160 754.01750,540.22060 753.06850,502.24260 C 751.70850,447.81260 690.47450,367.52960 667.34250,266.83660 C 641.48850,160.69960 611.91250,147.09260 595.58450,141.64850 L 595.22350,64.085560 L 513.57950,123.95850 L 467.31450,43.675570 L 421.04950,138.92750 C 260.48350,91.300560 89.752370,428.40260 84.309370,730.48460 L 84.310370,730.48460 z M 125.87840,697.92560 C 125.87840,436.61260 289.76850,168.92760 381.72850,167.10560 C 399.37150,167.41260 415.37150,173.41260 415.32750,179.85360 C 415.24050,192.63260 399.02750,197.15260 379.90750,197.15260 C 307.97850,199.88460 158.65640,453.00260 156.83540,695.19560 C 156.83540,713.40460 127.70040,712.49460 125.87940,697.92560 L 125.87840,697.92560 z M 678.74350,471.34160 C 684.09050,477.57960 689.68150,486.16560 689.86350,492.19160 C 690.09450,499.83660 684.07150,505.86160 678.28050,503.54360 C 672.48850,501.22760 665.53850,488.25260 660.90550,485.70560 C 656.27250,483.15660 642.14050,481.30360 642.37250,474.81660 C 642.60450,468.32960 652.10250,462.53760 657.66250,462.76960 C 663.22250,463.00160 675.96450,468.09760 678.74450,471.34160 L 678.74350,471.34160 z M 520.98750,218.08460 C 534.62350,223.81160 559.71450,235.26460 577.44150,255.99260 C 594.62350,278.90060 595.98650,304.80860 596.53150,323.35560 C 566.80450,326.90060 541.87450,318.25160 529.44150,290.90060 C 521.25950,272.90060 520.98650,239.62960 520.98650,218.08460 L 520.98750,218.08460 z ", + "blackPawn": "M 688.02380,750.97630 L 688.02380,624.97630 C 688.02380,579.97630 661.62380,452.47630 553.02380,408.97630 C 598.02380,354.97630 607.02380,255.97630 517.02380,192.97630 C 544.02380,156.97630 517.02380,30.976220 409.02380,30.976220 C 301.02380,30.976220 274.02380,156.97630 301.02380,192.97630 C 211.02380,255.97630 220.02380,354.97630 265.02380,408.97630 C 157.02380,453.97630 130.02380,579.97630 130.02380,624.97630 L 130.02380,750.97630 L 688.02380,750.97630 z ", + "whiteKing": "M 648.50000,730.65870 L 666.50000,613.65870 C 720.50000,577.65870 792.50000,514.65870 792.50000,397.65870 C 792.50000,325.65870 729.50000,280.65870 657.50000,280.65870 C 585.50000,280.65870 504.50000,334.65870 504.50000,334.65870 C 504.50000,334.65870 549.50000,190.65870 423.50000,154.65870 L 423.50000,118.65870 L 477.50000,118.65870 L 477.50000,64.658690 L 423.50000,64.658690 L 423.50000,10.658690 L 369.50000,10.658690 L 369.50000,64.658690 L 315.50000,64.658690 L 315.50000,118.65870 L 369.50000,118.65870 L 369.50000,154.65870 C 243.50000,190.65870 288.50000,334.65870 288.50000,334.65870 C 288.50000,334.65870 207.50000,280.65870 135.50000,280.65870 C 63.500000,280.65870 0.50000000,325.65870 0.50000000,397.65870 C 0.50000000,514.65870 72.500000,577.65870 126.50000,613.65870 L 144.50000,730.65870 C 153.50000,757.65870 216.50000,793.65870 396.50000,793.65870 C 576.50000,793.65870 639.50000,757.65870 648.50000,730.65870 z M 396.50000,451.65870 C 396.50000,451.65870 333.50000,343.65870 333.50000,280.65870 C 333.50000,217.65870 369.50000,208.65870 396.50000,208.65870 C 423.50000,208.65870 459.50000,226.65870 459.50000,280.65870 C 459.50000,334.65870 396.50000,451.65870 396.50000,451.65870 z M 369.50000,541.65870 C 324.50000,541.65870 207.50000,550.65870 162.50000,568.65870 C 108.50000,541.65870 54.500000,496.65870 54.500000,406.65870 C 54.500000,352.65870 81.500000,334.65870 144.50000,334.65870 C 207.50000,334.65870 351.50000,397.65870 369.50000,541.65870 z M 423.50000,541.65870 C 441.50000,397.65870 585.50000,334.65870 657.50000,334.65870 C 711.50000,334.65870 738.50000,352.65870 738.50000,406.65870 C 738.50000,496.65870 684.50000,541.65870 630.50000,568.65870 C 585.50000,550.65870 468.50000,541.65870 423.50000,541.65870 z M 612.50000,613.65870 L 603.50000,685.65870 C 432.50000,658.65870 360.50000,658.65870 189.50000,685.65870 L 180.50000,613.65870 C 360.50000,586.65870 432.50000,586.65870 612.50000,613.65870 z M 549.50000,730.65870 C 468.50000,748.65870 441.50000,748.65870 396.50000,748.65870 C 351.50000,748.65870 324.50000,748.65870 243.50000,730.65870 C 324.50000,712.65870 342.50000,712.65870 396.50000,712.65870 C 450.50000,712.65870 468.50000,712.65870 549.50000,730.65870 z ", + "whiteQueen": "M 764.60380,143.65350 C 764.80680,155.66150 755.91880,166.72550 742.61880,166.56350 C 727.46980,166.37750 719.85480,154.92450 719.70980,144.57750 C 719.52480,131.46050 729.68780,121.66950 742.24980,121.66950 C 754.99780,121.66950 764.41980,132.75350 764.60380,143.65350 L 764.60380,143.65350 z M 619.66280,626.00950 C 619.66280,599.00950 630.31280,557.68550 673.66280,536.00950 C 691.66280,527.00950 691.66280,509.00950 691.66280,500.00950 C 691.66280,471.54950 745.66280,203.00950 745.66280,203.00950 C 782.16480,198.74750 799.50580,170.47750 799.50580,144.34650 C 799.50580,112.98950 774.80780,85.907570 741.06780,85.907570 C 713.27280,85.907570 683.10280,109.18750 682.39080,143.63450 C 682.20480,152.63250 683.57880,169.05250 699.96980,186.15650 L 592.66280,392.00950 L 592.66280,158.00950 C 621.57380,151.95050 639.39180,127.48250 639.39180,99.687540 C 639.39180,69.517570 613.26080,42.199570 580.47680,42.199570 C 546.98180,42.199570 521.81880,70.470570 522.27580,102.06350 C 522.51280,118.45550 530.11480,136.03450 547.66380,149.00950 L 466.66380,383.00950 L 430.66380,131.00950 C 455.28280,117.03050 462.40880,97.075540 462.40880,80.208570 C 462.40880,41.011570 431.28880,20.581570 403.73180,20.818570 C 373.79980,21.076570 345.29380,46.950570 345.29380,77.120570 C 345.29380,107.05250 361.68480,122.73150 376.66380,131.00950 L 340.66380,383.00950 L 259.66380,149.00950 C 278.29980,134.13450 284.71380,119.64350 284.71380,98.976540 C 284.71380,77.833570 266.89880,42.199570 225.79980,42.199570 C 193.01680,42.199570 167.83480,70.944570 167.83480,99.926540 C 167.83480,134.84850 192.77880,153.37750 214.66380,158.01050 L 214.66380,392.01050 L 106.66380,185.01050 C 120.32180,170.95350 122.69780,154.06050 122.69780,145.06050 C 122.69780,114.41450 99.654770,85.807570 61.663770,86.010570 C 36.464770,86.146570 5.9407760,109.06250 5.5817760,145.06050 C 5.3447760,168.81650 23.398770,200.88650 61.663770,203.01050 C 61.663770,203.01050 115.66380,473.01050 115.66380,500.01050 C 115.66380,509.01150 115.66380,527.01050 133.66380,536.01050 C 169.66380,554.01150 187.66380,599.01050 187.66380,626.01050 C 187.66380,662.01150 160.66380,698.01150 160.66380,707.01150 C 160.66380,752.01050 322.66380,779.01150 403.66380,778.99150 C 475.66380,778.97350 646.66380,752.01050 646.66380,707.01050 C 646.66380,698.01050 619.66380,671.01050 619.66380,626.01050 L 619.66280,626.00950 z M 87.606770,144.04950 C 87.809770,156.05650 78.921770,167.12050 65.621770,166.95850 C 50.472770,166.77350 42.857770,155.31950 42.712770,144.97350 C 42.527770,131.85650 52.690770,122.06450 65.252770,122.06450 C 78.000770,122.06450 87.422770,133.14950 87.606770,144.04950 z M 603.61080,99.656540 C 603.81380,111.66350 594.92580,122.72750 581.62580,122.56550 C 566.47680,122.38050 558.86180,110.92650 558.71680,100.58050 C 558.53180,87.463540 568.69480,77.671570 581.25680,77.671570 C 594.00480,77.671570 603.42680,88.756540 603.61080,99.656540 L 603.61080,99.656540 z M 426.61880,78.157570 C 426.82180,90.165540 417.93380,101.22950 404.63380,101.06750 C 389.48480,100.88150 381.86980,89.428540 381.72480,79.081570 C 381.53980,65.964570 391.70280,56.173570 404.26480,56.173570 C 417.01280,56.173570 426.43480,67.257570 426.61880,78.157570 z M 249.12780,100.65650 C 249.33080,112.66350 240.44280,123.72750 227.14280,123.56550 C 211.99380,123.38050 204.37880,111.92650 204.23380,101.58050 C 204.04880,88.463540 214.21180,78.671570 226.77380,78.671570 C 239.52180,78.671570 248.94380,89.756540 249.12780,100.65650 z M 578.63980,575.93450 C 569.63980,591.93450 563.63980,630.93450 573.63980,663.93450 C 467.97280,643.60050 338.63980,637.93450 231.63980,663.93450 C 238.63980,638.93450 240.63980,613.93450 227.63980,575.93450 C 320.63980,544.93450 515.63980,553.93450 578.63980,575.93450 L 578.63980,575.93450 z M 537.63980,707.93450 C 489.97280,725.93450 429.97280,726.26850 399.97280,726.26850 C 369.97280,726.26850 308.97280,723.93450 264.63980,708.93450 C 317.63980,697.93450 362.30680,695.60050 397.30680,695.60050 C 432.30680,695.60050 497.97280,700.26850 537.63980,707.93450 z M 210.32980,536.94050 C 210.32980,536.94050 205.66280,530.27350 200.99580,524.94050 C 210.32980,522.27350 232.99580,509.60650 244.32980,494.94050 C 291.66280,508.94050 316.32980,498.94050 350.99580,476.27350 C 384.32980,494.94050 417.66280,494.27350 458.99580,474.94050 C 486.32980,498.27350 515.66280,504.27350 559.66280,495.60650 C 576.32980,512.94050 586.99580,518.94050 604.32980,525.60650 L 596.32980,536.94050 C 454.32980,506.94050 358.99580,506.27350 210.32980,536.94050 L 210.32980,536.94050 z M 691.30580,290.55250 L 654.25080,486.80250 C 626.80380,493.20650 606.21880,481.76950 593.40980,465.30150 L 691.30580,290.55250 z M 553.20180,247.31050 L 550.98680,445.24750 C 523.09080,454.98950 508.03480,450.56150 487.66580,434.17750 L 553.20180,247.31050 z M 401.76580,233.14150 L 433.64780,429.30650 C 416.82180,441.26250 388.48180,443.47650 369.44080,428.74850 L 401.76580,233.14150 z M 252.98380,254.39550 L 318.96280,441.26250 C 304.35080,457.64650 279.55280,464.28750 255.64180,452.77550 L 252.98480,254.39550 L 252.98380,254.39550 z M 116.63980,294.93450 L 212.13980,463.43450 C 201.63980,481.43450 175.13980,492.43450 151.13980,485.43450 L 116.63980,294.93450 z ", + "whiteRook": "M 227.86510,504.05560 L 119.86510,612.05560 L 119.86510,747.05560 L 677.86510,747.05560 L 677.86510,612.05560 L 569.86510,504.05560 L 569.86510,288.05560 L 677.86510,216.05560 L 677.86510,36.055570 L 515.86510,36.055570 L 515.86510,108.05560 L 479.86510,108.05560 L 479.86510,36.055570 L 317.86510,36.055570 L 317.86510,108.05560 L 281.86510,108.05560 L 281.86510,36.055570 L 119.86510,36.055570 L 119.86510,216.05560 L 227.86510,288.05560 L 227.86510,504.05560 z M 623.86510,90.055570 L 623.86510,180.05560 L 515.86510,252.05560 L 281.86510,252.05560 L 173.86510,180.05560 L 173.86510,90.055570 L 227.86510,90.055570 L 227.86510,162.05560 L 371.86510,162.05560 L 371.86510,90.055570 L 425.86510,90.055570 L 425.86510,162.05560 L 569.86510,162.05560 L 569.86510,90.055570 L 623.86510,90.055570 z M 515.86510,315.05560 L 515.86510,468.05560 L 281.86510,468.05560 L 281.86510,315.05560 L 515.86510,315.05560 z M 623.86510,657.05560 L 623.86510,693.05560 L 173.86510,693.05560 L 173.86510,657.05560 L 623.86510,657.05560 z M 515.86510,531.05560 L 596.86510,603.05560 L 200.86510,603.05560 L 281.86510,531.05560 L 515.86510,531.05560 z ", + "whiteBishop": "M 404.23410,59.693330 C 422.23410,59.693330 431.23410,68.693330 431.23410,86.693330 C 431.23410,104.69330 422.23410,113.69330 404.23410,113.69330 C 386.23410,113.69330 377.23410,104.69330 377.23410,86.693330 C 377.23410,68.693330 386.23410,59.693330 404.23410,59.693330 z M 404.23410,167.69330 C 440.23410,221.69330 458.23410,221.69330 503.23410,257.69330 C 548.23410,293.69330 557.23410,338.69330 557.23410,374.69430 C 557.23410,410.69330 536.23410,432.29530 512.23410,446.69430 C 512.23410,446.69430 476.23410,428.69430 404.23410,428.69430 C 332.23410,428.69430 296.23410,446.69430 296.23410,446.69430 C 296.23410,446.69430 251.23410,410.69330 251.23410,374.69430 C 251.23410,338.69330 260.23410,293.69330 305.23410,257.69330 C 350.23410,221.69330 368.23410,221.69330 404.23410,167.69330 z M 503.23410,482.69430 L 512.23410,509.69430 C 467.23410,491.69430 341.23410,491.69430 296.23410,509.69430 L 305.23410,482.69430 C 341.23410,464.69430 467.23410,464.69430 503.23410,482.69430 z M 404.23410,536.69430 C 440.23410,536.69530 494.23410,545.69430 494.23410,545.69430 C 494.23410,545.69430 440.23410,554.69430 404.23410,554.69430 C 368.23410,554.69430 314.23410,545.69530 314.23410,545.69530 C 314.23410,545.69530 368.23410,536.69330 404.23410,536.69430 z M 440.23410,635.69430 C 494.23410,671.69430 503.59610,666.60330 539.23410,671.69430 C 602.23410,680.69430 628.16110,676.01530 656.23410,680.69430 C 710.23410,689.69430 737.23410,698.69430 737.23410,698.69430 L 719.23410,743.69430 C 719.23410,743.69430 710.66410,732.18430 665.23410,725.69430 C 602.23410,716.69430 548.23410,716.69430 503.23410,707.69430 C 458.23410,698.69430 422.23410,680.69430 404.23410,662.69430 C 386.84810,680.08030 350.23410,698.69430 305.23410,707.69430 C 260.23410,716.69430 207.48310,712.84430 143.23410,725.69430 C 98.234040,734.69430 89.234040,743.69430 89.234040,743.69430 L 71.234040,698.69430 C 71.234040,698.69430 98.234040,689.69430 152.23410,680.69430 C 176.77810,676.60330 206.23410,680.69430 269.23410,671.69430 C 305.96910,666.44630 314.23410,671.69430 368.23410,635.69430 L 440.23410,635.69430 z M 431.23410,266.69430 L 377.23410,266.69430 L 377.23410,302.69430 L 341.23410,302.69430 L 341.23410,356.69430 L 377.23410,356.69430 L 377.23410,392.69430 L 431.23410,392.69430 L 431.23410,356.69430 L 467.23410,356.69430 L 467.23410,302.69430 L 431.23410,302.69430 L 431.23410,266.69430 z M 800.23410,653.69430 C 800.23410,653.69430 755.23410,635.69430 692.23410,626.69430 C 655.49910,621.44630 602.23410,635.69430 557.23410,626.69430 C 521.23410,617.69430 485.23410,599.69430 485.23410,599.69430 L 575.23410,554.69430 L 548.23410,473.69430 C 548.23410,473.69430 611.23410,446.69430 611.23410,365.69430 C 611.23410,302.69430 566.23410,230.69430 503.23410,194.69430 C 457.67010,168.65830 449.23410,149.69430 449.23410,149.69430 C 449.23410,149.69430 485.23410,131.69430 485.23410,86.694330 C 485.23410,50.694330 458.23410,5.6943260 404.23410,5.6943260 C 350.23410,5.6943260 323.23410,50.694330 323.23410,86.694330 C 323.23410,131.69430 359.23410,149.69430 359.23410,149.69430 C 359.23410,149.69430 350.79810,168.65830 305.23410,194.69430 C 242.23410,230.69430 197.23410,302.69430 197.23410,365.69430 C 197.23410,446.69430 260.23410,473.69430 260.23410,473.69430 L 233.23410,554.69430 L 323.23410,599.69430 C 323.23410,599.69430 287.23410,617.69430 251.23410,626.69430 C 206.71310,637.82430 149.53510,621.93730 116.23410,626.69430 C 53.234040,635.69430 8.2340370,653.69430 8.2340370,653.69430 L 53.234040,797.69430 C 116.23410,779.69430 125.23410,779.69430 179.23410,770.69430 C 212.32610,765.17930 294.05010,774.42230 332.23410,761.69430 C 386.23410,743.69430 404.23410,716.69430 404.23410,716.69430 C 404.23410,716.69430 422.23410,743.69430 476.23410,761.69430 C 514.41810,774.42230 600.94710,767.15830 629.23410,770.69430 C 683.55610,777.48430 755.23410,797.69430 755.23410,797.69430 L 800.23410,653.69430 L 800.23410,653.69430 z ", + "whiteKnight": "M 76.688770,727.94590 L 556.66680,726.94590 C 556.35380,598.04990 470.35380,554.04990 477.38380,475.20990 L 579.43880,550.04990 C 620.25980,599.03590 666.52580,603.11790 681.49380,574.54390 C 715.51280,581.34690 746.80780,554.13390 745.44780,499.70390 C 744.08780,445.27390 682.85280,364.99090 659.72080,264.29690 C 633.86680,158.15990 604.29080,144.55290 587.96280,139.10890 L 587.60180,61.545870 L 505.95780,121.41890 L 459.69280,41.135870 L 413.42780,136.38790 C 252.86280,88.761870 82.131770,425.86390 76.688770,727.94590 z M 505.95780,677.95990 L 126.31380,677.95990 C 205.68580,187.71690 362.68580,154.71690 437.92180,193.53890 L 462.41480,144.55290 L 481.46480,178.57090 L 567.19080,198.98090 L 576.71580,189.45790 C 597.12680,205.78590 608.14980,284.03590 634.35280,350.71790 C 662.29280,421.82190 697.90980,477.31990 697.82080,496.98190 C 697.68580,526.71790 689.01880,532.71790 671.96680,525.55790 C 663.68580,512.04990 655.01880,500.71790 639.30980,499.70390 C 632.35280,500.04990 621.14380,503.00890 631.68580,509.71790 C 646.35280,519.04990 642.75180,540.16490 642.75180,540.16490 C 616.01880,518.71790 515.25280,437.01890 459.69280,401.73090 C 442.35180,390.71690 426.68480,380.71690 414.68480,350.21690 C 390.82180,377.37090 416.35180,430.71690 431.68480,444.04890 C 408.35180,536.04890 484.35180,618.71690 505.95680,677.95890 L 505.95780,677.95990 z M 682.04780,490.00990 C 682.04780,485.18590 675.98380,472.91790 669.91880,467.95690 C 663.85480,462.99390 654.48180,460.23590 648.96880,460.37490 C 643.45580,460.51390 634.49680,465.74990 634.90980,472.36690 C 635.32280,478.98190 647.17680,480.77490 651.86280,482.56590 C 656.54880,484.35690 662.88880,496.62590 669.50480,500.75990 C 676.12080,504.89390 682.04780,496.67790 682.04780,490.00990 L 682.04780,490.00990 z M 588.45680,320.97290 C 588.11380,280.48690 578.16380,263.67390 566.49780,249.26390 C 554.83180,234.85190 512.97280,215.29490 512.97280,215.29490 C 512.97280,215.29490 508.16880,266.41790 525.66780,296.26990 C 543.16680,326.11990 570.61480,321.31690 588.45680,320.97290 L 588.45680,320.97290 z ", + "whitePawn": "M 688.02380,753.51590 L 688.02380,627.51590 C 688.02380,582.51590 661.62380,455.01590 553.02380,411.51590 C 598.02380,357.51590 607.02380,258.51590 517.02380,195.51590 C 544.02380,159.51590 517.02380,33.515900 409.02380,33.515900 C 301.02380,33.515900 274.02380,159.51590 301.02380,195.51590 C 211.02380,258.51590 220.02380,357.51590 265.02380,411.51590 C 157.02380,456.51590 130.02380,582.51590 130.02380,627.51590 L 130.02380,753.51590 L 688.02380,753.51590 z M 409.02380,87.515900 C 490.02380,87.515900 490.02380,177.51590 454.02380,213.51590 C 562.02380,258.51590 535.02380,375.51590 481.02380,429.51590 C 571.02380,456.51590 634.02380,546.51590 634.02380,609.51590 L 634.02380,699.51590 L 184.02380,699.51590 L 184.02380,609.51590 C 184.02380,546.51590 247.02380,456.51590 337.02380,429.51590 C 283.02380,375.51590 256.02380,258.51590 364.02380,213.51590 C 328.02380,177.51590 328.02380,87.515900 409.02380,87.515900 z "} + +parsedPieces = {} +for k, pi in pieces.iteritems(): + list = [] + for g1, g2 in elemExpr.findall(pi): + if not g1 or not g2: continue + list += [(g1.lower(), [float(s) for s in spaceExpr.split(g2)])] + parsedPieces[k] = list diff --git a/src/lib/scene/human.py b/src/lib/scene/human.py new file mode 100644 index 0000000..3e2506f --- /dev/null +++ b/src/lib/scene/human.py @@ -0,0 +1,165 @@ +""" +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +import glchess.scene + +class SceneHumanInput: + """ + """ + # Flag to control if human input is enabled + __inputEnabled = True + + # The selected square to move from + __startSquare = None + + def __init__(self): + """Constructor for a scene with human selectable components""" + pass + + # Methods to extend + + def onRedraw(self): + """This method is called when the scene needs redrawing""" + pass + + def playerIsHuman(self): + """Check if the current player is a human. + + Return True the current player is human else False. + """ + return False + + def getSquare(self, x, y): + """Find the chess square at a given 2D location. + + 'x' is the number of pixels from the left of the scene to select. + 'y' is the number of pixels from the bottom of the scene to select. + + Return the co-ordinate as a tuple in the form (file,rank) or None if + no square at this point. + """ + pass + + def squareIsFriendly(self, coord): + """Check if a given square contains a friendly piece. + + Return True if this square contains a friendly piece. + """ + return False + + def canMove(self, start, end): + """Check if a move is valid. + + 'start' is the location to move from in LAN format (string). + 'end' is the location to move from in LAN format (string). + """ + return False + + def moveHuman(self, start, end): + """Called when a human player moves. + + 'start' is the location to move from in LAN format (string). + 'end' is the location to move from in LAN format (string). + """ + pass + + def setBoardHighlight(self, coords): + """Called when a human player changes the highlighted squares. + + 'coords' is a list or tuple of co-ordinates to highlight. + The co-ordinates are in LAN format (string). + If None the highlight should be cleared. + """ + pass + + # Public methods + + def enableHumanInput(self, inputEnabled): + """Enable/disable human input. + + 'inputEnabled' is a flag to show if human input is enabled (True) or disabled (False). + """ + if inputEnabled is False: + self.__startSquare = None + self.setBoardHighlight(None) + self.__inputEnabled = inputEnabled + + def select(self, x, y): + """ + """ + if self.__inputEnabled is False: + return + + # Only bother if the current player is human + if self.playerIsHuman() is False: + return + + # Get the selected square + coord = self.getSquare(x, y) + if coord is None: + return + + # If this is a friendly piece then select it + if self.squareIsFriendly(coord): + self.__startSquare = coord + + # Highlight the squares that can be moved to + highlights = {} + for file in '12345678': + for rank in 'abcdefgh': + if self.canMove(coord, rank + file): + highlights[rank + file] = glchess.scene.HIGHLIGHT_CAN_MOVE + highlights[coord] = glchess.scene.HIGHLIGHT_SELECTED + self.setBoardHighlight(highlights) + + else: + # If we have already selected a start move try + # and move to this square + if self.__startSquare is not None: + self.__move(self.__startSquare, coord) + + # Redraw the scene + self.onRedraw() + + return coord + + def deselect(self, x, y): + """ + """ + if self.__inputEnabled is False: + return + + # Only bother if the current player is human + if self.playerIsHuman() is False: + return + + # Get the selected square + coord = self.getSquare(x, y) + if coord is None: + return + + # Attempt to move here + if self.__startSquare is not None and self.__startSquare != coord: + self.__move(self.__startSquare, coord) + + # Redraw the scene + self.onRedraw() + + return coord + + # Private methods + + def __move(self, start, end): + """Attempt to make a move. + + ... + """ + if self.canMove(start, end) is False: + return + self.__selectedSquare = None + self.setBoardHighlight(None) + self.moveHuman(start, end) diff --git a/src/lib/scene/opengl/Makefile.am b/src/lib/scene/opengl/Makefile.am new file mode 100644 index 0000000..380ef4b --- /dev/null +++ b/src/lib/scene/opengl/Makefile.am @@ -0,0 +1,8 @@ +glchessdir = $(pythondir)/glchess/scene/opengl +glchess_PYTHON = \ + builtin_models.py \ + __init__.py \ + new_models.py \ + opengl.py \ + texture.py \ + png.py diff --git a/src/lib/scene/opengl/__init__.py b/src/lib/scene/opengl/__init__.py new file mode 100644 index 0000000..4608e24 --- /dev/null +++ b/src/lib/scene/opengl/__init__.py @@ -0,0 +1,17 @@ +try: + import OpenGL.GL +except ImportError: + import glchess.scene + + class Piece(glchess.scene.ChessPiece): + pass + + class Scene(glchess.scene.Scene): + + def __init__(self): + pass + + def addChessPiece(self, chessSet, name, coord): + return Piece() +else: + from opengl import * diff --git a/src/lib/scene/opengl/builtin_models.py b/src/lib/scene/opengl/builtin_models.py new file mode 100644 index 0000000..86ec607 --- /dev/null +++ b/src/lib/scene/opengl/builtin_models.py @@ -0,0 +1,527 @@ +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +import math +from OpenGL.GL import * + +import glchess.scene +import texture + +from glchess.defaults import * + +# HACK +import os.path + +WHITE_BASE = (0.95, 0.81, 0.64) +WHITE_AMBIENT = (0.4*WHITE_BASE[0], 0.4*WHITE_BASE[1], 0.4*WHITE_BASE[2], 1.0) +WHITE_DIFFUSE = (0.7*WHITE_BASE[0], 0.7*WHITE_BASE[1], 0.7*WHITE_BASE[2], 1.0) +WHITE_SPECULAR = (1.0*WHITE_BASE[0], 1.0*WHITE_BASE[1], 1.0*WHITE_BASE[2], 1.0) +WHITE_SHININESS = 64.0 + +BLACK_BASE = (0.62, 0.45, 0.28) +BLACK_AMBIENT = (0.4*BLACK_BASE[0], 0.4*BLACK_BASE[1], 0.4*BLACK_BASE[2], 1.0) +BLACK_DIFFUSE = (0.7*BLACK_BASE[0], 0.7*BLACK_BASE[1], 0.7*BLACK_BASE[2], 1.0) +BLACK_SPECULAR = (1.0*BLACK_BASE[0], 1.0*BLACK_BASE[1], 1.0*BLACK_BASE[2], 1.0) +BLACK_SHININESS = 64.0 + +class BuiltinSet(glchess.scene.ChessSet): + """ + """ + # The models + __pawn = None + __rook = None + __knight = None + __bishop = None + __queen = None + __king = None + + # A dictionary of models indexed by name + __modelsByName = None + + # The rotation in degrees of pieces in the set (i.e. 0.0 for white and 180.0 for black) + __rotation = 0.0 + + __stateColours = None + __defaultState = None + + # The display lists for each model + __displayLists = None + + # The model texture + __texture = None + + def __init__(self, textureFileName, ambient, diffuse, specular, shininess): + self.__pawn = Pawn() + self.__rook = Rook() + self.__knight = Knight() + self.__bishop = Bishop() + self.__queen = Queen() + self.__king = King() + self.__modelsByName = {'pawn': self.__pawn, + 'rook': self.__rook, + 'knight': self.__knight, + 'bishop': self.__bishop, + 'queen': self.__queen, + 'king': self.__king} + self.__displayLists = {} + self.__stateColours = {} + self.__texture = texture.Texture(textureFileName, ambient = ambient, diffuse = diffuse, + specular = specular, shininess = shininess) + + def setRotation(self, theta): + """ + """ + self.__rotation = theta + + def addState(self, name, colour, default = False): + """ + """ + self.__stateColours[name] = colour + if default is True: + self.__defaultState = colour + + def drawPiece(self, pieceName, state, context): + """Draw a piece. + + 'pieceName' is the piece name (string). + 'state' is the piece state (string). + 'context' is a reference to the openGL context being used (user-defined). + + If a context is provided then the models are rendered using display lists. + """ + glRotatef(self.__rotation, 0.0, 1.0, 0.0) + + # Draw as white if textured + if glGetBoolean(GL_TEXTURE_2D): + glColor3f(1.0, 1.0, 1.0) + else: + try: + colour = self.__stateColours[state] + except KeyError: + colour = self.__defaultState + glColor3fv(colour) + + self.__texture.bind() + + # Render to a display list for optimisation + # TODO: This lists should be able to be shared between colours and games + try: + list = self.__displayLists[pieceName] + except KeyError: + # Get model to render + piece = self.__modelsByName[pieceName] + + # Attempt to make an optimised list, if none available just render normally + list = self.__displayLists[pieceName] = glGenLists(1) + if list == 0: + piece.draw() + return + + # Draw the model + glNewList(list, GL_COMPILE) + piece.draw() + glEndList() + + # Draw pre-rendered model + glCallList(list) + +class WhiteBuiltinSet(BuiltinSet): + """ + """ + + def __init__(self): + BuiltinSet.__init__(self, os.path.join(IMAGE_DIR, 'piece.png'), WHITE_AMBIENT, WHITE_DIFFUSE, WHITE_SPECULAR, WHITE_SHININESS) + self.setRotation(180.0) + self.addState('unselected', (0.9, 0.9, 0.9), default = True) + +class BlackBuiltinSet(BuiltinSet): + """ + """ + + def __init__(self): + BuiltinSet.__init__(self, os.path.join(IMAGE_DIR, 'piece.png'), BLACK_AMBIENT, BLACK_DIFFUSE, BLACK_SPECULAR, BLACK_SHININESS) + self.addState('unselected', (0.2, 0.2, 0.2), default = True) + +class SimpleModel: + """ + """ + + pos = None + + def start(self): + pass + + def revolve(self, coords, divisions = 16, scale = 1.0, maxHeight = None): + """ + """ + # Get the number of points + + # If less than two points, can not revolve + if len(coords) < 2: + raise TypeError() # FIXME + + # If the max_height hasn't been defined, find it + if maxHeight is None: + maxHeight = 0.0 + for coord in coords: + if maxHeight < coord[1]: + maxHeight = coord[1] + + # Draw the revolution + sin_ptheta = 0.0 + cos_ptheta = 1.0 + norm_ptheta = 0.0 + for division in range(1, divisions+1): + theta = 2.0 * (float(division) * math.pi) / float(divisions) + sin_theta = math.sin(theta) + cos_theta = math.cos(theta) + norm_theta = theta / (2.0 * math.pi) + coord = coords[0] + pradius = coord[0] * scale + pheight = coord[1] * scale + + for coord in coords[1:]: + radius = coord[0] * scale + height = coord[1] * scale + + # Get the normalized lengths of the normal vector + dradius = radius - pradius + dheight = height - pheight + length = math.sqrt(dradius * dradius + dheight * dheight) + dradius /= length + dheight /= length + + normal = (dheight, -dradius) + + # Rotate the normal + n0 = (normal[0] * sin_ptheta, normal[1], normal[0] * cos_ptheta) + n1 = (normal[0] * sin_theta, normal[1], normal[0] * cos_theta) + + # + # | | + # | | _ height1 + # | \ + # | \ + # | \ _ height0 + # | | + # + # | | + # 0 r1 r0 + + # + # d +----------+ c - upper point + # | | + # | | + # | | + # | | + # a +----------+ b - lower point + # + # | | + # smaller larger + # angle angle + a = (pradius * sin_ptheta, pheight, pradius * cos_ptheta) + b = (pradius * sin_theta, pheight, pradius * cos_theta) + c = (radius * sin_theta, height, radius * cos_theta) + d = (radius * sin_ptheta, height, radius * cos_ptheta) + + # Texture co-ordinates (conical transformation) + ta = self.__getTextureCoord(a, maxHeight) + tb = self.__getTextureCoord(b, maxHeight) + tc = self.__getTextureCoord(c, maxHeight) + td = self.__getTextureCoord(d, maxHeight) + + # If only triangles required + if c == d: + glBegin(GL_TRIANGLES) + + glNormal3fv(n0) + glTexCoord2fv(ta) + glVertex3fv(a) + + glNormal3fv(n1) + glTexCoord2fv(tb) + glVertex3fv(b) + + # FIXME: should have an average normal + glTexCoord2fv(tc) + glVertex3fv(c) + + glEnd() + + if a == b: + glBegin(GL_TRIANGLES) + + glNormal3fv(n0) + glTexCoord2fv(ta) + glVertex3fv(a) + + glNormal3fv(n1) + glTexCoord2fv(tc) + glVertex3fv(c) + + # FIXME: should have an average normal + glVertex3fv(td) + glVertex3fv(d) + + glEnd() + + else: + # quads required + glBegin(GL_QUADS) + + glNormal3fv(n0) + glTexCoord2fv(ta) + glVertex3fv(a) + + glNormal3fv(n1) + glTexCoord2fv(tb) + glVertex3fv(b) + + glNormal3fv(n1) + glTexCoord2fv(tc) + glVertex3fv(c) + + glNormal3fv(n0) + glTexCoord2fv(td) + glVertex3fv(d) + + glEnd() + + pradius = radius + pheight = height + + sin_ptheta = sin_theta + cos_ptheta = cos_theta + norm_ptheta = norm_theta + + def __getTextureCoord(self, vertex, maxHeight): + """ + """ + # FIXME: Change to a hemispherical projection so the top is not so flat + + # Conical transformation, get u and v based on vertex angle + u = vertex[0] + v = vertex[2] + + # Normalise + length = math.sqrt(u**2 + v**2) + if length != 0.0: + u /= length + v /= length + + # Maximum height is in the middle of the texture, minimum on the boundary + h = 1.0 - (vertex[1] / maxHeight) + return (0.5 + 0.5 * h * u, 0.5 + 0.5 * h * v) + + def __drawVertex(self, vertex, maxHeight): + glTexCoord2fv(self.__getTextureCoord(vertex, maxHeight)) + glVertex3fv(vertex) + + def drawTriangles(self, triangles, verticies, normals, maxHeight): + """ + """ + glBegin(GL_TRIANGLES) + for t in triangles: + glNormal3fv(normals[t[0]]) + v = t[1] + self.__drawVertex(verticies[v[0]], maxHeight) + self.__drawVertex(verticies[v[1]], maxHeight) + self.__drawVertex(verticies[v[2]], maxHeight) + glEnd() + + def drawQuads(self, quads, verticies, normals, maxHeight): + """ + """ + glBegin(GL_QUADS) + for t in quads: + glNormal3fv(normals[t[0]]) + v = t[1] + self.__drawVertex(verticies[v[0]], maxHeight) + self.__drawVertex(verticies[v[1]], maxHeight) + self.__drawVertex(verticies[v[2]], maxHeight) + self.__drawVertex(verticies[v[3]], maxHeight) + glEnd() + + def end(self): + pass + + def draw(self): + """ + """ + pass + +class Pawn(SimpleModel): + """ + """ + + # The co-ordinates of the revolution that makes the model + __revolveCoords = [(3.5, 0.0), (3.5, 2.0), (2.5, 3.0), (2.5, 4.0), (1.5, 6.0), (1.0, 8.8), + (1.8, 8.8), (1.0, 9.2), (2.0, 11.6), (1.0, 13.4), (0.0, 13.4)] + + def __init__(self): + self.pos = (0.0, 0.0, 0.0) + pass + + def draw(self): + self.revolve(self.__revolveCoords) + +class Rook(SimpleModel): + """ + """ + + # The co-ordinates of the revolution that makes the model + __revolveCoords = [(3.8, 0.0), (3.8, 2.0), (2.6, 5.0), (2.0, 10.2), (2.8, 10.2), (2.8, 13.6), (2.2, 13.6), (2.2, 13.0), (0.0, 13.0)] + + def __init__(self): + self.pos = (0.0, 0.0, 0.0) + pass + + def draw(self): + self.revolve(self.__revolveCoords) + +class Knight(SimpleModel): + """ + """ + + __maxHeight = 0.0 + + # The co-ordinates of the revolution that makes the model + __revolveCoords = [(4.1, 0.0), (4.1, 2.0), (2.0, 3.6), (2.0, 4.8), (2.6, 5.8)] + + # The other polygons + __verticies = [(2.6, 5.8, 2.6), (-2.6, 5.8, 2.6), (-2.6, 5.8, -0.8), (2.6, 5.8, -0.8), + (0.8, 16.2, 4.0), (1.0, 16.8, 3.4), (-1.0, 16.8, 3.4), (-0.80, 16.2, 4.0), + (1.0, 16.8, 3.0), (-1.0, 16.8, 3.0), (0.5, 16.8, 1.6), (-0.5, 16.8, 1.6), + (1.0, 16.8, 0.20), (-1.0, 16.8, 0.20), (1.0, 16.8, -0.20), (-1.0, 16.8, -0.20), + (0.4, 16.8, -1.1), (-0.4, 16.8, -1.1), (1.0, 16.8, -2.0), (-1.0, 16.8, -2.0), + (1.0, 16.8, -4.4), (-1.0, 16.8, -4.4), (1.0, 15.0, -4.4), (-1.0, 15.0, -4.4), + (0.55, 14.8, -2.8), (-0.55, 14.8, -2.8), (-1.0, 14.0, 1.3), (-1.2, 13.8, 2.4), + (-0.8, 16.8, 0.20), (-1.2, 13.8, 0.20), (-0.82666667, 16.6, 0.20), (-1.0, 16.6, -0.38), + (-0.88, 16.2, 0.20), (-1.0, 16.2, -0.74), (-1.2, 13.6, -0.20), (-1.0, 15.8, -1.1), + (-0.6, 14.0, -1.4), (1.2, 13.8, 2.4), (1.0, 14.0, 1.3), (1.2, 13.8, 0.20), + (0.8, 16.8, 0.20), (0.82666667, 16.6, 0.20), (1.0, 16.6, -0.38), (1.2, 13.6, -0.20), + (1.0, 16.2, -0.74), (0.88, 16.2, 0.20), (0.6, 14.0, -1.4), (1.0, 15.8, -1.1), + (0.8, 16.4, -0.56), (0.61333334, 16.4, 0.20), (-0.61333334, 16.4, 0.20), (-0.8, 16.4, -0.56), + (0.35, 17.8, -0.8), (0.35, 17.8, -4.4), (-0.35, 17.8, -4.4), (-0.35, 17.8, -0.8), + (0.35, 16.8, -0.8), (0.35, 16.8, -4.4), (-0.35, 16.8, -4.4), (-0.35, 16.8, -0.8), + (0.0, 15.0, -3.6), (0.0, 7.8, -4.0), (-0.5, 13.8, 0.4), (-2.0, 8.8, 4.0), + (2.0, 8.8, 4.0), (0.5, 13.8, 0.4), (-1.4, 12.2, -0.4), (-1.1422222222, 12.2, -2.2222222222), + (1.4, 12.2, -0.4), (1.1422222222, 12.2, -2.2222222222), (1.44, 5.8, -2.6), (-1.44, 5.8, -2.6), + (0.0, 14.0, 4.0), (-0.45, 13.8, -0.20), (0.45, 13.8, -0.20)] + __normals = [(0.0, -1.0, 0.0), (0.0, 0.707107, 0.707107), (0.0, 1.0, 0.0), (0.0, 0.0, -1.0), + (-0.933878, 0.128964, -0.333528), (-0.966676, 0.150427, 0.207145), (-0.934057, 0.124541, -0.334704), (-0.970801, -0.191698, -0.144213), + (-0.97561, 0.219512, 0.0), (0.933878, 0.128964, -0.333528), (0.966676, 0.150427, 0.207145), (0.934057, 0.124541, -0.334704), + (0.970801, -0.191698, -0.144213), (0.97561, -0.219512, 0.0), (0.598246, 0.797665, 0.076372), (0.670088, -0.714758, 0.200256), + (-0.598246, 0.797665, 0.076372), (-0.670088, -0.714758, 0.200256), (1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), + (0.0, 0.0, 1.0), (0.0, -0.853282, 0.52145), (0.0, -0.98387, -0.178885), (-0.788443, 0.043237, -0.613587), + (0.788443, 0.043237, -0.613587), (0.0, 0.584305, 0.811534), (0.0, -0.422886, 0.906183), (-0.969286, 0.231975, -0.081681), + (-0.982872, 0.184289, 0.0), (0.969286, 0.231975, -0.081681), (0.982872, 0.184289, 0.0), (0.81989, -0.220458, -0.528373), + (0.0, -0.573462, -0.819232), (-0.819890, -0.220459, -0.528373), (-0.752714, -0.273714, 0.59875), (-0.957338, 0.031911, 0.287202), + (-0.997785, 0.066519, 0.0), (0.752714, -0.273714, 0.59875), (0.957338, 0.031911, 0.287202), (0.997785, 0.066519, 0.0), + (0.0, -0.992278, 0.124035), (-0.854714, 0.484047, 0.187514), (-0.853747, 0.515805, -0.0711460), (0.854714, 0.484047, 0.187514), + (0.853747, 0.515805, -0.0711460), (0.252982, -0.948683, -0.189737), (0.257603, -0.966012, 0.021467), (0.126745, -0.887214, 0.443607), + (-0.252982, -0.948683, -0.189737), (-0.257603, -0.966012, 0.021467), (-0.126745, -0.887214, 0.443607), (0.000003, -0.668965, 0.743294), + (-0.000003, -0.668965, 0.743294), (-0.997484, 0.070735, 0.004796), (-0.744437, 0.446663, -0.496292), (0.997484, 0.070735, 0.004796), + (0.744437, 0.446663, -0.496292)] + __triangles = ((31, (3, 70, 61)), (32, (70, 71, 61)), (33, (2, 61, 71)), (20, (72, 4, 7)), + (34, (72, 7, 27)), (35, (27, 7, 6)), (36, (27, 6, 9)), (37, (72, 37, 4)), + (38, (37, 5, 4)), (39, (37, 8, 5)), (40, (72, 27, 37)), (41, (36, 66, 73)), + (42, (73, 66, 62)), (43, (46, 74, 68)), (44, (74, 65, 68)), (45, (46, 43, 74)), + (46, (65, 74, 43)), (47, (65, 43, 39)), (48, (36, 73, 34)), (49, (62, 34, 73)), + (50, (62, 29, 34)), (3, (45, 49, 41)), (51, (44, 42, 48)), (3, (32, 30, 50)), + (52, (33, 51, 31)), (53, (17, 19, 35)), (54, (15, 17, 35)), (55, (16, 47, 18)), + (56, (14, 47, 16))) + __quads = ((0, (0, 1, 2, 3)), (1, (4, 5, 6, 7)), (2, (5, 8, 9, 6)), (2, (8, 10, 11, 9)), + (2, (10, 12, 13, 11)), (2, (12, 14, 15, 13)), (2, (14, 16, 17, 15)), (2, (16, 18, 19, 17)), + (2, (18, 20, 21, 19)), (3, (21, 20, 22, 23)), (3, (23, 22, 24, 25)), (4, (9, 11, 26, 27)), + (5, (11, 28, 29, 26)), (6, (30, 28, 15, 31)), (6, (29, 32, 33, 34)), (6, (34, 33, 35, 36)), + (7, (19, 25, 36, 35)), (8, (19, 21, 23, 25)), (9, (8, 37, 38, 10)), (10, (10, 38, 39, 40)), + (11, (41, 42, 14, 40)), (11, (39, 43, 44, 45)), (11, (43, 46, 47, 44)), (12, (18, 47, 46, 24)), + (13, (18, 24, 22, 20)), (14, (45, 44, 48, 49)), (15, (49, 48, 42, 41)), (16, (32, 50, 51, 33)), + (17, (50, 30, 31, 51)), (2, (52, 53, 54, 55)), (18, (52, 56, 57, 53)), (19, (55, 54, 58, 59)), + (20, (52, 55, 59, 56)), (3, (53, 57, 58, 54)), (21, (26, 29, 39, 38)), (22, (26, 38, 37, 27)), + (23, (2, 25, 60, 61)), (24, (3, 61, 60, 24)), (25, (62, 63, 64, 65)), (26, (63, 1, 0, 64)), + (27, (62, 66, 1, 63)), (28, (66, 67, 2, 1)), (28, (25, 67, 66, 36)), (29, (65, 64, 0, 68)), + (30, (68, 0, 3, 69)), (30, (24, 46, 68, 69))) + + def __init__(self): + self.pos = (0.0, 0.0, 0.0) + self.__maxHeight = 0.0 + for v in self.__verticies: + if v[1] > self.__maxHeight: + self.__maxHeight = v[1] + + def draw(self): + self.revolve(self.__revolveCoords, maxHeight = self.__maxHeight) + self.drawTriangles(self.__triangles, self.__verticies, self.__normals, self.__maxHeight) + self.drawQuads(self.__quads, self.__verticies, self.__normals, self.__maxHeight) + +class Bishop(SimpleModel): + """ + """ + + # The co-ordinates of the revolution that makes the model + + __revolveCoords = [(4.0, 0.0), (4.0, 2.0), (2.5, 3.0), (2.5, 4.0), (1.5, 7.0), (1.2, 9.4), (2.5, 9.4), (1.7, 11.0), + (1.7, 12.2), (2.2, 13.2), (2.2, 14.8), (1.0, 16.0), (0.8, 17.0), (1.2, 17.7), (0.8, 18.4), (0.0, 18.4)] + + def __init__(self): + self.pos = (0.0, 0.0, 0.0) + pass + + def draw(self): + self.revolve(self.__revolveCoords) + +class Queen(SimpleModel): + """ + """ + + # The co-ordinates of the revolution that makes the model + + __revolveCoords = [(4.8, 0.0), (4.8, 2.2), (3.4, 4.0), (3.4, 5.0), (1.8, 8.0), (1.4, 11.8), (2.9, 11.8), + (1.8, 13.6), (1.8, 15.2), (2.0, 17.8), (2.7, 19.2), (2.4, 20.0), (1.7, 20.0), + (0.95, 20.8), (0.7, 20.8), (0.9, 21.4), (0.7, 22.0), (0.0, 22.0)] + + def __init__(self): + self.pos = (0.0, 0.0, 0.0) + pass + + def draw(self): + self.revolve(self.__revolveCoords) + +class King(SimpleModel): + """ + """ + + __maxHeight = 0.0 + + # The co-ordinates of the revolution that makes the model + + __revolveCoords = [(5.0, 0.0), (5.0, 2.0), (3.5, 3.0), (3.5, 4.6), (2.0, 7.6), (1.4, 12.6), (3.0, 12.6), + (2.0, 14.6), (2.0, 15.6), (2.8, 19.1), (1.6, 19.7), (1.6, 20.1), (0.0, 20.1)] + + __verticies = [(-0.3, 20.1, 0.351), (0.3, 20.1, 0.35), (0.3, 23.1, 0.35), (-0.3, 23.1, 0.35), + (-0.9, 21.1, 0.35), (-0.3, 21.1, 0.35), (-0.3, 22.1, 0.35), (-0.9, 22.1, 0.35), + (0.9, 21.1, 0.35), (0.9, 22.1, 0.35), (0.3, 22.1, 0.35), (0.3, 21.1, 0.35), + (0.3, 20.1, -0.35), (-0.3, 20.1, -0.35), (-0.3, 23.1, -0.35), (0.3, 23.1, -0.35), + (-0.3, 21.1, -0.35), (-0.9, 21.1, -0.35), (-0.9, 22.1, -0.35), (-0.3, 22.1, -0.35), + (0.3, 21.1, -0.35), (0.3, 22.1, -0.35), (0.9, 22.1, -0.35), (0.9, 21.1, -0.35), + (-0.3, 20.1, 0.35), (-0.3, 22.1, 0.3), (-0.3, 23.1, 0.3), (-0.3, 23.1, -0.3), + (-0.3, 22.1, -0.3)] + __normals = [(0.0, 0.0, 1.0), (0.0, 0.0, -1.0), (-1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)] + __quads = [(0, (0, 1, 2, 3)), (0, (4, 5, 6, 7)), (0, (8, 9, 10, 11)), (1, (12, 13, 14, 15)), + (1, (16, 17, 18, 19)), (1, (20, 21, 22, 23)), (2, (4, 7, 18, 17)), (2, (24, 5, 16, 13)), + (2, (25, 26, 27, 28)), (3, (23, 22, 9, 8)), (3, (12, 20, 11, 1)), (3, (21, 15, 2, 10)), + (4, (18, 7, 6, 19)), (4, (21, 10, 9, 22)), (4, (14, 3, 2, 15))] + + def __init__(self): + self.pos = (0.0, 0.0, 0.0) + self.__maxHeight = 0.0 + for v in self.__verticies: + if v[1] > self.__maxHeight: + self.__maxHeight = v[1] + + def draw(self): + self.revolve(self.__revolveCoords, maxHeight = self.__maxHeight) + self.drawQuads(self.__quads, self.__verticies, self.__normals, self.__maxHeight) diff --git a/src/lib/scene/opengl/new_models.py b/src/lib/scene/opengl/new_models.py new file mode 100644 index 0000000..624256e --- /dev/null +++ b/src/lib/scene/opengl/new_models.py @@ -0,0 +1,1552 @@ +__author__ = 'Robert Ancell , John-Paul Gignac ' +__license__ = 'GNU General Public License Version 2+' +__copyright__ = 'Copyright 2005-2006 Robert Ancell, Copyright 2006 John-Paul Gignac' + +import math +from OpenGL.GL import * +from glchess.defaults import * +import glchess.scene +import texture + +WHITE_BASE = (0.95, 0.81, 0.64) +WHITE_AMBIENT = (0.7*WHITE_BASE[0], 0.7*WHITE_BASE[1], 0.7*WHITE_BASE[2], 1.0) +WHITE_DIFFUSE = (0.7*WHITE_BASE[0], 0.7*WHITE_BASE[1], 0.7*WHITE_BASE[2], 1.0) +WHITE_SPECULAR = (1.0*WHITE_BASE[0], 1.0*WHITE_BASE[1], 1.0*WHITE_BASE[2], 1.0) +WHITE_SHININESS = 64.0 + +BLACK_BASE = (0.62, 0.45, 0.28) +BLACK_AMBIENT = (0.7*BLACK_BASE[0], 0.7*BLACK_BASE[1], 0.7*BLACK_BASE[2], 1.0) +BLACK_DIFFUSE = (0.7*BLACK_BASE[0], 0.7*BLACK_BASE[1], 0.7*BLACK_BASE[2], 1.0) +BLACK_SPECULAR = (1.0*BLACK_BASE[0], 1.0*BLACK_BASE[1], 1.0*BLACK_BASE[2], 1.0) +BLACK_SHININESS = 64.0 + +# The display lists for each model +# NOTE: This will not work if rendered in different openGL contexts +_displayLists = {} + +# HACK +import os.path + +# Vector methods + +def vectordiff(a, b): + return (b[0] - a[0], b[1] - a[1], b[2] - a[2]) + +def vectoradd(a, b): + return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) + +def crossprod(a, b): + return (a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]) + +def normalize(a): + length = math.sqrt(a[0]**2 + a[1]**2 + a[2]**2) + if length == 0.0: + return (1.0, 0.0, 0.0) + else: + return (a[0] / length, a[1] / length, a[2] / length) + +class BuiltinSet(glchess.scene.ChessSet): + """ + """ + # The models + __pawn = None + __rook = None + __knight = None + __bishop = None + __queen = None + __king = None + + # A dictionary of models indexed by name + __modelsByName = None + + # The rotation in degrees of pieces in the set (i.e. 0.0 for white and 180.0 for black) + __rotation = 0.0 + + __stateColours = None + __defaultState = None + + # The model texture + __texture = None + + def __init__(self, textureFileName, ambient, diffuse, specular, shininess): + self.__pawn = Pawn() + self.__rook = Rook() + self.__knight = Knight() + self.__bishop = Bishop() + self.__queen = Queen() + self.__king = King() + self.__modelsByName = {'pawn': self.__pawn, + 'rook': self.__rook, + 'knight': self.__knight, + 'bishop': self.__bishop, + 'queen': self.__queen, + 'king': self.__king} + self.__stateColours = {} + self.__texture = texture.Texture(textureFileName, ambient = ambient, diffuse = diffuse, + specular = specular, shininess = shininess) + + def setRotation(self, theta): + """ + """ + self.__rotation = theta + + def addState(self, name, colour, default = False): + """ + """ + self.__stateColours[name] = colour + if default is True: + self.__defaultState = colour + + def drawPiece(self, pieceName, state, context = None): + """Draw a piece. + + 'pieceName' is the piece name (string). + 'state' is the piece state (string). + 'context' is a reference to the openGL context being used (user-defined). + + If a context is provided then the models are rendered using display lists. + """ + glRotatef(self.__rotation, 0.0, 1.0, 0.0) + + # Draw as white if textured + if glGetBoolean(GL_TEXTURE_2D): + glColor3f(1.0, 1.0, 1.0) + else: + try: + colour = self.__stateColours[state] + except KeyError: + colour = self.__defaultState + glColor3fv(colour) + self.__texture.bind() + + # Render to a display list for optimisation + # TODO: This lists should be able to be shared between colours and games + try: + list = _displayLists[(context, pieceName)] + except KeyError: + # Get model to render + piece = self.__modelsByName[pieceName] + + # Attempt to make an optimised list, if none available just render normally + list = 0 + # TEMP: Assume that the context is shared across all scenes + if context is not None: + list = _displayLists[(context, pieceName)] = glGenLists(1) + + # Draw the model + if list != 0: + glNewList(list, GL_COMPILE) + piece.draw() + if list != 0: + glEndList() + glCallList(list) + + # Draw pre-rendered model + else: + glCallList(list) + +class WhiteBuiltinSet(BuiltinSet): + """ + """ + + def __init__(self): + BuiltinSet.__init__(self, os.path.join(IMAGE_DIR, 'piece.png'), WHITE_AMBIENT, WHITE_DIFFUSE, WHITE_SPECULAR, WHITE_SHININESS) + self.setRotation(180.0) + self.addState('unselected', (0.9, 0.9, 0.9), default = True) + +class BlackBuiltinSet(BuiltinSet): + """ + """ + + def __init__(self): + BuiltinSet.__init__(self, os.path.join(IMAGE_DIR, 'piece.png'), BLACK_AMBIENT, BLACK_DIFFUSE, BLACK_SPECULAR, BLACK_SHININESS) + self.addState('unselected', (0.2, 0.2, 0.2), default = True) + +ENDOFDATA = 65535 +SPIN = 65534 +VERTICES = 65533 +QUADS = 65532 +TRIANGLES = 65531 +POLARQUADSTRIP = 65530 +QUADSTRIP = 65529 +SEAM = 65528 +PATTERN = 65527 +STEPUP = 65526 +STEPDOWN = 65525 +SETBACKREF = 65524 +BACKREF = 65523 + +class SimpleModel: + """ + """ + + pos = (0,0,0) + data = [ENDOFDATA] + + def __getTextureCoord(self, vertex, maxHeight): + """ + """ + # FIXME: Change to a hemispherical projection so the top is not so flat + + # Conical transformation, get u and v based on vertex angle + u = vertex[0] + v = vertex[2] + + # Normalise + length = math.sqrt(u**2 + v**2) + if length != 0.0: + u /= length + v /= length + + # Maximum height is in the middle of the texture, minimum on the boundary + h = 1.0 - (vertex[1] / maxHeight) + return (0.5 + 0.5 * h * u, 0.5 + 0.5 * h * v) + + def draw(self): + """ + """ + # Collect the vertex coordinates + # FIXME: How does the scaling work? + vertices = [] + for v in self.__vertices( 11.0 * 0.3 / 8192): + vertices.append( v) + + # Zero out the normals + normals = [(0,0,0)] * len(vertices) + + # Add up all the face normals at each vertex + for f in self.__faces(): + if len(f) == 3: + d1 = vectordiff(vertices[f[1]], vertices[f[0]]) + d2 = vectordiff(vertices[f[2]], vertices[f[0]]) + normal = normalize(crossprod(d1,d2)) + normals[f[0]] = vectoradd(normals[f[0]],normal) + normals[f[1]] = vectoradd(normals[f[1]],normal) + normals[f[2]] = vectoradd(normals[f[2]],normal) + else: + d1 = vectordiff(vertices[f[1]], vertices[f[0]]) + d2 = vectordiff(vertices[f[3]], vertices[f[0]]) + normal = normalize(crossprod(d1,d2)) + normals[f[0]] = vectoradd(normals[f[0]],normal) + d1 = vectordiff(vertices[f[2]], vertices[f[1]]) + d2 = vectordiff(vertices[f[0]], vertices[f[1]]) + normal = normalize(crossprod(d1,d2)) + normals[f[1]] = vectoradd(normals[f[1]],normal) + d1 = vectordiff(vertices[f[3]], vertices[f[2]]) + d2 = vectordiff(vertices[f[1]], vertices[f[2]]) + normal = normalize(crossprod(d1,d2)) + normals[f[2]] = vectoradd(normals[f[2]],normal) + d1 = vectordiff(vertices[f[0]], vertices[f[3]]) + d2 = vectordiff(vertices[f[2]], vertices[f[3]]) + normal = normalize(crossprod(d1,d2)) + normals[f[3]] = vectoradd(normals[f[3]],normal) + + # Normalize the vertex normals + for i in xrange(len(normals)): + normals[i] = normalize(normals[i]) + + # Now draw the faces + for f in self.__faces(): + if len(f) == 3: + glBegin(GL_TRIANGLES) + elif len(f) == 4: + glBegin(GL_QUADS) + elif len(f) == 0: + continue + else: + assert(False) + + for v in f: + glNormal3fv(normals[v]) + vertex = vertices[v] + glTexCoord2fv(self.__getTextureCoord(vertex, 16.783)) # FIXME: Max height not calculated + glVertex3fv(vertex) + + glEnd() + + def end(self): + pass + + def __vertices(self, piece_size): + """ + """ + i = 0 + while 1: + if self.data[i] == SPIN: + steps = self.data[i+1] + i += 2 + + while self.data[i] <= SEAM: + if self.data[i] in (SETBACKREF,BACKREF): + i += 2 + elif self.data[i] == STEPUP: + steps *= 2 + i += 1 + elif self.data[i] == STEPDOWN: + steps /= 2 + i += 1 + elif self.data[i] == SEAM: + i += 1 + for v in self.__ring_vertices( steps, i, piece_size): + yield v[0] + for v in self.__ring_vertices( steps, i, piece_size): + yield v[0] + i = v[1] + else: + for v in self.__ring_vertices( steps, i, piece_size): + yield v[0] + i = v[1] + elif self.data[i] == POLARQUADSTRIP: + steps = self.data[i+1] + i += 2 + + dtheta = math.pi * 2 / steps + + while self.data[i] <= SEAM: + if self.data[i] != BACKREF: + theta = dtheta * self.data[i] + r = self.data[i+1] * piece_size + y = self.data[i+2] * piece_size + yield (r * math.cos(theta), y, r * math.sin(theta)) + i += 3 + elif self.data[i] in (QUADSTRIP,VERTICES): + i += 1 + + while self.data[i] <= SEAM: + if self.data[i] == SETBACKREF: + i += 2 + continue + + if self.data[i] != BACKREF: + yield (self.data[i] * piece_size, + self.data[i+1] * piece_size, + self.data[i+2] * piece_size) + + i += 3 + elif self.data[i] in (QUADS,TRIANGLES): + i += 1 + while self.data[i] <= SEAM: i += 1 + else: + break + + def __faces(self): + basevertex = 0 + startofvertices = 0 + backrefs = [0,0,0,0,0] + + i = 0 + while 1: + if self.data[i] == SPIN: + prevsteps = -1 + prevbase = 0 + + steps = self.data[i+1] + i += 2 + + while self.data[i] <= SEAM: + if self.data[i] == SETBACKREF: + backrefs[self.data[i+1]] = basevertex + i += 2 + continue + + if self.data[i] == STEPUP: + steps *= 2 + i += 1 + continue + + if self.data[i] == STEPDOWN: + steps /= 2 + i += 1 + continue + + if self.data[i] == BACKREF: + if prevsteps != -1: + for f in self.__ring_faces( + backrefs[self.data[i+1]], + steps, prevbase, prevsteps): yield f + + prevbase = backrefs[self.data[i+1]] + i += 2 + else: + isseam = 0 + if self.data[i] == SEAM: + isseam = 1 + i += 1 + + if self.data[i] == PATTERN: + i += 2 + self.data[i+1] * 2 + else: + if self.data[i] == 0: steps = 1 + i += 2 + + if prevsteps != -1: + for f in self.__ring_faces( basevertex, + steps, prevbase, prevsteps): yield f + + if isseam: basevertex += steps + prevbase = basevertex + basevertex += steps + + prevsteps = steps + + elif self.data[i] in (POLARQUADSTRIP,QUADSTRIP): + v0 = -1 + + if self.data[i] == POLARQUADSTRIP: i += 1 + i += 1 + + while self.data[i] <= SEAM: + if self.data[i] == BACKREF: + v2 = backrefs[self.data[i+1]] + self.data[i+2] + else: + v2 = basevertex + basevertex += 1 + + if self.data[i+3] == BACKREF: + v3 = backrefs[self.data[i+4]] + self.data[i+5] + else: + v3 = basevertex + basevertex += 1 + + i += 6 + + if v0 != -1: yield (v0,v1,v3,v2) + + v0 = v2 + v1 = v3 + + elif self.data[i] == VERTICES: + i += 1 + startofvertices = basevertex + + while self.data[i] <= SEAM: + if self.data[i] == SETBACKREF: + backrefs[self.data[i+1]] = basevertex + i += 2 + continue + + i += 3 + basevertex += 1 + + elif self.data[i] == QUADS: + i += 1 + while self.data[i] <= SEAM: + yield (self.data[i] + startofvertices, + self.data[i+1] + startofvertices, + self.data[i+2] + startofvertices, + self.data[i+3] + startofvertices) + i += 4 + + elif self.data[i] == TRIANGLES: + i += 1 + while self.data[i] <= SEAM: + yield (self.data[i] + startofvertices, + self.data[i+1] + startofvertices, + self.data[i+2] + startofvertices) + i += 3 + else: + break + + def __ring_vertices(self, steps, i, piece_size): + patlen = 1 + dtheta = math.pi * 2 / steps + + if self.data[i] == PATTERN: + patlen = self.data[i+1] + i += 2 + + if self.data[i] == 0: steps = 1 + + endindex = i + patlen * 2 + + for j in xrange(steps): + r = self.data[i + (j % patlen) * 2] * piece_size + y = self.data[i + (j % patlen) * 2 + 1] * piece_size + theta = dtheta * j + yield ((r * math.cos(theta), y, r * math.sin(theta)), endindex) + + def __ring_faces(self, basevertex, steps, prevbase, prevsteps): + if steps == 1: + for i in xrange(prevsteps): + yield (basevertex, prevbase + i, prevbase + (i-1)%prevsteps) + elif steps == prevsteps: + for i in xrange(steps): + yield (basevertex + i, prevbase + i, + prevbase + (i-1)%steps, basevertex + (i-1)%steps) + else: + j = 0 + i = 0 + while 1: + while j < prevsteps and steps*(1+2*j) < prevsteps*(1+2*i): + yield (basevertex + i%steps, + prevbase + (j+1)%prevsteps, + prevbase + j) + j += 1 + if i == steps: break + yield (basevertex + i, basevertex + (i+1)%steps, + prevbase + j%prevsteps) + i += 1 + +class Pawn(SimpleModel): + """ + """ + data = (SPIN,16, + 7395,0,7395,609, + SEAM,7102,910,7345,1199,7345,1572,7191,1910, + STEPDOWN,5826,2484,4941,3446,4625,4781, + STEPUP,4492,6371,4358,6508, + STEPDOWN,3691,6794,2912,7657,2473,10091, + SEAM,2100,15344, + STEPUP,4518,15697,4695,15900,4649,16218,4509,16382, + STEPDOWN,SEAM,3150,16755,STEPUP,3858,17678,4303,18752,4455,19905, + 4303,21058,3858,22132, + STEPDOWN,3150,23055,2227,23763,STEPDOWN,1153,24208,0,24360, + ENDOFDATA) + +class Rook(SimpleModel): + """ + """ + + data = (SPIN,20, + 9374,0,9374,756,SEAM,9003,1062,9311,1487, + 9311,1951,9116,2371,8521,3083,6701,5807,SEAM,6009,7595, + 6167,7812,6138,8066,5926,8460,5216,12608, + SEAM,4883,21434, + SEAM,5140,21608, + SEAM,5176,22792, + SEAM,5953,23030, + + SETBACKREF,0, + 6103,26819, + + SETBACKREF,1, + SPIN,20, + 5020,26819,5020,26114,4906,25858,0,25666, + + POLARQUADSTRIP,20,BACKREF,0,1,1,6143,27971,BACKREF,0,2,2,6143,27971, + BACKREF,0,3,3,6143,27971,BACKREF,0,4,4,6143,27971, + POLARQUADSTRIP,20,BACKREF,0,6,6,6143,27971,BACKREF,0,7,7,6143,27971, + BACKREF,0,8,8,6143,27971,BACKREF,0,9,9,6143,27971, + POLARQUADSTRIP,20,BACKREF,0,11,11,6143,27971,BACKREF,0,12,12,6143,27971, + BACKREF,0,13,13,6143,27971,BACKREF,0,14,14,6143,27971, + POLARQUADSTRIP,20,BACKREF,0,16,16,6143,27971,BACKREF,0,17,17,6143,27971, + BACKREF,0,18,18,6143,27971,BACKREF,0,19,19,6143,27971, + + POLARQUADSTRIP,20,1,5053,27971,BACKREF,1,1,2,5053,27971,BACKREF,1,2, + 3,5053,27971,BACKREF,1,3,4,5053,27971,BACKREF,1,4, + POLARQUADSTRIP,20,6,5053,27971,BACKREF,1,6,7,5053,27971,BACKREF,1,7, + 8,5053,27971,BACKREF,1,8,9,5053,27971,BACKREF,1,9, + POLARQUADSTRIP,20,11,5053,27971,BACKREF,1,11,12,5053,27971,BACKREF,1,12, + 13,5053,27971,BACKREF,1,13,14,5053,27971,BACKREF,1,14, + POLARQUADSTRIP,20,16,5053,27971,BACKREF,1,16,17,5053,27971,BACKREF,1,17, + 18,5053,27971,BACKREF,1,18,19,5053,27971,BACKREF,1,19, + + POLARQUADSTRIP,20,1,5020,26819,1,6103,26819, + 0,5020,26819,0,6103,26819,19,5020,26819,19,6103,26819, + POLARQUADSTRIP,20,6,5020,26819,6,6103,26819, + 5,5020,26819,5,6103,26819,4,5020,26819,4,6103,26819, + POLARQUADSTRIP,20,11,5020,26819,11,6103,26819, + 10,5020,26819,10,6103,26819,9,5020,26819,9,6103,26819, + POLARQUADSTRIP,20,16,5020,26819,16,6103,26819, + 15,5020,26819,15,6103,26819,14,5020,26819,14,6103,26819, + + POLARQUADSTRIP,20,1,5053,27971,1,6143,27971,1,5020,26819,1,6103,26819, + POLARQUADSTRIP,20,4,5020,26819,4,6103,26819,4,5053,27971,4,6143,27971, + POLARQUADSTRIP,20,6,5053,27971,6,6143,27971,6,5020,26819,6,6103,26819, + POLARQUADSTRIP,20,9,5020,26819,9,6103,26819,9,5053,27971,9,6143,27971, + POLARQUADSTRIP,20,11,5053,27971,11,6143,27971,11,5020,26819,11,6103,26819, + POLARQUADSTRIP,20,14,5020,26819,14,6103,26819,14,5053,27971,14,6143,27971, + POLARQUADSTRIP,20,16,5053,27971,16,6143,27971,16,5020,26819,16,6103,26819, + POLARQUADSTRIP,20,19,5020,26819,19,6103,26819,19,5053,27971,19,6143,27971, + + POLARQUADSTRIP,20,1,6143,27971,1,5053,27971,2,6143,27971,2,5053,27971, + 3,6143,27971,3,5053,27971,4,6143,27971,4,5053,27971, + POLARQUADSTRIP,20,6,6143,27971,6,5053,27971,7,6143,27971,7,5053,27971, + 8,6143,27971,8,5053,27971,9,6143,27971,9,5053,27971, + POLARQUADSTRIP,20,11,6143,27971,11,5053,27971,12,6143,27971,12,5053,27971, + 13,6143,27971,13,5053,27971,14,6143,27971,14,5053,27971, + POLARQUADSTRIP,20,16,6143,27971,16,5053,27971,17,6143,27971,17,5053,27971, + 18,6143,27971,18,5053,27971,19,6143,27971,19,5053,27971, + ENDOFDATA) + +class Knight(SimpleModel): + """ + """ + data = (VERTICES, SETBACKREF,0, 7910,8863,0, 7790,8863,1326, 7433,8863,2611, + 6850,8863,3817, 6059,8863,4907, 5084,8863,5847, 3955,8863,6611, + 2705,8863,7173, 1373,8863,7517, 0,8863,7633, -1373,8863,7517, + -2705,8863,7173, -3955,8863,6611, -5084,8863,5847, -6059,8863,4907, + -6850,8863,3817, -7433,8863,2611, -7790,8863,1326, -7910,8863,0, + -7790,8863,-1326, -7433,8863,-2611, -6850,8863,-3817, + -6059,8863,-4907, -5066,8863,-5896, -3955,8863,-6611, + -2705,8863,-7173, -1373,8863,-7517, 0,8863,-7633, 1373,8863,-7517, + 2705,8863,-7173, 3955,8863,-6611, 5066,8863,-5896, 6059,8863,-4907, + 6850,8863,-3817, 7433,8863,-2611, 7790,8863,-1326, -1183,11744,7939, + -1183,12003,7939, -1183,14019,6547, -1183,16307,5288, + -1183,16555,5281, -1183,20128,2191, -1134,20304,2131, + -1183,20516,2156, -1417,21874,1842, -1417,23109,2185, + -1417,23961,3121, -1417,24001,4252, 0,23917,5637, -1418,23893,5418, + -1151,23389,6664, -1151,23501,6906, -1151,23806,6987, + -1151,24102,6987, -1151,24209,7189, -1151,24371,7513, + -1151,24605,7715, -1151,24939,7674, -1313,25568,7149, + -1313,25695,7149, -1598,26707,7610, 0,26837,7841, 0,27354,8076, + -1598,27262,7839, -1598,27842,7723, 0,27919,7998, 0,28449,7606, + -1598,28309,7303, -1302,28414,6723, 0,28544,6980, 0,28540,6197, + -1187,28523,5990, -1304,28447,4204, -1158,28789,1627, + -561,28931,-1220, -357,29608,-1244, -357,30527,-1441, + -357,31249,-1837, -357,31511,-2627, -357,31511,-3484, + -357,31118,-4143, -357,30264,-4538, -436,29406,-5256, 0,29409,-5243, + -2207,29018,-6763, -914,28658,-6964, 0,26292,-7237, -1305,26324,-7143, + -806,23401,-6784, -812,20723,-6228, -796,16757,-6210, + -1559,24934,7435, -1566,24633,7460, -1531,24429,7334, + -1475,24293,7131, -1440,24203,7004, -1372,23935,7015, + -1364,23606,6868, -1389,23515,6705, -1687,28010,6952, + -1687,27926,7343, -1687,27629,7491, -1687,27324,7552, + -1687,27032,7432, -1687,26791,7148, -1642,27135,7165, + -1642,27254,7304, -1642,27397,7364, -1642,27546,7334, + -1642,27693,7261, -1642,27737,7088, -1611,10591,8159, + -888,9327,-8560, -4491,13292,1032, -3840,15084,786, -3412,17397,397, + -2937,20005,-35, -5108,11669,1240, -6344,10251,1395, -6345,10246,1248, + -5109,11664,1092, -2964,20022,-132, -3413,17393,250, -3841,15079,638, + -4491,13288,885, -3743,13207,-535, -3085,15092,-710, -2727,17642,-878, + -2569,20636,-797, -4348,11575,-324, -5584,10108,-169, + -5403,10079,-1732, -4167,11644,-1888, -2465,20842,-2651, + -2522,18130,-2392, -2905,15407,-2077, -3562,13227,-1951, + -3901,13568,-3294, -3243,15993,-3519, -2861,18863,-3735, + -2776,22447,-4309, -4486,11792,-3132, -5783,9930,-2931, + -5783,9930,-3054, -4486,11792,-3255, -2776,22438,-4430, + -2861,18863,-3858, -3243,15993,-3641, -3901,13568,-3417, + -6199,9466,4558, -5766,10642,5726, -5228,11829,6090, -4801,12891,6048, + -4155,14560,5246, -3546,16847,3719, -3334,17643,2937, + -2860,20062,1230, -4822,12054,7102, -5325,10909,7208, + -5732,9938,6026, -4026,14701,5522, -2852,20170,1447, -3319,17801,3175, + -3538,16955,3935, -4090,13256,1679, -2874,20073,676, -3706,15083,1464, + -3301,17348,1117, -5931,10206,1996, -4695,11624,1840, + -4445,11658,2844, -5681,10240,3000, -3074,17398,2202, + -3456,15117,2468, -3840,13290,2683, -4642,13358,4019, + -4257,15194,3908, -6017,10188,3746, -5237,11714,4039, + -5621,11218,5077, -5026,12862,5058, -5134,10861,-3154, + -4193,12680,-3336, -2832,20609,-4118, -3052,17428,-3750, + -3572,14780,-3529, -3579,14774,-3623, -3059,17422,-3843, + -2839,20596,-4211, -4200,12674,-3429, -5141,10855,-3248, + -3908,13562,-3510, -3250,15987,-3735, -2868,18857,-3952, + -4492,11786,-3348, -5789,9924,-3148, -4817,11321,-3298, + -5465,10389,-3198, -4054,13118,-3470, -4346,12230,-3389, + -2853,19681,-4056, -2825,21511,-4367, -3155,16705,-3789, + -2963,18140,-3897, -3744,14168,-3567, -3415,15381,-3679, + -845,13482,-6604, -945,10997,-7893, -981,11110,-7735, + -859,13469,-6526, -899,9307,-8439, -813,16486,-6146, -832,20681,-6100, + -826,23358,-6656, -882,10149,-8148, -913,12323,-7139, + -822,14910,-6353, -817,18235,-6210, -827,21937,-6382, + -846,21937,-6341, -841,14912,-6313, -931,12326,-7098, + -901,10152,-8107, -844,23359,-6615, -851,20681,-6059, + -831,16487,-6105, -919,9307,-8399, -869,13482,-6492, -999,11113,-7694, + -860,9726,-8255, -942,10578,-7958, -962,11715,-7413, -900,12896,-6796, + -863,14065,-6404, -818,15760,-6221, -828,17367,-6171, + -854,19800,-5994, -848,21309,-6200, -843,22566,-6482, + -3407,15409,-3785, -3736,14196,-3673, -2911,18365,-4048, + -3147,16733,-3895, -2726,21608,-4455, -2808,19905,-4191, + -4338,12259,-3495, -4046,13146,-3576, -5457,10418,-3304, + -4809,11349,-3404, -4484,11815,-3454, -2715,22288,-4534, + -2832,19035,-4066, -3242,16015,-3841, -3900,13590,-3616, + -5133,10883,-3354, -4171,12742,-3524, -2803,20773,-4339, + -3051,17450,-3949, -3571,14803,-3729, -4979,11072,-3368, + -5619,10185,-3279, -4119,12924,-3556, -4411,12037,-3475, + -2801,20363,-4269, -2721,21826,-4489, -3099,17092,-3922, + -2887,18618,-4038, -3653,14499,-3701, -3325,15712,-3813, + -3489,15106,-3757, -3818,13893,-3645, -3011,17758,-3971, + -3194,16374,-3868, -2755,21085,-4385, -2815,19494,-4139, + -4265,12481,-3515, -3973,13368,-3596, -5304,10595,-3318, + -4664,11477,-3407, -5441,10207,-3361, -5350,10338,-3375, + -5139,10672,-3428, -5058,10742,-3431, -4831,11101,-3457, + -4742,11228,-3470, -4516,11559,-3504, -4423,11693,-3518, + -4277,12079,-3558, -4232,12180,-3571, -4138,12522,-3597, + -4094,12603,-3609, -3979,12935,-3675, -3943,13043,-3671, + -3852,13383,-3699, -3813,13496,-3705, -3692,13947,-3767, + -3653,14073,-3790, -3506,14587,-3772, -3467,14685,-3777, + -3349,15192,-3888, -3308,15317,-3887, -3162,15800,-3957, + -3119,15954,-3961, -3009,16528,-3946, -3002,16637,-3937, + -2914,17260,-4014, -2909,17347,-4006, -2834,17893,-4049, + -2813,18060,-4060, -2760,18849,-4232, -2746,18968,-4242, + -850,22569,-6438, -855,21313,-6156, -861,19804,-5949, + -834,17371,-6127, -824,15765,-6176, -869,14070,-6360, + -906,12901,-6751, -968,11720,-7368, -948,10645,-7895, -865,9731,-8211, + -1006,11164,-7630, -875,13487,-6447, -926,9310,-8354, + -837,16490,-6061, -857,20684,-6015, -867,23379,-6515, + -907,10157,-8062, -937,12331,-7053, -847,14917,-6268, + -842,18236,-6111, -852,21941,-6297, -885,9891,-8156, -985,10905,-7724, + -947,11969,-7255, -885,13125,-6649, -858,14493,-6314, + -839,16049,-6111, -836,17735,-6083, -859,20244,-5982, + -891,21559,-6157, -848,22883,-6508, -851,22255,-6367, + -856,20999,-6085, -867,19042,-6000, -836,16930,-6094, + -835,15341,-6222, -880,13646,-6405, -916,12540,-6953, + -981,11337,-7564, -927,10370,-7988, -932,9518,-8285, -931,9583,-8174, + -863,9671,-8145, -892,9982,-8037, -903,10075,-7996, -952,10454,-7865, + -961,10550,-7832, -1008,10963,-7617, -1018,11105,-7567, + -996,11458,-7405, -992,11582,-7342, -972,12080,-7057, + -968,12210,-6984, -935,12620,-6792, -931,12759,-6714, + -914,13199,-6441, -910,13346,-6359, -915,13767,-6194, + -910,13941,-6175, -890,14633,-6107, -886,14783,-6091, + -864,15476,-6035, -859,15645,-6017, -870,16062,-5957, + -873,16231,-5936, -853,17048,-5965, -861,17185,-5974, + -879,17812,-5997, -885,17961,-5997, -851,18486,-6138, + -865,18526,-5933, -872,18761,-5875, -870,19337,-5971, + -864,19470,-5784, -937,19590,-5786, -833,19023,-6126, + -851,20344,-5894, -851,20576,-5912, -855,21086,-5973, + -854,21224,-6003, -871,21651,-6070, -852,21853,-6116, + -850,22332,-6224, -849,22490,-6259, -831,22924,-6338, + -834,23240,-6364, -2743,19661,-4287, -2740,19863,-4312, + -2723,20501,-4469, -2705,20711,-4503, -2661,21262,-4565, + -2658,21427,-4593, -2687,22088,-4631, -2695,22209,-4643, + -2774,23170,-4392, -2633,23103,-4527, -2681,22439,-4516, + -2656,22665,-4573, -2552,22727,-4653, -2534,23015,-4628, + -2607,23397,-4556, -2759,23681,-4398, -2740,24136,-4370, + -2610,24100,-4580, -2412,24481,-5089, -2671,24389,-4569, + -2397,24935,-5652, -2562,25022,-5528, -939,23708,-6566, + -1009,24359,-6776, -1095,24912,-6886, -1049,24117,-6493, + -990,23894,-6415, -2290,23837,-4814, -2312,23612,-4723, + -2183,24804,-5574, -2191,24632,-5391, -1156,24771,-6650, + -1154,24624,-6586, -1104,24526,-6656, -1281,9161,-8432, + -2524,9161,-8046, -3691,9161,-7572, -4745,9161,-6945, + -5662,9127,-6096, -3697,9192,-7581, -2531,9192,-8056, + -4753,9183,-6951, -5664,9161,-6101, -5664,9185,-6101, + -4753,9207,-6951, -2531,9216,-8055, -3697,9216,-7581, + -3684,9232,-7560, -2518,9232,-8034, -4740,9223,-6930, + -5651,9200,-6080, -2828,20259,1366, -2798,20256,1187, -2820,20237,692, + -2843,20292,-25, -2874,20473,735, -2975,20508,90, -2474,20772,-759, + -2660,20879,-691, -2471,21553,-1445, -2498,22175,-1940, + -2471,24346,-2830, -2556,22373,-1903, -2528,21595,-1351, + -2493,24314,-2936, -2576,22239,-2123, -2548,21461,-1571, + -2823,20635,1307, -2741,20503,1328, -1490,23972,5392, + -1489,24080,4226, -1489,24040,3095, -1489,23188,2159, + -1489,21953,1816, -2479,22156,182, -1515,23830,2117, -1605,24415,3131, + -1599,24460,4263, -1580,24357,5431, -1448,23885,6743, + -1252,24770,5442, -1271,24873,4274, -1277,24828,3142, + -1389,24618,2016, -1479,24733,1993, -1301,25010,3165, + -1295,25055,4297, -1391,24890,5460, -1512,25216,5507, + -1602,25353,4317, -1608,25308,3186, -1979,23458,-29, -1320,25312,1928, + -1495,25055,546, -2438,25790,-488, -1605,25585,5599, -1583,26793,6150, + -1850,26272,4298, -1578,27435,7117, -1658,27625,6884, + -1671,27398,6846, -1678,27176,6937, -1715,27784,6626, + -1744,27323,6551, -1753,26904,6776, -1884,28809,-1688, + -1555,28655,1620, -1658,28252,4204, -1581,28263,5997, + -1567,28312,6790, -1935,27827,4220, -1818,28249,1603, + -1570,27742,6195, -1532,27288,6120, -2038,27074,4245, -1418,28416,228, + -413,30264,-4539, -424,31094,-4152, -418,31478,-3497, + -410,31453,-2653, -410,31190,-1863, -410,30468,-1467, + -410,29550,-1270, -411,31041,-1902, -448,31253,-2696, + -449,31323,-3466, -425,31182,-3496, -411,31060,-2738, + -387,30899,-1932, -316,30806,-1957, -335,30952,-2766, + -353,31089,-3521, -322,31007,-3540, -303,30871,-2786, + -284,30725,-1976, -304,30630,-1993, -323,30776,-2802, + -342,30912,-3557, -396,30843,-3565, -378,30707,-2811, + -358,30561,-2001, -414,30477,-2013, -433,30623,-2822, + -452,30759,-3576, -474,30560,-3616, -456,30424,-2861, + -436,30278,-2052, -349,30194,-2077, -368,30340,-2886, + -386,30477,-3641, -309,30389,-3665, -291,30253,-2911, + -271,30107,-2102, -267,29996,-2124, -286,30142,-2933, + -305,30278,-3688, -376,30179,-3700, -357,30043,-2945, + -338,29897,-2136, -420,29789,-2150, -439,29935,-2959, + -458,30071,-3713, -500,29883,-3751, -463,29601,-2187, + -470,30974,-4049, -462,30153,-4391, -466,30563,-4220, + -410,30009,-1368, -725,29372,-3225, -677,29335,-5130, + -908,29205,-3300, -632,29533,-4803, -2587,28768,-3408, + -2730,28611,-3456, -2404,28568,-6916, -2413,28849,-6612, + -1904,26715,1801, -1394,25919,831, -2224,27270,-3994, + -2730,28438,-3011, -2587,28570,-2864, -2826,28152,-3163, + -2476,28668,-6523, -2476,28433,-6776, -2787,28338,-3507, + -2216,27658,-3972, -2207,27950,-5907, -2311,28216,-6128, + -2320,27876,-3971, -2311,28118,-6301, -2311,27845,-6252, + -2320,27703,-3749, -2320,27084,-3798, -2320,26986,-4070, + -2698,26705,-4014, -2557,26974,-3547, -2557,27739,-3485, + -2572,27738,-6362, -2436,28204,-6533, -2436,28421,-6303, + -2556,28092,-3822, -2431,28142,-6975, -2544,27863,-6726, + -2266,28229,-1686, -1939,28156,-1065, -2569,27943,-2270, + -2381,27545,-1682, -2799,27594,-3104, -2750,27866,-2738, + -2539,27578,-6309, -2720,26853,-3133, -2788,26461,-3776, + -1061,27543,-7078, -1780,27941,135, -1859,27747,-527, -1756,27784,691, + -1855,27001,736, -1950,26711,19, -1964,27006,-630, -2342,26955,-1898, + -2595,25515,-3113, -2147,27855,132, -2126,27722,601, -2213,27691,-428, + -2210,27061,639, -2290,26816,34, -2302,27065,-515, -2347,27680,100, + -2335,27602,376, -2386,27584,-229, -2385,27213,398, -2432,27069,42, + -2439,27215,-281, -2481,27452,103, -1786,24263,399, -2278,24851,-562, + -2372,25143,-1163, 7383,9172,0, 7270,9172,-1487, 6937,9172,-2929, + 6393,9172,-4281, 1282,9172,-8433, 0,9172,-8563, 1301,9439,8159, + 2371,9313,7844, 3857,9286,7355, 4477,9172,6559, 5704,9174,5179, + 6393,9172,4281, 6937,9172,2929, 7270,9172,1487, -7270,9172,-1487, + -6937,9172,-2929, -6393,9172,-4281, -1282,9172,-8433, 0,9339,8274, + -1301,9439,8159, -2371,9313,7844, -3857,9286,7355, -4477,9172,6559, + -5704,9174,5179, -6393,9172,4281, -6937,9172,2929, -7270,9172,1487, + -796,9467,8260, 0,9503,8356, 0,9667,8438, -796,9666,8325, + -807,10584,8327, -7383,9172,0, 796,9467,8260, 796,9666,8325, + 807,10584,8327, 0,10584,8457, 0,11744,8130, 1183,11744,7939, + 1183,12003,7939, 0,12003,8130, 0,14019,6737, 1183,14019,6547, + 1183,16307,5288, 0,16307,5479, 0,16555,5472, 1183,16555,5281, + 1183,20128,2191, 0,20128,2382, 0,20304,2322, 1134,20304,2131, + 1183,20516,2156, 0,20516,2346, 0,21898,2060, 1417,21874,1842, + 1417,23109,2185, 0,23133,2404, 0,23985,3339, 1417,23961,3121, + 1417,24001,4252, 0,24025,4470, 1418,23893,5418, 1151,23389,6664, + 0,23394,6882, 0,23506,7125, 1151,23501,6906, 1151,23806,6987, + 0,23811,7205, 0,24107,7205, 1151,24102,6987, 1151,24209,7189, + 0,24213,7407, 0,24376,7731, 1151,24371,7513, 1151,24605,7715, + 0,24610,7933, 0,24944,7892, 1151,24939,7674, 1313,25568,7149, + 0,25562,7367, 0,25689,7367, 1313,25695,7149, 1598,26707,7610, + 1598,27262,7839, 1598,27842,7723, 1598,28309,7303, 1302,28414,6723, + 1187,28523,5990, 1304,28447,4204, 0,28469,4435, 0,28654,1893, + 1158,28789,1627, 561,28931,-1220, 0,29310,-864, 0,29574,-1062, + 357,29608,-1244, 357,30527,-1441, 0,30496,-1259, 0,31221,-1655, + 357,31249,-1837, 357,31511,-2627, 0,31485,-2445, 0,31485,-3302, + 357,31511,-3484, 357,31118,-4143, 0,31089,-3961, 0,30233,-4356, + 357,30264,-4538, 436,29406,-5256, 0,29018,-6407, 2207,29018,-6763, + 914,28658,-6964, 0,28472,-7040, 1305,26324,-7143, 806,23401,-6784, + 0,23246,-6890, 0,20735,-6319, 812,20723,-6228, 796,16757,-6210, + 0,17171,-6133, 1559,24934,7435, 1566,24633,7460, 1531,24429,7334, + 1475,24293,7131, 1440,24203,7004, 1372,23935,7015, 1364,23606,6868, + 1389,23515,6705, 1687,28010,6952, 1687,27926,7343, 1687,27629,7491, + 1687,27324,7552, 1687,27032,7432, 1687,26791,7148, 1642,27135,7165, + 1642,27254,7304, 1642,27397,7364, 1642,27546,7334, 1642,27693,7261, + 1642,27737,7088, 1611,10591,8159, 888,9327,-8560, 4491,13292,1032, + 3840,15084,786, 3412,17397,397, 2937,20005,-35, 5108,11669,1240, + 6344,10251,1395, 6345,10246,1248, 5109,11664,1092, 2964,20022,-132, + 3413,17393,250, 3841,15079,638, 4491,13288,885, 3743,13207,-535, + 3085,15092,-710, 2727,17642,-878, 2569,20636,-797, 4348,11575,-324, + 5584,10108,-169, 5403,10079,-1732, 4167,11644,-1888, + 2465,20842,-2651, 2522,18130,-2392, 2905,15407,-2077, + 3562,13227,-1951, 3901,13568,-3294, 3243,15993,-3519, + 2861,18863,-3735, 2776,22447,-4309, 4486,11792,-3132, 5783,9930,-2931, + 5783,9930,-3054, 4486,11792,-3255, 2776,22438,-4430, 2861,18863,-3858, + 3243,15993,-3641, 3901,13568,-3417, 6199,9466,4558, 5766,10642,5726, + 5228,11829,6090, 4801,12891,6048, 4155,14560,5246, 3546,16847,3719, + 3334,17643,2937, 2860,20062,1230, 4822,12054,7102, 5325,10909,7208, + 5732,9938,6026, 4026,14701,5522, 2852,20170,1447, 3319,17801,3175, + 3538,16955,3935, 4090,13256,1679, 2874,20073,676, 3706,15083,1464, + 3301,17348,1117, 5931,10206,1996, 4695,11624,1840, 4445,11658,2844, + 5681,10240,3000, 3074,17398,2202, 3456,15117,2468, 3840,13290,2683, + 4642,13358,4019, 4257,15194,3908, 6017,10188,3746, 5237,11714,4039, + 5621,11218,5077, 5026,12862,5058, 5134,10861,-3154, 4193,12680,-3336, + 2832,20609,-4118, 3052,17428,-3750, 3572,14780,-3529, + 3579,14774,-3623, 3059,17422,-3843, 2839,20596,-4211, + 4200,12674,-3429, 5141,10855,-3248, 3908,13562,-3510, + 3250,15987,-3735, 2868,18857,-3952, 4492,11786,-3348, 5789,9924,-3148, + 4817,11321,-3298, 5465,10389,-3198, 4054,13118,-3470, + 4346,12230,-3389, 2853,19681,-4056, 2825,21511,-4367, + 3155,16705,-3789, 2963,18140,-3897, 3744,14168,-3567, + 3415,15381,-3679, 0,14037,-6616, 845,13482,-6604, 945,10997,-7893, + 0,11066,-7866, 981,11110,-7735, 859,13469,-6526, 899,9307,-8439, + 813,16486,-6146, 832,20681,-6100, 826,23358,-6656, 882,10149,-8148, + 913,12323,-7139, 822,14910,-6353, 817,18235,-6210, 827,21937,-6382, + 846,21937,-6341, 841,14912,-6313, 931,12326,-7098, 901,10152,-8107, + 844,23359,-6615, 851,20681,-6059, 831,16487,-6105, 919,9307,-8399, + 869,13482,-6492, 999,11113,-7694, 860,9726,-8255, 942,10578,-7958, + 962,11715,-7413, 900,12896,-6796, 863,14065,-6404, 818,15760,-6221, + 828,17367,-6171, 854,19800,-5994, 848,21309,-6200, 843,22566,-6482, + 3407,15409,-3785, 3736,14196,-3673, 2911,18365,-4048, + 3147,16733,-3895, 2726,21608,-4455, 2808,19905,-4191, + 4338,12259,-3495, 4046,13146,-3576, 5457,10418,-3304, + 4809,11349,-3404, 4484,11815,-3454, 2715,22288,-4534, + 2832,19035,-4066, 3242,16015,-3841, 3900,13590,-3616, + 5133,10883,-3354, 4171,12742,-3524, 2803,20773,-4339, + 3051,17450,-3949, 3571,14803,-3729, 4979,11072,-3368, + 5619,10185,-3279, 4119,12924,-3556, 4411,12037,-3475, + 2801,20363,-4269, 2721,21826,-4489, 3099,17092,-3922, + 2887,18618,-4038, 3653,14499,-3701, 3325,15712,-3813, + 3489,15106,-3757, 3818,13893,-3645, 3011,17758,-3971, + 3194,16374,-3868, 2755,21085,-4385, 2815,19494,-4139, + 4265,12481,-3515, 3973,13368,-3596, 5304,10595,-3318, + 4664,11477,-3407, 5441,10207,-3361, 5350,10338,-3375, + 5139,10672,-3428, 5058,10742,-3431, 4831,11101,-3457, + 4742,11228,-3470, 4516,11559,-3504, 4423,11693,-3518, + 4277,12079,-3558, 4232,12180,-3571, 4138,12522,-3597, + 4094,12603,-3609, 3979,12935,-3675, 3943,13043,-3671, + 3852,13383,-3699, 3813,13496,-3705, 3692,13947,-3767, + 3653,14073,-3790, 3506,14587,-3772, 3467,14685,-3777, + 3349,15192,-3888, 3308,15317,-3887, 3162,15800,-3957, + 3119,15954,-3961, 3009,16528,-3946, 3002,16637,-3937, + 2914,17260,-4014, 2909,17347,-4006, 2834,17893,-4049, + 2813,18060,-4060, 2760,18849,-4232, 2746,18968,-4242, 850,22569,-6438, + 855,21313,-6156, 860,19804,-5949, 834,17371,-6127, 824,15765,-6176, + 869,14070,-6360, 906,12901,-6751, 968,11720,-7368, 948,10645,-7895, + 865,9731,-8211, 1006,11164,-7630, 875,13487,-6447, 926,9310,-8354, + 837,16490,-6061, 857,20684,-6015, 867,23379,-6515, 907,10157,-8062, + 937,12331,-7053, 847,14917,-6268, 842,18236,-6111, 852,21941,-6297, + 885,9891,-8156, 985,10905,-7724, 947,11969,-7255, 885,13125,-6649, + 858,14493,-6314, 839,16049,-6111, 836,17735,-6083, 859,20244,-5982, + 891,21559,-6157, 848,22883,-6508, 851,22255,-6367, 856,20999,-6085, + 867,19042,-6000, 836,16930,-6094, 835,15341,-6222, 880,13646,-6405, + 916,12540,-6953, 981,11337,-7564, 927,10370,-7988, 932,9518,-8285, + 931,9583,-8174, 863,9671,-8145, 892,9982,-8037, 903,10075,-7996, + 952,10454,-7865, 961,10550,-7832, 1008,10963,-7617, 1018,11105,-7567, + 996,11458,-7405, 992,11582,-7342, 972,12080,-7057, 968,12210,-6984, + 935,12620,-6792, 931,12759,-6714, 914,13199,-6441, 910,13346,-6359, + 915,13767,-6194, 910,13941,-6175, 890,14633,-6107, 886,14783,-6091, + 864,15476,-6035, 859,15645,-6017, 870,16062,-5957, 873,16231,-5936, + 853,17048,-5965, 861,17185,-5974, 879,17812,-5997, 885,17961,-5997, + 851,18486,-6138, 865,18526,-5933, 872,18761,-5875, 870,19337,-5971, + 864,19470,-5784, 937,19590,-5786, 833,19023,-6126, 851,20344,-5894, + 851,20576,-5912, 855,21086,-5973, 854,21224,-6003, 871,21651,-6070, + 852,21853,-6116, 850,22332,-6224, 849,22490,-6259, 831,22924,-6338, + 834,23240,-6364, 2743,19661,-4287, 2740,19863,-4312, 2723,20501,-4469, + 2705,20711,-4503, 2661,21262,-4565, 2658,21427,-4593, + 2687,22088,-4631, 2695,22209,-4643, 2774,23170,-4392, + 2633,23103,-4527, 2681,22439,-4516, 2656,22665,-4573, + 2552,22727,-4653, 2534,23015,-4628, 2607,23397,-4556, + 2759,23681,-4398, 2740,24136,-4370, 2610,24100,-4580, + 2412,24481,-5089, 2671,24389,-4569, 2397,24935,-5652, + 2562,25022,-5528, 939,23708,-6566, 1009,24359,-6776, 1095,24912,-6886, + 1049,24117,-6493, 990,23894,-6415, 2290,23837,-4814, 2312,23612,-4723, + 2183,24804,-5574, 2191,24632,-5391, 1156,24771,-6650, + 1154,24624,-6586, 1104,24526,-6656, 0,9161,-8562, 1281,9161,-8432, + 2524,9161,-8046, 3691,9161,-7572, 4745,9161,-6945, 5662,9127,-6096, + 3697,9192,-7581, 2531,9192,-8056, 4753,9183,-6951, 5664,9161,-6101, + 5664,9185,-6101, 4753,9207,-6951, 2531,9216,-8055, 3697,9216,-7581, + 3684,9232,-7560, 2518,9232,-8034, 4740,9223,-6930, 5651,9200,-6080, + 2828,20259,1366, 2798,20256,1187, 2820,20237,692, 2843,20292,-25, + 2874,20473,735, 2975,20508,90, 2474,20772,-759, 2660,20879,-691, + 2471,21553,-1445, 2498,22175,-1940, 2471,24346,-2830, + 2556,22373,-1903, 2528,21595,-1351, 2493,24314,-2936, + 2576,22239,-2123, 2548,21461,-1571, 2823,20635,1307, 2741,20503,1328, + 1490,23972,5392, 1489,24080,4226, 1489,24040,3095, 1489,23188,2159, + 1489,21953,1816, 2479,22156,182, 1515,23830,2117, 1605,24415,3131, + 1599,24460,4263, 1580,24357,5431, 1448,23885,6743, 1252,24770,5442, + 1271,24873,4274, 1277,24828,3142, 1389,24618,2016, 1479,24733,1993, + 1301,25010,3165, 1295,25055,4297, 1391,24890,5460, 1512,25216,5507, + 1602,25353,4317, 1608,25308,3186, 1979,23458,-29, 1320,25312,1928, + 1495,25055,546, 2438,25790,-488, 1605,25585,5599, 1583,26793,6150, + 1850,26272,4298, 1578,27435,7117, 1658,27625,6884, 1671,27398,6846, + 1678,27176,6937, 1715,27784,6626, 1744,27323,6551, 1753,26904,6776, + 1884,28809,-1688, 1555,28655,1620, 1658,28252,4204, 1581,28263,5997, + 1567,28312,6790, 1935,27827,4220, 1818,28249,1603, 1570,27742,6195, + 1532,27288,6120, 2038,27074,4245, 1418,28416,228, 413,30264,-4539, + 424,31094,-4152, 418,31478,-3497, 410,31453,-2653, 410,31190,-1863, + 410,30468,-1467, 410,29550,-1270, 411,31041,-1902, 448,31253,-2696, + 449,31323,-3466, 425,31182,-3496, 411,31060,-2738, 387,30899,-1932, + 316,30806,-1957, 335,30952,-2766, 353,31089,-3521, 322,31007,-3540, + 303,30871,-2786, 284,30725,-1976, 304,30630,-1993, 323,30776,-2802, + 342,30912,-3557, 396,30843,-3565, 378,30707,-2811, 358,30561,-2001, + 414,30477,-2013, 433,30623,-2822, 452,30759,-3576, 474,30560,-3616, + 456,30424,-2861, 436,30278,-2052, 349,30194,-2077, 368,30340,-2886, + 386,30477,-3641, 309,30389,-3665, 291,30253,-2911, 271,30107,-2102, + 267,29996,-2124, 286,30142,-2933, 305,30278,-3688, 376,30179,-3700, + 357,30043,-2945, 338,29897,-2136, 420,29789,-2150, 439,29935,-2959, + 458,30071,-3713, 500,29883,-3751, 463,29601,-2187, 470,30974,-4049, + 462,30153,-4391, 466,30563,-4220, 410,30009,-1368, 725,29372,-3225, + 677,29335,-5130, 908,29205,-3300, 632,29533,-4803, 2587,28768,-3408, + 2730,28611,-3456, 2404,28568,-6916, 2413,28849,-6612, + 1904,26715,1801, 1394,25919,831, 2224,27270,-3994, 2730,28438,-3011, + 2587,28570,-2864, 2826,28152,-3163, 2476,28668,-6523, + 2476,28433,-6776, 2787,28338,-3507, 2216,27658,-3972, + 2207,27950,-5907, 2311,28216,-6128, 2320,27876,-3971, + 2311,28118,-6301, 2311,27845,-6252, 2320,27703,-3749, + 2320,27084,-3798, 2320,26986,-4070, 2698,26705,-4014, + 2557,26974,-3547, 2557,27739,-3485, 2572,27738,-6362, + 2436,28204,-6533, 2436,28421,-6303, 2556,28092,-3822, + 2431,28142,-6975, 2544,27863,-6726, 2266,28229,-1686, + 1939,28156,-1065, 2569,27943,-2270, 2381,27545,-1682, + 2799,27594,-3104, 2750,27866,-2738, 2539,27578,-6309, + 2720,26853,-3133, 2788,26461,-3776, 1061,27543,-7078, 1780,27941,135, + 1859,27747,-527, 1756,27784,691, 1855,27001,736, 1950,26711,19, + 1964,27006,-630, 2342,26955,-1898, 2595,25515,-3113, 2147,27855,132, + 2126,27722,601, 2213,27691,-428, 2210,27061,639, 2290,26816,34, + 2302,27065,-515, 2347,27680,100, 2335,27602,376, 2386,27584,-229, + 2385,27213,398, 2432,27069,42, 2439,27215,-281, 2481,27452,103, + 1786,24263,399, 2278,24851,-562, 2372,25143,-1163, -2567,23141,-2607, + -2513,23156,-2494, 2513,23156,-2494, 2567,23141,-2607, + + TRIANGLES, 657,656,159, 100,506,99, 1003,900,995, 1048,1009,901, + 1051,901,1009, 1070,1073,1071, 1070,803,808, 1071,1077,1070, + 1072,1006,1073, 1072,808,915, 1073,808,1072, 1077,1071,1076, + 1078,1283,1127, 1081,1078,1079, 1081,1079,1080, 1083,1082,1086, + 1083,747,1274, 1084,888,1085, 1085,878,1086, 1085,1086,1095, + 1086,747,1083, 1093,1095,1086, 1094,1095,1093, 658,657,149, + 1101,637,32, 1103,638,1108, 1105,637,1101, 1106,637,1105, + 1108,638,1111, 111,36,158, 111,653,665, 111,665,36, 1110,858,1112, + 1111,988,1110, 1111,638,891, 1111,891,988, 1112,858,1113, + 1113,637,1106, 1117,779,784, 112,214,207, 112,651,210, 1124,1297,1154, + 1125,1123,1128, 1125,1302,1154, 1127,1077,1078, 1129,791,796, + 1130,1118,1137, 1136,1130,1137, 1137,1118,1119, 1137,1119,1121, + 1137,1121,1126, 1142,758,759, 1142,759,760, 1142,760,761, + 1147,1146,1154, 1150,755,756, 1150,756,757, 1154,1302,1124, + 1155,1147,1154, 1155,1297,1156, 1155,1154,1297, 1156,1240,1155, + 1156,1298,1157, 1157,1240,1156, 1157,1299,1273, 1157,1282,1281, + 1158,1151,1152, 1161,768,1164, 1162,773,1161, 1162,1161,1163, + 1163,1161,1164, 1165,1172,762, 1168,726,1178, 1168,1267,1266, + 1169,1178,725, 1171,1172,1165, 1171,720,1172, 1171,1165,1175, + 1178,1169,1174, 1178,1174,1278, 1179,1229,1228, 118,660,168, + 1180,1229,1179, 1185,726,1226, 1186,1183,1184, 1186,1184,1191, + 119,660,118, 1191,1184,1192, 1192,1184,1197, 1197,1184,1198, + 1198,1184,1203, 1203,1184,1204, 121,116,451, 1210,1209,1230, + 1215,1210,1230, 1216,1215,1230, 1221,1216,1230, 1222,1185,1226, + 1222,1221,1230, 1223,1222,1226, 1224,1223,1225, 1225,1223,1226, + 1226,726,1231, 1227,1188,1189, 1227,1189,1194, 1227,1194,1195, + 1227,1195,1200, 1227,1200,1201, 1227,1201,1206, 1228,1224,1225, + 1228,1225,1234, 1229,1207,1212, 1229,1212,1213, 1229,1213,1218, + 1229,1218,1219, 1229,1219,1224, 1229,1180,1227, 1229,1224,1228, + 1230,729,1185, 1230,1185,1222, 1231,1225,1226, 1232,1179,1234, + 1233,744,1232, 1234,1179,1228, 1234,1225,1231, 1235,744,1233, + 1237,744,1238, 1239,1155,1240, 1240,1157,1239, 1243,1168,1266, + 1248,1241,1249, 1250,1249,1252, 1252,1249,1253, 1254,1248,1251, + 1256,1241,1255, 1259,1244,1270, 1264,745,1237, 1265,1260,1272, + 1267,1168,1178, 1267,1178,1277, 1269,1282,1270, 1270,1282,1273, + 1271,1268,1269, 1271,1269,1270, 1273,1283,1274, 1274,1078,1081, + 1274,1081,1083, 1274,747,1275, 1275,745,1264, 1275,1264,1265, + 1275,1265,1272, 1275,1272,1274, 1276,1178,1278, 1277,1178,1276, + 1280,1157,1281, 1281,1269,1277, 1282,1157,1273, 1282,1269,1281, + 1283,1078,1274, 1292,1290,1296, 1295,1292,1296, 1296,1290,1291, + 1296,1291,1293, 1296,1293,1294, 1296,1294,1295, 1297,1298,1156, + 1297,1124,1298, 1298,1124,1299, 1299,1124,1273, 1299,1157,1298, + 660,659,168, 133,128,463, 137,182,141, 138,185,137, 139,184,138, + 140,183,139, 141,181,142, 141,182,144, 142,181,143, 143,649,142, + 143,197,195, 144,196,181, 144,199,194, 145,183,140, 145,201,183, + 145,140,405, 146,184,139, 146,139,183, 146,203,184, 147,185,138, + 147,138,184, 147,205,185, 148,198,182, 148,137,185, 149,657,159, + 150,149,159, 151,150,157, 152,151,157, 153,152,157, 157,150,158, + 157,38,160, 157,37,38, 158,150,159, 160,153,157, 160,39,163, + 160,38,39, 162,41,161, 163,39,40, 168,659,171, 172,155,156, + 176,153,154, 176,154,155, 177,658,149, 179,149,150, 179,150,151, + 180,152,153, 181,197,143, 181,141,144, 181,196,190, 182,199,144, + 182,137,148, 182,198,189, 183,200,146, 184,202,147, 185,204,148, + 186,204,185, 186,267,204, 186,185,205, 187,202,184, 187,265,202, + 187,184,203, 188,200,183, 188,263,200, 188,183,201, 189,275,199, + 189,261,255, 190,277,197, 190,259,254, 191,276,198, 191,148,204, + 192,205,147, 192,147,202, 192,268,205, 193,203,146, 193,146,200, + 193,266,203, 194,278,196, 194,262,249, 196,259,190, 196,144,194, + 196,278,248, 197,181,190, 197,260,195, 197,277,247, 198,261,189, + 198,148,191, 198,276,246, 199,182,189, 199,262,194, 199,275,245, + 200,274,193, 201,273,188, 202,272,192, 203,271,187, 204,270,191, + 205,269,186, 206,215,209, 206,216,90, 207,215,206, 207,214,208, + 208,231,215, 208,230,228, 209,233,216, 209,232,227, 210,229,214, + 210,651,226, 211,235,217, 211,234,225, 212,237,218, 212,236,224, + 212,386,236, 213,238,223, 213,421,88, 214,230,208, 214,112,210, + 214,229,222, 215,207,208, 215,232,209, 215,231,221, 216,206,209, + 216,234,211, 216,233,220, 217,90,211, 217,386,212, 218,89,212, + 218,238,213, 218,237,219, 219,342,238, 219,340,331, 220,346,234, + 220,336,329, 221,348,232, 221,334,328, 222,350,230, 222,332,327, + 223,341,326, 224,343,237, 224,339,325, 225,345,235, 225,337,324, + 226,351,229, 226,651,445, 227,347,233, 227,335,322, 228,349,231, + 228,333,321, 229,332,222, 229,210,226, 229,351,320, 230,214,222, + 230,333,228, 230,350,319, 231,334,221, 231,208,228, 231,349,318, + 232,215,221, 232,335,227, 232,348,317, 233,336,220, 233,209,227, + 233,347,316, 234,216,220, 234,337,225, 234,346,315, 235,211,225, + 235,345,314, 236,339,224, 236,383,313, 236,344,383, 237,340,219, + 237,212,224, 237,343,312, 238,218,219, 238,341,223, 238,342,311, + 239,269,205, 239,205,268, 240,270,204, 240,204,267, 241,271,203, + 241,203,266, 242,272,202, 242,202,265, 243,273,201, 243,201,264, + 244,274,200, 244,200,263, 250,145,407, 251,266,193, 251,193,274, + 252,268,192, 252,192,272, 253,191,270, 256,263,188, 256,188,273, + 257,265,187, 257,187,271, 258,267,186, 258,186,269, 259,196,248, + 22,650,435, 260,197,247, 261,198,246, 262,199,245, 21,650,22, + 275,189,255, 276,191,253, 277,190,254, 278,194,249, 323,226,445, + 330,217,235, 330,235,338, 332,229,320, 333,230,319, 334,231,318, + 335,232,317, 336,233,316, 337,234,315, 338,235,314, 339,236,313, + 664,653,661, 340,237,312, 341,238,311, 342,219,331, 343,224,325, + 344,236,386, 345,225,324, 346,220,329, 347,227,322, 348,221,328, + 349,228,321, 665,653,664, 350,222,327, 351,226,323, 380,217,330, + 405,408,145, 405,412,406, 406,408,405, 407,145,408, 408,341,407, + 411,406,412, 413,412,461, 414,413,416, 415,414,416, 416,413,608, + 418,87,421, 418,416,608, 419,223,326, 420,213,223, 420,223,419, + 421,417,418, 421,213,420, 421,430,428, 421,87,88, 428,430,429, + 430,421,420, 435,650,439, 439,650,440, 440,650,447, 442,651,437, + 444,195,323, 444,323,445, 445,651,442, 446,195,444, 447,650,195, + 447,195,446, 453,452,471, 455,453,471, 460,455,471, 461,617,413, + 462,457,459, 465,43,44, 471,452,464, 471,464,470, 486,485,492, + 488,458,1301, 488,631,458, 488,1301,459, 488,480,481, 488,481,489, + 489,574,490, 490,631,489, 490,574,491, 490,632,631, 491,632,490, + 495,105,106, 495,106,107, 495,107,108, 495,108,109, 495,109,110, + 495,110,496, 497,495,496, 498,105,495, 498,495,497, 499,506,505, + 505,68,71, 506,68,505, 506,67,68, 508,503,512, 509,499,505, + 512,74,502, 512,502,601, 513,563,514, 518,517,520, 518,564,76, + 519,75,564, 519,74,75, 523,522,561, 525,518,520, 526,518,525, + 528,523,561, 529,528,561, 531,518,526, 532,518,531, 534,529,561, + 535,534,561, 537,518,532, 538,518,537, 540,535,561, 546,541,563, + 547,546,563, 552,547,563, 553,552,563, 556,519,564, 558,553,563, + 559,557,558, 559,558,562, 560,74,519, 560,519,556, 560,556,557, + 560,557,559, 560,559,565, 561,514,563, 562,563,513, 562,558,563, + 562,513,568, 564,543,544, 564,544,549, 564,549,550, 564,550,555, + 564,555,556, 565,74,560, 565,559,568, 566,84,567, 567,84,569, + 568,559,562, 568,513,566, 571,85,598, 571,84,85, 572,84,571, + 573,491,574, 574,489,573, 583,575,582, 585,582,588, 586,583,584, + 587,583,586, 589,575,590, 598,85,609, 599,598,609, 653,111,654, + 600,601,502, 600,502,577, 603,602,605, 604,578,593, 604,616,603, + 604,603,605, 606,594,599, 606,599,609, 607,633,491, 607,616,604, + 607,491,616, 607,458,633, 608,87,418, 608,617,607, 608,606,609, + 608,413,617, 609,87,608, 609,86,87, 610,512,611, 611,512,601, + 611,603,615, 612,508,512, 612,512,610, 615,616,491, 615,491,614, + 615,603,616, 625,624,630, 627,625,630, 628,627,630, 629,628,630, + 630,624,626, 630,626,629, 631,488,489, 632,458,631, 632,491,633, + 633,458,632, 639,651,112, 641,774,640, 32,637,33, 667,640,668, + 668,640,669, 669,640,774, 67,506,100, 672,669,774, 676,673,820, + 677,676,823, 680,677,826, 688,685,1131, 712,711,754, 720,719,1172, + 721,720,1171, 725,1178,726, 729,726,1185, 729,1230,730, 73,512,503, + 730,1230,1184, 74,512,73, 743,82,83, 745,744,1237, 745,743,744, + 745,1275,746, 746,85,743, 746,743,745, 746,609,85, 746,1275,86, + 747,86,1275, 748,747,1086, 748,883,751, 748,1086,878, 751,882,752, + 752,881,870, 76,564,75, 762,1172,763, 763,1172,719, 769,768,1161, + 770,769,1161, 771,770,1161, 772,771,1161, 773,772,1161, 774,641,642, + 775,638,639, 781,647,782, 655,654,111, 800,848,801, 801,847,802, + 801,848,810, 802,846,803, 802,847,809, 803,846,808, 804,845,800, + 805,844,804, 805,636,806, 806,844,805, 806,860,844, 807,845,804, + 807,804,844, 807,862,845, 808,1073,1070, 809,863,846, 809,866,856, + 810,865,847, 810,868,855, 811,800,845, 811,867,848, 812,644,645, + 812,645,840, 813,812,842, 814,813,842, 816,815,843, 817,816,839, + 818,817,839, 819,818,835, 820,813,814, 820,814,815, 820,815,816, + 820,816,823, 821,672,774, 821,813,820, 822,642,643, 822,643,644, + 822,644,812, 822,812,813, 822,813,821, 823,676,820, 824,681,825, + 826,677,823, 83,742,743, 831,646,647, 831,647,781, 834,646,831, + 844,859,807, 845,861,811, 846,864,808, 846,802,809, 846,863,851, + 847,866,809, 847,801,810, 847,865,850, 848,868,810, 848,800,811, + 848,867,849, 849,934,868, 849,932,923, 85,84,743, 850,936,866, + 850,930,922, 851,938,864, 851,928,921, 852,861,845, 852,926,861, + 852,845,862, 853,859,844, 853,924,859, 853,844,860, 854,811,861, + 854,935,867, 855,937,865, 855,933,917, 856,939,863, 856,931,916, + 857,862,807, 857,807,859, 857,927,862, 858,637,1113, 858,860,806, + 858,925,860, 859,943,857, 86,609,746, 860,942,853, 861,941,854, + 862,940,852, 863,928,851, 863,809,856, 863,939,909, 864,846,851, + 864,938,908, 865,930,850, 865,810,855, 865,937,907, 866,847,850, + 866,931,856, 866,936,906, 867,932,849, 867,811,854, 867,935,905, + 868,848,849, 868,933,855, 868,934,904, 870,880,871, 871,879,775, + 873,879,871, 873,895,879, 873,871,880, 874,880,870, 874,897,880, + 874,870,881, 875,638,775, 875,775,879, 876,881,752, 876,899,881, + 876,752,882, 877,882,751, 877,1051,882, 877,751,883, 878,883,748, + 878,903,883, 879,894,875, 88,218,213, 880,896,873, 881,898,874, + 882,900,876, 883,902,877, 884,902,883, 884,1005,902, 884,883,903, + 885,898,881, 885,1001,898, 885,881,899, 886,896,880, 886,999,896, + 886,880,897, 887,894,879, 887,997,894, 887,879,895, 888,878,1085, + 888,903,878, 888,1006,903, 889,901,877, 889,1004,901, 889,877,902, + 89,217,212, 89,218,88, 890,899,876, 890,1002,899, 890,876,900, + 891,638,875, 891,875,894, 892,897,874, 892,1000,897, 892,874,898, + 893,895,873, 893,998,895, 893,873,896, 894,1016,891, 895,1015,887, + 896,1014,893, 897,1013,886, 898,1012,892, 899,1011,885, 656,655,159, + 90,216,211, 90,217,89, 900,1010,890, 900,882,995, 901,1051,877, + 902,1008,889, 903,1007,884, 91,57,58, 910,940,862, 910,862,927, + 911,941,861, 911,861,926, 912,942,860, 912,860,925, 913,943,859, + 913,859,924, 914,927,857, 914,857,943, 918,854,941, 919,924,853, + 919,853,942, 920,926,852, 920,852,940, 928,863,909, 929,864,908, + 93,92,484, 930,865,907, 931,866,906, 932,867,905, 933,868,904, + 934,849,923, 935,854,918, 936,850,922, 937,855,917, 938,851,921, + 939,856,916, 94,93,484, 96,95,476, 97,96,476, 976,903,1006, + 976,1007,903, 977,902,1005, 977,1008,902, 978,901,1004, 978,1048,901, + 979,900,1003, 979,1010,900, 98,97,476, 980,899,1002, 980,1011,899, + 981,898,1001, 981,1012,898, 982,897,1000, 982,1013,897, 983,1014,896, + 983,896,999, 984,1015,895, 984,895,998, 985,1016,894, 985,894,997, + 986,893,1014, 986,998,893, 987,892,1012, 987,1000,892, 988,891,1016, + 988,858,1110, 989,890,1010, 989,1002,890, 99,506,499, 990,889,1008, + 990,1004,889, 991,888,1084, 991,1006,888, 992,887,1015, 992,997,887, + 993,886,1013, 993,999,886, 994,885,1011, 994,1001,885, 995,882,1045, + 996,884,1007, 996,1005,884, + + QUADS, 648,666,130,131, 648,649,20,19, 657,658,15,14, + 1000,987,1032,1031, 1001,994,1036,1035, 1002,989,1040,1039, + 1003,995,1044,1043, 1004,990,1053,1052, 1005,996,1057,1056, + 1006,991,1061,1060, 1007,976,1059,1058, 1008,977,1055,1054, + 1009,906,973,1047, 101,64,67,100, 1010,979,1042,1041, + 1011,980,1038,1037, 1012,981,1034,1033, 1013,982,1030,1029, + 1014,983,1026,1025, 1015,984,1022,1021, 1016,985,1018,1017, + 102,63,64,101, 103,60,63,102, 104,59,60,103, 1045,882,1051,1009, + 1045,1009,1047,1046, 1048,978,1050,1049, 105,104,103,106, + 106,103,102,107, 1061,991,1071,1075, 1062,939,1004,1052, + 1062,1052,1053,1063, 1063,909,939,1062, 1064,928,1008,1054, + 1064,1054,1055,1065, 1065,921,928,1064, 1066,938,1005,1056, + 1066,1056,1057,1067, 1067,908,938,1066, 1068,929,1007,1058, + 1068,1058,1059,1069, 1069,915,929,1068, 107,102,101,108, + 1071,991,1084,1076, 1072,915,976,1006, 1073,1006,1060,1074, + 1074,1060,1061,1075, 1075,1071,1073,1074, 1078,1077,1076,1079, + 108,101,100,109, 1080,1079,1085,1095, 1081,1080,1082,1083, + 1082,1080,1092,1091, 1084,1085,1087,1088, 1085,1079,1089,1087, + 1086,1082,1091,1093, 1088,1087,1089,1090, 1089,1079,1076,1090, + 109,100,99,110, 1090,1076,1084,1088, 1092,1080,1095,1094, + 1093,1091,1092,1094, 1096,27,26,431, 1097,638,1103,1098, + 658,659,16,15, 110,99,499,496, 1100,1099,1102,1104, + 1101,1100,1104,1105, 1102,1099,1098,1103, 1106,1105,1104,1107, + 1107,1104,1102,1109, 1109,1102,1103,1108, 1110,1109,1108,1111, + 1112,1107,1109,1110, 1113,1106,1107,1112, 1115,819,828,1116, + 1117,784,791,1120, 1118,1116,1117,1119, 1119,1117,1120,1121, + 1120,791,1129,1122, 1121,1120,1122,1126, 1122,1123,1125,1126, + 1126,1125,1154,1137, 1127,1124,1302,1303, 1128,1123,1122,1129, + 1128,803,1070,1303, 1129,796,803,1128, 113,114,123,124, + 1130,1115,1116,1118, 1131,1114,1115,1130, 1132,1133,1140,1141, + 1133,1134,1139,1140, 1134,1135,1138,1139, 1136,688,1131,1130, + 1138,1135,1136,1137, 1138,1137,1154,1146, 1139,1138,1146,1145, + 114,115,122,123, 1140,1139,1145,1144, 1141,1140,1144,1143, + 1142,761,1132,1141, 1143,758,1142,1141, 1143,1144,1149,1150, + 1144,1145,1148,1149, 1145,1146,1147,1148, 1148,1147,1155,1153, + 1149,1148,1153,1152, 115,116,121,122, 115,114,166,167, + 1150,757,758,1143, 1150,1149,1152,1151, 1151,754,755,1150, + 1158,712,754,1151, 1158,1152,1160,1159, 1160,1152,1153,1155, + 1160,1155,1239,1177, 1165,762,773,1162, 1165,1162,1163,1166, + 1166,1163,1164,1167, 1167,1159,1176,1166, 1169,1170,1173,1174, + 1173,1170,1171,1175, 1174,1173,1177,1239, 1175,1165,1166,1176, + 1176,1159,1160,1177, 1177,1173,1175,1176, 1182,1183,1186,1187, + 1187,1186,1191,1190, 1188,1181,1182,1187, 1189,1188,1187,1190, + 119,118,117,120, 1190,1191,1192,1193, 1193,1192,1197,1196, + 1194,1189,1190,1193, 1195,1194,1193,1196, 1196,1197,1198,1199, + 1199,1198,1203,1202, 659,660,17,16, 659,658,177,171, 120,117,113,124, + 1200,1195,1196,1199, 1201,1200,1199,1202, 1202,1203,1204,1205, + 1204,1184,1230,1209, 1205,1204,1209,1208, 1206,1201,1202,1205, + 1207,1206,1205,1208, 1208,1209,1210,1211, 1211,1210,1215,1214, + 1212,1207,1208,1211, 1213,1212,1211,1214, 1214,1215,1216,1217, + 1217,1216,1221,1220, 1218,1213,1214,1217, 1219,1218,1217,1220, + 122,121,128,127, 1220,1221,1222,1223, 1224,1219,1220,1223, + 1227,1180,1181,1188, 1227,1206,1207,1229, 123,122,127,126, + 1231,726,1168,1233, 1233,1168,1243,1235, 1234,1231,1233,1232, + 1236,1235,1243,1242, 1236,1242,1244,1247, 1237,1238,1245,1246, + 1238,744,1235,1236, 1239,1157,1280,1279, 124,123,126,125, + 1244,1242,1271,1270, 1245,1238,1236,1247, 1246,1245,1262,1261, + 1247,1244,1259,1263, 1249,1241,1256,1253, 125,126,135,136, + 1251,1248,1249,1250, 1251,1250,1262,1263, 1254,1251,1263,1259, + 1255,1241,1248,1254, 1257,1256,1255,1258, 1258,1255,1254,1259, + 126,127,134,135, 1260,1253,1256,1257, 1261,1252,1253,1260, + 1262,1250,1252,1261, 1262,1245,1247,1263, 1264,1237,1246,1265, + 1265,1246,1261,1260, 1268,1243,1266,1269, 1269,1266,1267,1277, + 127,128,133,134, 1271,1242,1243,1268, 1272,1260,1257,1274, + 1273,1258,1259,1270, 1273,1124,1127,1283, 1274,1257,1258,1273, + 1278,1174,1239,1279, 128,121,451,454, 1284,1276,1278,1285, + 1285,1278,1279,1287, 1286,1277,1276,1284, 1287,1279,1280,1288, + 1288,1280,1281,1289, 1289,1281,1277,1286, 129,120,124,125, + 1290,1284,1285,1291, 1291,1285,1287,1293, 1292,1286,1284,1290, + 1293,1287,1288,1294, 1294,1288,1289,1295, 1295,1289,1286,1292, + 660,666,18,17, 130,666,660,119, 130,119,120,129, 1301,458,461,1300, + 1301,1300,462,459, 1303,1070,1077,1127, 1303,1302,1125,1128, + 131,130,129,132, 132,129,125,136, 134,133,140,139, 135,134,139,138, + 136,135,138,137, 140,133,463,462, 141,132,136,137, 142,131,132,141, + 154,153,160,163, 158,36,37,157, 159,655,111,158, 161,156,155,162, + 161,41,42,448, 162,155,154,163, 163,40,41,162, 164,113,117,169, + 164,169,170,174, 165,116,115,167, 165,156,449,450, 166,114,113,164, + 167,166,173,172, 169,117,118,168, 170,169,168,171, 172,156,165,167, + 173,166,164,174, 174,170,178,175, 175,178,179,180, 176,155,172,173, + 176,173,174,175, 177,149,179,178, 178,170,171,177, 179,151,152,180, + 180,153,176,175, 649,648,131,142, 649,650,21,20, 201,145,250,264, + 206,90,753,869, 207,206,869,872, 263,256,400,399, 264,250,404,403, + 265,257,306,305, 266,251,310,309, 267,258,298,297, 268,252,302,301, + 269,239,300,299, 270,240,296,295, 271,241,308,307, 272,242,304,303, + 273,243,402,401, 274,244,398,397, 279,260,247,280, 280,247,320,353, + 281,277,254,282, 282,254,327,355, 283,259,248,284, 284,248,319,357, + 285,278,249,286, 286,249,321,359, 287,262,245,288, 288,245,318,361, + 289,275,255,290, 19,18,666,648, 290,255,328,363, 291,261,246,292, + 292,246,317,365, 293,276,253,294, 294,253,322,367, 296,240,316,369, + 298,258,329,371, 650,649,143,195, 300,239,315,373, 302,252,324,375, + 304,242,314,377, 306,257,330,379, 308,241,344,382, 661,653,652,662, + 310,251,313,385, 311,250,407,341, 312,256,273,340, 313,251,274,339, + 314,242,265,338, 315,239,268,337, 316,240,267,336, 317,246,276,335, + 318,245,275,334, 319,248,278,333, 662,667,668,663, 320,247,277,332, + 321,249,262,349, 322,253,270,347, 323,195,260,351, 324,252,272,345, + 325,244,263,343, 327,254,259,350, 328,255,261,348, 329,258,269,346, + 663,668,669,670, 330,257,271,380, 331,243,264,342, 332,277,281,354, + 333,278,285,358, 334,275,289,362, 335,276,293,366, 336,267,297,370, + 337,268,301,374, 338,265,305,378, 339,274,397,387, 664,661,662,663, + 340,273,401,391, 342,264,403,393, 343,263,399,389, 344,241,266,383, + 345,272,303,376, 346,269,299,372, 347,270,295,368, 348,261,291,364, + 349,262,287,360, 665,664,663,670, 350,259,283,356, 351,260,279,352, + 352,279,280,353, 353,320,351,352, 354,281,282,355, 355,327,332,354, + 356,283,284,357, 357,319,350,356, 358,285,286,359, 359,321,333,358, + 36,665,670,671, 360,287,288,361, 361,318,349,360, 362,289,290,363, + 363,328,334,362, 364,291,292,365, 365,317,348,364, 366,293,294,367, + 367,322,335,366, 368,295,296,369, 369,316,347,368, 37,36,671,674, + 370,297,298,371, 371,329,336,370, 372,299,300,373, 373,315,346,372, + 374,301,302,375, 375,324,337,374, 376,303,304,377, 377,314,345,376, + 378,305,306,379, 379,330,338,378, 38,37,674,675, 380,271,307,381, + 381,307,308,382, 382,344,380,381, 383,266,309,384, 384,309,310,385, + 385,313,383,384, 386,217,380,344, 388,325,339,387, 388,387,397,398, + 39,38,675,678, 390,312,343,389, 390,389,399,400, 392,331,340,391, + 392,391,401,402, 394,311,342,393, 394,393,403,404, 395,341,408,409, + 396,326,341,395, 396,395,409,410, 398,244,325,388, 40,39,678,679, + 400,256,312,390, 402,243,331,392, 404,250,311,394, 405,140,462,1300, + 406,326,396,410, 408,406,410,409, 41,40,679,682, 411,412,413,414, + 411,414,424,425, 412,405,1300,461, 417,415,416,418, 419,326,406,411, + 419,411,425,423, 42,41,682,683, 420,414,415,430, 422,420,419,423, + 424,414,420,422, 424,422,423,425, 426,417,421,428, 427,415,417,426, + 427,426,428,429, 43,42,683,686, 430,415,427,429, 431,651,639,1096, + 431,26,25,432, 432,25,24,433, 432,433,436,437, 433,24,23,434, + 434,23,22,435, 436,433,434,438, 436,438,441,443, 437,651,431,432, + 437,436,443,442, 438,434,435,439, 438,439,440,441, 44,43,686,687, + 441,440,447,446, 442,443,444,445, 443,441,446,444, 448,42,43,465, + 449,156,161,448, 449,448,465,464, 45,44,687,690, 450,449,464,452, + 451,116,165,450, 451,450,452,453, 454,451,453,455, 456,454,455,460, + 456,457,462,463, 459,457,456,460, 46,45,690,691, 461,458,607,617, + 463,128,454,456, 465,44,470,464, 466,98,476,475, 466,49,50,98, + 467,47,49,466, 468,46,47,467, 469,45,46,468, 47,46,691,694, + 470,44,45,469, 470,469,472,471, 472,469,468,473, 473,468,467,474, + 474,467,466,475, 476,95,477,475, 478,474,475,477, 479,473,474,478, + 48,695,696,697, 480,472,473,479, 481,480,479,482, 482,479,478,483, + 483,478,477,484, 486,483,484,485, 487,482,483,486, 487,486,494,489, + 488,459,460,471, 488,471,472,480, 489,481,482,487, 49,47,694,48, + 492,58,59,493, 493,59,104,501, 494,486,492,493, 494,493,510,511, + 497,496,499,500, 498,497,500,501, 652,640,667,662, 652,653,10,9, + 50,49,48,697, 500,499,509,510, 501,104,105,498, 502,74,565,567, + 504,72,73,503, 505,71,72,504, 505,504,507,509, 507,504,503,508, + 509,507,511,510, 51,50,697,698, 510,493,501,500, 511,507,508,573, + 513,81,82,566, 514,80,81,513, 515,79,80,514, 515,514,561,522, + 516,78,79,515, 516,515,522,521, 517,77,78,516, 518,76,77,517, + 52,51,698,701, 520,517,516,521, 521,522,523,524, 524,523,528,527, + 525,520,521,524, 526,525,524,527, 527,528,529,530, 53,52,701,702, + 530,529,534,533, 531,526,527,530, 532,531,530,533, 533,534,535,536, + 536,535,540,539, 537,532,533,536, 538,537,536,539, 539,540,541,542, + 54,53,702,705, 541,540,561,563, 542,541,546,545, 543,538,539,542, + 544,543,542,545, 545,546,547,548, 548,547,552,551, 549,544,545,548, + 55,54,705,706, 550,549,548,551, 551,552,553,554, 554,553,558,557, + 555,550,551,554, 556,555,554,557, 56,55,706,709, 564,518,538,543, + 566,82,743,84, 567,565,568,566, 569,84,572,570, 57,56,709,710, + 570,572,579,581, 573,489,494,511, 573,508,612,613, 577,502,567,569, + 577,569,570,576, 577,576,605,602, 578,576,570,581, 579,572,571,580, + 58,57,710,713, 580,571,598,599, 581,579,596,597, 582,575,589,588, + 583,582,585,584, 586,584,596,595, 587,586,595,594, 588,589,592,593, + 589,590,591,592, 59,58,713,714, 590,575,583,587, 590,587,594,591, + 591,594,606,608, 592,591,608,607, 593,578,581,597, 593,592,607,604, + 595,580,599,594, 596,579,580,595, 596,584,585,597, 597,585,588,593, + 653,654,11,10, 60,59,714,61, 600,577,602,603, 601,600,603,611, + 605,576,578,604, 61,716,717,62, 610,611,620,618, 611,615,623,620, + 612,610,618,619, 613,612,619,621, 614,491,573,613, 614,613,621,622, + 615,614,622,623, 618,620,626,624, 619,618,624,625, 62,717,718,65, + 620,623,629,626, 621,619,625,627, 622,621,627,628, 623,622,628,629, + 63,60,61,62, 639,638,1097,1096, 64,63,62,65, 647,634,793,782, + 1,647,646,2, 2,646,645,3, 65,718,719,66, 3,645,644,4, 4,644,643,5, + 5,643,642,6, 6,642,641,7, 7,641,640,8, 8,640,652,9, 28,27,1096,1097, + 29,28,1097,1098, 66,719,720,69, 30,29,1098,1099, 31,30,1099,1100, + 32,31,1100,1101, 33,637,636,34, 34,636,635,35, 35,635,634,0, + 0,634,647,1, 67,64,65,66, 670,669,672,671, 671,672,673,674, + 673,672,821,820, 674,673,676,675, 675,676,677,678, 678,677,680,679, + 679,680,681,682, 68,67,66,69, 681,680,826,825, 682,681,684,683, + 683,684,685,686, 684,681,824,1114, 685,684,1114,1131, + 686,685,688,687, 687,688,689,690, 689,688,1136,1135, 69,720,721,70, + 690,689,692,691, 691,692,693,694, 692,689,1135,1134, + 693,692,1134,1133, 694,693,695,48, 695,693,1133,1132, + 696,695,1132,761, 697,696,699,698, 698,699,700,701, 699,696,761,760, + 654,655,12,11, 70,721,722,723, 700,699,760,759, 701,700,703,702, + 702,703,704,705, 703,700,759,758, 704,703,758,757, 705,704,707,706, + 706,707,708,709, 707,704,757,756, 708,707,756,755, 709,708,711,710, + 71,68,69,70, 710,711,712,713, 711,708,755,754, 713,712,715,714, + 714,715,716,61, 715,712,1158,1159, 716,715,767,766, 717,716,766,765, + 718,717,765,764, 719,718,764,763, 72,71,70,723, 722,721,1171,1170, + 723,722,725,724, 724,725,726,727, 725,722,1170,1169, 727,726,729,728, + 728,729,730,731, 73,72,723,724, 731,730,733,732, 732,733,734,735, + 733,730,1184,1183, 734,733,1183,1182, 735,734,737,736, + 736,737,738,739, 737,734,1182,1181, 738,737,1181,1180, + 739,738,741,740, 74,73,724,727, 740,741,742,83, 741,738,1180,1179, + 742,741,1179,1232, 743,742,1232,744, 749,748,751,750, 75,74,727,728, + 750,751,752,753, 753,752,870,869, 76,75,728,731, 762,763,772,773, + 763,764,771,772, 764,765,770,771, 765,766,769,770, 766,767,768,769, + 767,715,1159,1167, 768,767,1167,1164, 77,76,731,732, 774,642,822,821, + 775,639,872,871, 776,780,783,787, 776,777,829,827, 778,779,828,830, + 78,77,732,735, 780,781,782,783, 780,776,827,832, 781,780,832,831, + 783,782,793,792, 784,779,778,785, 785,778,777,786, 786,777,776,787, + 787,783,792,788, 788,792,795,799, 789,786,787,788, 79,78,735,736, + 790,785,786,789, 791,784,785,790, 792,793,794,795, 793,634,635,794, + 794,635,636,805, 795,794,805,804, 796,791,790,797, 797,790,789,798, + 798,789,788,799, 799,795,804,800, 655,656,13,12, 80,79,736,739, + 801,798,799,800, 802,797,798,801, 803,796,797,802, 806,636,637,858, + 81,80,739,740, 815,814,842,843, 817,818,825,826, 818,819,824,825, + 82,81,740,83, 823,816,817,826, 824,819,1115,1114, 827,829,836,837, + 828,779,1117,1116, 828,819,835,830, 829,777,778,830, 831,832,833,834, + 833,832,827,837, 834,833,841,840, 835,818,839,836, 836,829,830,835, + 837,836,839,838, 839,816,843,838, 840,645,646,834, 841,833,837,838, + 842,812,840,841, 842,841,838,843, 86,747,748,749, 869,870,871,872, + 872,639,112,207, 88,87,86,749, 89,88,749,750, 656,657,14,13, + 90,89,750,753, 91,58,492,485, 910,927,952,953, 911,926,956,957, + 912,925,944,945, 913,924,948,949, 914,943,950,951, 915,808,864,929, + 918,941,958,959, 919,942,946,947, 92,91,485,484, 92,56,57,91, + 920,940,954,955, 924,919,992,1015, 925,858,988,1016, 926,920,993,1013, + 927,914,986,1014, 928,909,990,1008, 929,908,996,1007, 93,55,56,92, + 930,907,979,1003, 931,906,1009,1048, 932,905,981,1001, + 933,904,980,1002, 934,923,994,1011, 935,918,987,1012, + 936,922,995,1045, 937,917,989,1010, 938,921,977,1005, + 939,916,978,1004, 94,54,55,93, 940,910,983,999, 941,911,982,1000, + 942,912,985,997, 943,913,984,998, 944,925,1016,1017, + 945,944,1017,1018, 946,942,997,1019, 947,946,1019,1020, + 948,924,1015,1021, 949,948,1021,1022, 95,94,484,477, 95,53,54,94, + 950,943,998,1023, 951,950,1023,1024, 952,927,1014,1025, + 953,952,1025,1026, 954,940,999,1027, 955,954,1027,1028, + 956,926,1013,1029, 957,956,1029,1030, 958,941,1000,1031, + 959,958,1031,1032, 96,52,53,95, 960,935,1012,1033, 961,960,1033,1034, + 961,905,935,960, 962,932,1001,1035, 963,962,1035,1036, + 963,923,932,962, 964,934,1011,1037, 965,964,1037,1038, + 965,904,934,964, 966,933,1002,1039, 967,966,1039,1040, + 967,917,933,966, 968,937,1010,1041, 969,968,1041,1042, + 969,907,937,968, 97,51,52,96, 970,930,1003,1043, 971,970,1043,1044, + 971,922,930,970, 972,936,1045,1046, 973,972,1046,1047, + 973,906,936,972, 974,931,1048,1049, 975,974,1049,1050, + 975,916,931,974, 976,915,1069,1059, 977,921,1065,1055, + 978,916,975,1050, 979,907,969,1042, 98,50,51,97, 980,904,965,1038, + 981,905,961,1034, 982,911,957,1030, 983,910,953,1026, + 984,913,949,1022, 985,912,945,1018, 986,914,951,1024, + 987,918,959,1032, 989,917,967,1040, 990,909,1063,1053, + 992,919,947,1020, 993,920,955,1028, 994,923,963,1036, + 995,922,971,1044, 996,908,1067,1057, 997,992,1020,1019, + 998,986,1024,1023, 999,993,1028,1027, + + SPIN,18, + 9510,0, 9510,756, SEAM, 9134,1129, 9447,1487, + 9447,1951, 9103,2371, STEPDOWN, 8211,3083, + 7167,4242, 6662,5664, 7040,7142, STEPUP, SEAM, 7935,8560, + STEPUP, BACKREF,0, + ENDOFDATA) + +class Bishop(SimpleModel): + """ + """ + data = (VERTICES, SETBACKREF,0, 5233,26960,0, 5154,26960,909, 4918,26960,1790, + 4532,26960,2617, 4009,26960,3364, 3364,26960,4009, 2617,26960,4532, + 1790,26960,4918, 909,26960,5154, 0,26833,5233, -909,26960,5154, + -1790,26960,4918, -2617,26960,4532, -3364,26960,4009, + -4009,26960,3364, -4532,26960,2617, -4918,26960,1790, + -5154,26960,909, -5233,26960,0, -5154,26960,-909, -4918,26960,-1790, + -4532,26960,-2617, -4009,26960,-3364, -3364,26960,-4009, + -2617,26960,-4532, -1790,26960,-4918, -909,26960,-5154, 0,26833,-5233, + 909,26960,-5154, 1790,26960,-4918, 2617,26960,-4532, 3364,26960,-4009, + 4009,26960,-3364, 4532,26960,-2617, 4918,26960,-1790, 5154,26960,-909, + SETBACKREF,1, 3812,31178,0, 3765,31144,729, 3624,31040,1435, + 3395,30872,2153, 3084,30642,2820, 2701,30360,3389, 2076,29899,4102, + 1492,30015,4340, 845,30033,4442, 0,30044,4511, -657,30063,4443, + -1481,30081,4214, -2190,30081,3884, -2830,30081,3435, + -3383,30081,2883, -3831,30081,2242, -4162,30081,1534, + -4364,30081,779, -4432,30081,0, -4364,30081,-779, -4162,30081,-1534, + -3831,30081,-2242, -3383,30081,-2883, -2830,30081,-3435, + -2190,30081,-3884, -1481,30081,-4214, -657,30063,-4443, 0,30044,-4511, + 845,30033,-4442, 1492,30015,-4340, 2076,29899,-4102, 2701,30360,-3388, + 3084,30642,-2820, 3395,30872,-2153, 3624,31040,-1435, 3765,31144,-729, + 240,28546,-4957, 884,29021,-4784, 1490,29467,-4537, 2076,29899,-4102, + 2701,30360,-3388, 3084,30642,-2820, 3395,30872,-2153, + 3624,31040,-1435, 3765,31144,-729, 2177,28477,4637, 1021,27605,5037, + 1021,27605,-5042, 2170,28478,-4644, 0,26833,-5233, 0,26833,5233, + -3153,28619,-3758, 240,28546,4957, 884,29021,4784, 2076,29899,4102, + 2701,30360,3389, 3084,30642,2820, 3395,30872,2153, 3624,31040,1435, + 3765,31144,729, -719,28582,4883, 4863,28569,977, 4560,28569,1925, + 4064,28569,2815, 3465,28569,3723, 2622,28569,4448, 2621,28583,-4401, + 3473,28588,-3691, 4064,28569,-2815, 4560,28569,-1925, 4863,28569,-977, + 4965,28569,0, -1678,28619,4610, -3153,28619,3758, -2453,28619,4248, + -4248,28619,2453, -3758,28619,3153, -4831,28619,852, -4906,28619,0, + -4831,28619,-852, -4610,28619,-1678, -4248,28619,-2453, + -3758,28619,-3153, -1678,28619,-4610, -2453,28619,-4248, + -644,27895,5037, -644,27895,-5037, -4610,28619,1678, -719,28582,-4883, + 2170,28478,-4644, 1021,27605,-5042, 1021,27605,5037, 2177,28477,4637, + 0,26833,-5233, 0,26833,5233, -644,27895,-5037, -644,27895,5037, + -644,27895,5037, -644,27895,-5037, 1490,29467,-4537, 884,29021,-4784, + 240,28546,-4957, 240,28546,4957, 884,29021,4784, 3812,31178,0, + 4349,30116,-867, 4197,30001,-1705, 3948,29813,-2492, 3610,29558,-3203, + 3193,29244,-3817, 2711,28880,-4315, 4349,30116,-867, 4197,30001,-1705, + 3948,29813,-2492, 3610,29558,-3203, 3193,29244,-3817, + 2711,28880,-4315, 1608,28047,-4906, 1608,28047,4901, 2711,28880,4310, + 3193,29244,3812, 3610,29558,3198, 3948,29813,2487, 4197,30001,1701, + 4349,30116,862, 4401,30155,-176, 1490,29467,4537, 1490,29467,4537, + 4401,30155,-176, 4349,30116,862, 4197,30001,1701, 3948,29813,2487, + 3610,29558,3198, 3193,29244,3812, 2711,28880,4310, 1608,28047,4901, + 1608,28047,-4906, + + TRIANGLES, 8,127,9, 7,128,154, 2,98,3, 121,96,10, 121,10,9, 121,138,96, + 122,27,26, 27,126,28, 66,135,65, 162,42,43, 165,164,141, 80,140,95, + 103,151,150, 102,125,152, 101,155,128, 100,157,156, 124,137,122, + 33,105,34, 29,153,125, 26,124,122, + + QUADS, 22,118,87,23, 21,117,118,22, 20,116,117,21, 19,115,116,20, + 18,114,115,19, 17,113,114,18, 16,123,113,17, 15,111,123,16, 14,112,111,15, + 13,109,112,14, 12,110,109,13, 11,108,110,12, 7,154,127,8, 110,108,47,48, + 135,136,64,65, 136,137,63,64, 138,139,44,45, 139,162,43,44, 109,110,48,49, + 147,161,107,106, 166,165,141,142, 167,166,142,143, 168,167,143,144, + 169,168,144,145, 112,109,49,50, 170,169,145,146, 171,81,84,172, + 111,112,50,51, 72,88,133,134, 73,89,88,72, 74,163,89,73, 75,90,163,74, + 76,91,90,75, 77,92,91,76, 78,93,92,77, 79,94,93,78, 80,95,94,79, + 81,170,146,84, 82,171,172,83, 86,82,83,85, 130,129,131,132, 123,111,51,52, + 113,123,52,53, 114,113,53,54, 115,114,54,55, 116,115,55,56, 117,116,56,57, + 118,117,57,58, 87,118,58,59, 120,87,59,60, 119,120,60,61, 107,161,160,97, + 106,107,0,35, 105,148,147,106, 105,106,35,34, 104,149,148,105, + 104,105,33,32, 103,150,149,104, 103,104,32,31, 102,152,151,103, + 102,103,31,30, 101,128,7,6, 100,101,6,5, 100,156,155,101, 99,100,5,4, + 99,158,157,100, 98,99,4,3, 98,159,158,99, 97,98,2,1, 97,160,159,98, + 96,108,11,10, 96,138,45,46, 124,119,61,62, 47,108,96,46, 63,137,124,62, + 0,107,97,1, 29,125,102,30, 28,126,153,29, 25,119,124,26, 24,120,119,25, + 23,87,120,24, + + SPIN,18, + 8870,0,8870,731,SEAM,8519,1091,8811,1438,8811,1886,8626,2292, + STEPDOWN,6989,2980,5927,4133,5548,5735, + STEPUP,5388,7642,5228,7807,STEPDOWN,4427,8149,4057,8434, + 3493,9185,2816,13524,SEAM,2690,18532,5301,18690, + STEPUP,6810,19005,6861,19277,6804,19625,STEPDOWN,6502,19845, + SEAM,4305,20394,STEPUP,4796,20522,4924,20759,4778,20979, + STEPDOWN,SEAM,3727,21207,SEAM,3726,22181,STEPUP,SEAM,4546,22705, + SEAM,3846,23385,4718,24227,5226,25516,STEPUP,BACKREF,0, + + SPIN,36, + BACKREF,1,STEPDOWN,3548,31590,STEPDOWN,2724,32633,SEAM,1581,33500, + 2013,33901,STEPUP,2281,34500,2281,34936,STEPDOWN,1947,35372, + STEPDOWN,1233,35734,STEPDOWN,0,35891, + + ENDOFDATA) + +class Queen(SimpleModel): + """ + """ + data = (SPIN,24, + 11092,0,11092,914,SEAM,10653,1284, + 11018,1798,11018,2358,10787,2866, + STEPDOWN,8739,3726,7412,5168,6937,7171, + STEPUP,6737,9556,6537,9762,STEPDOWN,5536,10191,5073,10546, + 4368,11485,3678,15137,SEAM,3259,26879, + 5966,27091,STEPUP,7332,27515,7619,27882,7545,28455,7317,28751, + 5654,29177,5538,29326,5542,29982,5377,30278, + STEPDOWN,SEAM,4194,30585, + SEAM,4226,31822,5002,32218,STEPUP,5139,32477,5058,32774, + SEAM,4227,33040,STEPDOWN,4421,34778,5042,36612,5874,38429, + STEPUP,SEAM,PATTERN,3,6018,39660,6018,39660,6804,39977, + SEAM,PATTERN,3,5015,41139,5015,41139,5673,41460, + SEAM,4349,40044, + STEPDOWN,SEAM,1381,41188, + 1396,42332,STEPDOWN,1082,43072,481,43476,0,43543, + ENDOFDATA) + +class King(SimpleModel): + """ + """ + + data = (SPIN,20, + 11378,0,11378,856,SEAM,10928,1152, + 11302,1684,11302,2209,11065,2684, + STEPDOWN,8964,3490,7603,4841,7116,6717, + STEPUP,6911,8950,6705,9144,STEPDOWN,5678,9545,5204,9878, + 4481,10758,3696,14808,SEAM,3065,26979, + 5813,27155,STEPUP,7145,27507,7424,27812,7352,28288,7131,28533, + 5477,28882,5397,29010,5406,29363,4903,29934, + STEPDOWN,SEAM,3944,30227, + SEAM,3974,31478,4703,31849,STEPUP,4832,32092,4756,32370, + SEAM,3975,32620,6899,39055,6877,39351,2833,39514, + 2786,39612,2786,39807,2734,39856,STEPDOWN,STEPDOWN,2590,39905,0,39969, + + SETBACKREF,0, + QUADSTRIP,-1613,39866,0,-1543,39866,702,-1651,40481,0,-1580,40590,702, + -1531,40917,0,-1465,41008,702, + QUADSTRIP,-1531,40917,0,-1465,41008,702,-2956,41104,0,-2829,41187,702, + -3075,41520,0,-2943,41585,702,-3075,43849,0,-2943,43805,702, + -2862,44347,0,-2739,44282,702,-1116,44636,0,-1068,44554,702, + QUADSTRIP,-1116,44636,0,-1068,44554,702,-1102,45692,0,-1054,45576,702, + -973,45829,0,-973,45747,702,973,45829,0,973,45747,702,1102,45692,0, + 1054,45576,702,1116,44636,0,1068,44554,702, + QUADSTRIP,1116,44636,0,1068,44554,702,2862,44347,0,2739,44282,702, + 3075,43849,0,2943,43805,702,3075,41520,0,2943,41585,702,2956,41104,0, + 2829,41187,702,1531,40917,0,1465,41008,702, + QUADSTRIP,1531,40917,0,1465,41008,702,1651,40481,0,1580,40590,702, + 1613,39866,0,1543,39866,702, + QUADSTRIP,-1543,39866,702,1543,39866,702,-1580,40590,702, + 1580,40590,702,-1465,41008,702,1465,41008,702,-2829,41187,702, + 2829,41187,702,-2943,41585,702,2943,41585,702,-2943,43805,702, + 2943,43805,702,-2739,44282,702,2739,44282,702,-1068,44554,702, + 1068,44554,702,-1054,45576,702,1054,45576,702,-973,45747,702, + 973,45747,702, + QUADSTRIP,-1543,39866,-702,BACKREF,0,0,-1580,40590,-702,BACKREF,0,2, + -1465,41008,-702,BACKREF,0,4, + QUADSTRIP,-1465,41008,-702,BACKREF,0,6,-2829,41187,-702,BACKREF,0,8, + -2943,41585,-702,BACKREF,0,10,-2943,43805,-702,BACKREF,0,12, + -2739,44282,-702,BACKREF,0,14,-1068,44554,-702,BACKREF,0,16, + QUADSTRIP,-1068,44554,-702,BACKREF,0,18,-1054,45576,-702, + BACKREF,0,20,-973,45747,-702,BACKREF,0,22,973,45747,-702,BACKREF,0,24, + 1054,45576,-702,BACKREF,0,26,1068,44554,-702,BACKREF,0,28, + QUADSTRIP,1068,44554,-702,BACKREF,0,30,2739,44282,-702, + BACKREF,0,32,2943,43805,-702,BACKREF,0,34,2943,41585,-702,BACKREF,0,36, + 2829,41187,-702,BACKREF,0,38,1465,41008,-702,BACKREF,0,40, + QUADSTRIP,1465,41008,-702,BACKREF,0,42,1580,40590,-702, + BACKREF,0,44,1543,39866,-702,BACKREF,0,46, + QUADSTRIP,1543,39866,-702,-1543,39866,-702,1580,40590,-702, + -1580,40590,-702,1465,41008,-702,-1465,41008,-702,2829,41187,-702, + -2829,41187,-702,2943,41585,-702,-2943,41585,-702,2943,43805,-702, + -2943,43805,-702,2739,44282,-702,-2739,44282,-702,1068,44554,-702, + -1068,44554,-702,1054,45576,-702,-1054,45576,-702,973,45747,-702, + -973,45747,-702, + ENDOFDATA) + diff --git a/src/lib/scene/opengl/opengl.py b/src/lib/scene/opengl/opengl.py new file mode 100644 index 0000000..0645e56 --- /dev/null +++ b/src/lib/scene/opengl/opengl.py @@ -0,0 +1,680 @@ +import math + +from OpenGL.GL import * +from OpenGL.GLU import * + +from glchess.defaults import * + +import glchess.scene +import texture +import new_models +builtin_models = new_models + +SQUARE_WIDTH = 10.0 +BOARD_DEPTH = 3.0 +BOARD_BORDER = 5.0 +BOARD_CHAMFER = 2.0 +BOARD_INNER_WIDTH = (SQUARE_WIDTH * 8.0) +BOARD_OUTER_WIDTH = (BOARD_INNER_WIDTH + BOARD_BORDER * 2.0) +OFFSET = (BOARD_OUTER_WIDTH * 0.5) + +LIGHT_AMBIENT_COLOUR = (0.4, 0.4, 0.4, 1.0) +LIGHT_DIFFUSE_COLOUR = (0.7, 0.7, 0.7, 1.0) +LIGHT_SPECULAR_COLOUR = (1.0, 1.0, 1.0, 1.0) + +BOARD_AMBIENT = (0.2, 0.2, 0.2, 1.0) +BOARD_DIFFUSE = (0.8, 0.8, 0.8, 1.0) +BOARD_SPECULAR = (1.0, 1.0, 1.0, 1.0) +BOARD_SHININESS = 128.0 + +BACKGROUND_COLOUR = (0.53, 0.63, 0.75, 0.0) +BORDER_COLOUR = (0.72, 0.33, 0.0) +BLACK_SQUARE_COLOURS = {None: (0.8, 0.8, 0.8), glchess.scene.HIGHLIGHT_SELECTED: (0.3, 1.0, 0.3), glchess.scene.HIGHLIGHT_CAN_MOVE: (0.3, 0.3, 1.0)} +WHITE_SQUARE_COLOURS = {None: (1.0, 1.0, 1.0), glchess.scene.HIGHLIGHT_SELECTED: (0.2, 1.0, 0.0), glchess.scene.HIGHLIGHT_CAN_MOVE: (0.2, 0.2, 0.8)} + +# HACK +import os.path + +class ChessPiece(glchess.scene.ChessPiece): + """ + """ + __scene = None + + __chessSet = None + __name = None + + pos = None + __targetPos = None + + def __init__(self, scene, chessSet, name, startPos = (0.0, 0.0, 0.0)): + """ + """ + self.__scene = scene + self.__chessSet = chessSet + self.__name = name + self.pos = startPos + + def move(self, coord): + """Extends glchess.scene.ChessPiece""" + self.__targetPos = self.__scene._coordToLocation(coord) + self.__scene._startAnimation() + + def draw(self, state = 'default'): + """ + """ + self.__chessSet.drawPiece(self.__name, state, self.__scene) + + def animate(self, timeStep): + """ + + Return True if the piece has moved otherwise False. + """ + if self.__targetPos is None: + return False + + if self.pos == self.__targetPos: + self.__targetPos = None + return False + + # Get distance to target + dx = self.__targetPos[0] - self.pos[0] + dy = self.__targetPos[1] - self.pos[1] + dz = self.__targetPos[2] - self.pos[2] + + # Get movement step in each direction + SPEED = 50.0 # FIXME + xStep = timeStep * SPEED + if xStep > abs(dx): + xStep = dx + else: + xStep *= cmp(dx, 0.0) + yStep = timeStep * SPEED + if yStep > abs(dy): + yStep = dy + else: + yStep *= cmp(dy, 0.0) + zStep = timeStep * SPEED + if zStep > abs(dz): + zStep = dz + else: + zStep *= cmp(dz, 0.0) + + # Move the piece + self.pos = (self.pos[0] + xStep, self.pos[1] + yStep, self.pos[2] + zStep) + return True + +class Scene(glchess.scene.Scene): + """ + """ + # The viewport dimensions + __viewportWidth = 0 + __viewportHeight = 0 + __viewportAspect = 1.0 + + __animating = False + + # Loading screen properties + __throbberEnabled = False + __throbberAngle = 0.0 + + # The scene light position + __lightPos = None + + # The board angle in degrees + __boardAngle = 0.0 + __oldBoardAngle = 0.0 + __targetBoardAngle = 0.0 + + # OpenGL display list for the board and a flag to regenerate it + __boardList = None + __regenerateBoard = False + + # Texture objects for the board + __whiteTexture = None + __blackTexture = None + + # ... + __pieces = None + __chessSets = None + __piecesMoving = False + + # Dictionary of co-ordinates to highlight + __highlights = None + + def __init__(self): + """Constructor for an OpenGL scene""" + self.__lightPos = [100.0, 100.0, 100.0, 1.0] + self.__pieces = [] + self.__highlights = {} + + self.__chessSets = {'white': builtin_models.WhiteBuiltinSet(), 'black': builtin_models.BlackBuiltinSet()} + + self.__whiteTexture = texture.Texture(os.path.join(IMAGE_DIR, 'board.png'), + ambient = BOARD_AMBIENT, diffuse = BOARD_DIFFUSE, + specular = BOARD_SPECULAR, shininess = BOARD_SHININESS) + self.__blackTexture = texture.Texture(os.path.join(IMAGE_DIR, 'board.png'), + ambient = BOARD_AMBIENT, diffuse = BOARD_DIFFUSE, + specular = BOARD_SPECULAR, shininess = BOARD_SHININESS) + + def onRedraw(self): + """This method is called when the scene needs redrawing""" + pass + + def _startAnimation(self): + """ + """ + self.__changed = True + if self.__animating is False: + self.__animating = True + self.startAnimation() + + def addChessPiece(self, chessSet, name, coord): + """Add a chess piece model into the scene. + + 'chessSet' is the name of the chess set (string). + 'name' is the name of the piece (string). + 'coord' is the the chess board location of the piece in LAN format (string). + + Returns a reference to this chess piece or raises an exception. + """ + chessSet = self.__chessSets[chessSet] + piece = ChessPiece(self, chessSet, name, self._coordToLocation(coord)) + self.__pieces.append(piece) + + # Redraw the scene + self.onRedraw() + + return piece + + def removeChessPiece(self, piece): + """Remove chess piece. + + 'piece' is a chess piece instance as returned by addChessPiece(). + """ + self.__pieces.remove(piece) + self.onRedraw() + + def setBoardHighlight(self, coords): + """Highlight a square on the board. + + 'coords' is a dictionary of highlight types keyed by square co-ordinates. + The co-ordinates are a tuple in the form (file,rank). + If None the highlight will be cleared. + """ + if coords is None: + self.__highlights = {} + else: + self.__highlights = coords.copy() + + # Regenerate the optimised board model + self.__regenerateBoard = True + + self.onRedraw() + + def reshape(self, width, height): + """Resize the viewport into the scene. + + 'width' is the width of the viewport in pixels. + 'height' is the width of the viewport in pixels. + """ + self.__viewportWidth = int(width) + self.__viewportHeight = int(height) + self.__viewportAspect = float(self.__viewportWidth) / float(self.__viewportHeight) + self.onRedraw() + + def setBoardRotation(self, angle): + """Set the rotation on the board. + + 'angle' is the angle the board should be drawn at in degress (float, [0.0, 360.0]). + """ + self.__targetBoardAngle = angle + self._startAnimation() + + def animate(self, timeStep): + """Extends glchess.scene.Scene""" + redraw1 = self.__animateThrobber(timeStep) + self.__piecesMoving = self.__animatePieces(timeStep) + redraw2 = self.__animateRotation(timeStep) + if redraw1 or redraw2 or self.__piecesMoving: + self.__animating = True + self.onRedraw() + else: + self.__animating = False + return self.__animating + + def render(self): + """Render the scene. + + This requires an OpenGL context. + """ + glClearColor(*BACKGROUND_COLOUR) + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + + # Set the projection for this scene + self.__setViewport() + + # Do camera and board rotation/translation + glEnable(GL_DEPTH_TEST) + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + self.__transformCamera() + + glLightfv(GL_LIGHT0, GL_AMBIENT, LIGHT_AMBIENT_COLOUR) + glLightfv(GL_LIGHT0, GL_DIFFUSE, LIGHT_DIFFUSE_COLOUR) + glLightfv(GL_LIGHT0, GL_SPECULAR, LIGHT_SPECULAR_COLOUR) + glLightfv(GL_LIGHT0, GL_POSITION, self.__lightPos) + glEnable(GL_LIGHTING) + glEnable(GL_LIGHT0) + + self.__transformBoard() + + glEnable(GL_DEPTH_TEST) + glEnable(GL_CULL_FACE) + + glEnable(GL_TEXTURE_2D) + glEnable(GL_COLOR_MATERIAL) + self.__drawBoard() + glDisable(GL_COLOR_MATERIAL) + glDisable(GL_TEXTURE_2D) + + # WORKAROUND: Mesa is corrupting polygons on the bottom of the models + # It could be because the depth buffer has a low bit depth? + glClear(GL_DEPTH_BUFFER_BIT) + + if self.__throbberEnabled: + self.__drawThrobber() + else: + self.__drawPieces() + + def getSquare(self, x, y): + """Find the chess square at a given 2D location. + + 'x' is the number of pixels from the left of the scene to select. + 'y' is the number of pixels from the bottom of the scene to select. + + This requires an OpenGL context. + + Return the co-ordinate in LAN format (string) or None if no square at this point. + """ + # FIXME: Don't rely on this? It seems to get corrupt when multiple games are started + viewport = glGetIntegerv(GL_VIEWPORT) + + # Don't render to screen, just select + # Selection buffer is large in case we select multiple squares at once (it generates an exception) + glSelectBuffer(20) + glRenderMode(GL_SELECT) + + glInitNames() + glPushName(0) + + # Create pixel picking region near cursor location + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPickMatrix(x, (float(viewport[3]) - y), 1.0, 1.0, viewport) + gluPerspective(60.0, float(viewport[2]) / float(viewport[3]), 0, 1) + + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + self.__transformCamera() + + # Draw board + + self.__transformBoard() + self.__drawSquares() + + # Render and check for hits + # Catch the exception in case we select more than we can fit in the selection buffer + glFlush() + try: + records = glRenderMode(GL_RENDER) + except GLerror: + records = None + + # Get the first record and use this as the selected square + coord = None + if records is not None: + for record in records: + coord = record[2] + break + + # Reset projection matrix + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPerspective(60.0, float(viewport[2]) / float(viewport[3]), 0.1, 1000) + + # Convert from co-ordinates to LAN format + rank = chr(ord('a') + coord[0]) + file = chr(ord('1') + coord[1]) + + return rank + file + + # Private methods + + def _coordToLocation(self, coord): + """ + """ + rank = ord(coord[0]) - ord('a') + file = ord(coord[1]) - ord('1') + x = BOARD_BORDER + float(rank) * SQUARE_WIDTH + 0.5 * SQUARE_WIDTH + z = -(BOARD_BORDER + float(file) * SQUARE_WIDTH + 0.5 * SQUARE_WIDTH) + + return (x, 0.0, z) + + def __animateThrobber(self, timeStep): + """ + """ + if self.__throbberEnabled is False: + return False + + self.__throbberAngle += timeStep * (math.pi * 2.0) / 2.0 + while self.__throbberAngle > (math.pi * 2.0): + self.__throbberAngle -= 2.0 * math.pi + return True + + def __animateRotation(self, timeStep): + """ + """ + if self.__boardAngle == self.__targetBoardAngle: + return False + + # Wait unti pieces have stopped moving + if self.__piecesMoving: + return False + + # Rotate board to the chosen angle + length = abs(self.__targetBoardAngle - self.__oldBoardAngle) + self.__boardAngle += timeStep * length / 0.8 + while self.__boardAngle > 360.0: + self.__boardAngle -= 360.0 + travelled = self.__targetBoardAngle - self.__boardAngle + while travelled < 0.0: + travelled += 360.0 + + # If have moved through the remaining angle then clip to the target + if travelled >= length: + self.__oldBoardAngle = self.__boardAngle = self.__targetBoardAngle + + return True + + def __animatePieces(self, timeStep): + """ + """ + active = False + for piece in self.__pieces: + if piece.animate(timeStep): + active = True + + # If the throbber is enabled the pieces are hidden so don't redraw + if active and not self.__throbberEnabled: + return True + return False + + def __drawThrobber(self): + """ + """ + glDisable(GL_LIGHTING) + glDisable(GL_DEPTH_TEST) + + # Orthographic projection with even scaling + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + + if self.__viewportWidth > self.__viewportHeight: + h = 1.0 + w = 1.0 * self.__viewportWidth / self.__viewportHeight + else: + h = 1.0 * self.__viewportHeight / self.__viewportWidth + w = 1.0 + gluOrtho2D(0, w, 0, h) + + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glColor4f(0.0, 0.0, 0.0, 0.75) + glBegin(GL_QUADS) + glVertex2f(-1.0, -1.0) + glVertex2f(w + 1.0, -1.0) + glVertex2f(w + 1.0, h + 1.0) + glVertex2f(-1.0, h + 1.0) + glEnd() + + NSECTIONS = 9 + RADIUS_OUT0 = 0.4 + RADIUS_OUT1 = 0.43 + RADIUS_OUT2 = 0.4 + RADIUS_IN0 = 0.25#0.1 + RADIUS_IN1 = 0.24#0.09 + RADIUS_IN2 = 0.25#0.1 + STEP_ANGLE = 2.0 * math.pi / float(NSECTIONS) + HALF_WIDTH = 0.8 * (0.5 * STEP_ANGLE) + + glTranslatef(0.5 * w, 0.5 * h, 0.0) + glBegin(GL_QUADS) + + for i in xrange(NSECTIONS): + theta = 2.0 * math.pi * float(i) / float(NSECTIONS) + leadTheta = theta + HALF_WIDTH + lagTheta = theta - HALF_WIDTH + x0 = math.sin(leadTheta) + y0 = math.cos(leadTheta) + x1 = math.sin(theta) + y1 = math.cos(theta) + x2 = math.sin(lagTheta) + y2 = math.cos(lagTheta) + + angleDifference = self.__throbberAngle - theta + if angleDifference > math.pi: + angleDifference -= 2.0 * math.pi + elif angleDifference < -math.pi: + angleDifference += 2.0 * math.pi + + stepDifference = angleDifference / STEP_ANGLE + if stepDifference > -0.5 and stepDifference < 0.5: + x = 2.0 * abs(stepDifference) + glColor4f(1.0, x, x, 0.6) + else: + glColor4f(1.0, 1.0, 1.0, 0.6) + + glVertex2f(RADIUS_IN0 * x0, RADIUS_IN0 * y0) + glVertex2f(RADIUS_OUT0 * x0, RADIUS_OUT0 * y0) + glVertex2f(RADIUS_OUT1 * x1, RADIUS_OUT1 * y1) + glVertex2f(RADIUS_IN1 * x1, RADIUS_IN1 * y1) + + glVertex2f(RADIUS_IN1 * x1, RADIUS_IN1 * y1) + glVertex2f(RADIUS_OUT1 * x1, RADIUS_OUT1 * y1) + glVertex2f(RADIUS_OUT2 * x2, RADIUS_OUT2 * y2) + glVertex2f(RADIUS_IN2 * x2, RADIUS_IN2 * y2) + + glEnd() + + glDisable(GL_BLEND) + + def __setViewport(self): + """Perform the projection matrix transformation for the current viewport""" + glViewport(0, 0, self.__viewportWidth, self.__viewportHeight) + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluPerspective(60.0, self.__viewportAspect, 0.1, 1000) + + def __transformCamera(self): + """Perform the camera matrix transformation""" + gluLookAt(0.0, 90.0, 45.0, + 0.0, 0.0, 5.0, + 0.0, 1.0, 0.0) + + def __drawBoard(self): + """Draw a chessboard""" + # Use pre-rendered version if available + if self.__regenerateBoard is False and self.__boardList is not None: + glCallList(self.__boardList) + return + + # Attempt to store the board as a display list + if self.__boardList is None: + list = glGenLists(1) + if list != 0: + self.__boardList = list + + # If have a list store there + if self.__boardList is not None: + glNewList(self.__boardList, GL_COMPILE_AND_EXECUTE) + + # Board verticies + # (lower 12-15 are under 8-11) + # + # a b c d e f + # + # 8-----------------9 g + # |\ /| + # | 4-------------5 | h + # | | | | + # | | 0---------1 | | i + # | | | | | | + # | | | | | | + # | | 3---------2 | | j + # | | | | + # | 7-------------6 | k + # |/ \| + # 11---------------10 l + # + # |- board -| + # width + + # Draw the border + glColor3f(*BORDER_COLOUR) + + # Top + a = 0.0 + b = BOARD_CHAMFER + c = BOARD_BORDER + d = c + (SQUARE_WIDTH * 8.0) + e = d + BOARD_BORDER - BOARD_CHAMFER + f = d + BOARD_BORDER + l = 0.0 + k = -BOARD_CHAMFER + j = -BOARD_BORDER + i = j - (SQUARE_WIDTH * 8.0) + h = i - BOARD_BORDER + BOARD_CHAMFER + g = i - BOARD_BORDER + verticies = [(c, 0.0, i), (d, 0.0, i), + (d, 0.0, j), (c, 0.0, j), + (b, 0.0, h), (e, 0.0, h), + (e, 0.0, k), (b, 0.0, k), + (a, -BOARD_CHAMFER, g), (f, -BOARD_CHAMFER, g), + (f, -BOARD_CHAMFER, l), (a, -BOARD_CHAMFER, l), + (a, -BOARD_DEPTH, g), (f, -BOARD_DEPTH, g), (f, -BOARD_DEPTH, l), (a, -BOARD_DEPTH, l)] + + normals = [(0.0, 1.0, 0.0), (0.0, 0.0, -1.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (-1.0, 0.0, 0.0), + (0.0, 0.707, -0.707), (0.707, 0.707, 0.0), (0.0, 0.707, 0.707), (-0.707, 0.707, 0.0)] + + #textureCoords = [(0.0, 0.0), ( + + quads = [(0, 1, 5, 4, 0), (1, 2, 6, 5, 0), (2, 3, 7, 6, 0), (3, 0, 4, 7, 0), + (4, 5, 9, 8, 5), (5, 6, 10, 9, 6), (6, 7, 11, 10, 7), (7, 4, 8, 11, 8), + (8, 9, 13, 12, 1), (9, 10, 14, 13, 2), (10, 11, 15, 14, 3), (11, 8, 12, 15, 4)] + + glDisable(GL_TEXTURE_2D) + glBegin(GL_QUADS) + for q in quads: + glNormal3fv(normals[q[4]]) + #glTexCoord2fv(textureCoords[q[0]]) + glVertex3fv(verticies[q[0]]) + #glTexCoord2fv(textureCoords[q[1]]) + glVertex3fv(verticies[q[1]]) + #glTexCoord2fv(textureCoords[q[2]]) + glVertex3fv(verticies[q[2]]) + #glTexCoord2fv(textureCoords[q[3]]) + glVertex3fv(verticies[q[3]]) + glEnd() + + # Draw the squares + glEnable(GL_TEXTURE_2D) + for x in [0, 1, 2, 3, 4, 5, 6, 7]: + for y in [0, 1, 2, 3, 4, 5, 6, 7]: + isBlack = (x + (y % 2) + 1) % 2 + + # Get the highlight type + coord = chr(ord('a') + x) + chr(ord('1') + y) + try: + highlight = self.__highlights[coord] + except KeyError: + highlight = None + + if isBlack: + colour = BLACK_SQUARE_COLOURS[highlight] + self.__whiteTexture.bind() #blackTexture + else: + colour = WHITE_SQUARE_COLOURS[highlight] + self.__whiteTexture.bind() + + x0 = BOARD_BORDER + (x * SQUARE_WIDTH) + x1 = x0 + SQUARE_WIDTH + z0 = BOARD_BORDER + (y * SQUARE_WIDTH) + z1 = z0 + SQUARE_WIDTH + + glBegin(GL_QUADS) + glNormal3f(0.0, 1.0, 0.0) + glColor3fv(colour) + glTexCoord2f(0.0, 0.0) + glVertex3f(x0, 0.0, -z0) + glTexCoord2f(1.0, 0.0) + glVertex3f(x1, 0.0, -z0) + glTexCoord2f(1.0, 1.0) + glVertex3f(x1, 0.0, -z1) + glTexCoord2f(0.0, 1.0) + glVertex3f(x0, 0.0, -z1) + glEnd() + + if self.__boardList is not None: + glEndList() + glCallList(self.__boardList) + + def __drawSquares(self): + """Draw the board squares for picking""" + + # draw the floor squares + for u in [0, 1, 2, 3, 4, 5, 6, 7]: + glLoadName(u) + + for v in [0, 1, 2, 3, 4, 5, 6, 7]: + glPushName(v) + + # Draw square + glBegin(GL_QUADS) + x0 = BOARD_BORDER + (u * SQUARE_WIDTH) + x1 = x0 + SQUARE_WIDTH + z0 = BOARD_BORDER + (v * SQUARE_WIDTH) + z1 = z0 + SQUARE_WIDTH + + glVertex3f(x0, 0.0, -z0) + glVertex3f(x1, 0.0, -z0) + glVertex3f(x1, 0.0, -z1) + glVertex3f(x0, 0.0, -z1) + glEnd() + + glPopName() + + def __drawPieces(self): + """Draw the pieces in the scene""" + glEnable(GL_TEXTURE_2D) + + for piece in self.__pieces: + glPushMatrix() + glTranslatef(piece.pos[0], piece.pos[1], piece.pos[2]) + + # Draw the model + piece.draw() + + glPopMatrix() + + glDisable(GL_TEXTURE_2D) + + def __transformBoard(self): + """Perform the board transform""" + # Rotate the board + glRotatef(self.__boardAngle, 0.0, 1.0, 0.0) + + # Offset board so centre is (0.0,0.0) + glTranslatef(-OFFSET, 0.0, OFFSET) diff --git a/src/lib/scene/opengl/png.py b/src/lib/scene/opengl/png.py new file mode 100644 index 0000000..4edb106 --- /dev/null +++ b/src/lib/scene/opengl/png.py @@ -0,0 +1,1022 @@ +#!/usr/bin/env python +# png.py - PNG encoder in pure Python +# Copyright (C) 2006 Johann C. Rocholl +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Contributors (alphabetical): +# Nicko van Someren +# +# Changelog (recent first): +# 2006-06-17 Nicko: Reworked into a class, faster interlacing. +# 2006-06-17 Johann: Very simple prototype PNG decoder. +# 2006-06-17 Nicko: Test suite with various image generators. +# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support. +# 2006-06-15 Johann: Scanline iterator interface for large input files. +# 2006-06-09 Johann: Very simple prototype PNG encoder. + + +""" +Pure Python PNG Reader/Writer + +This is an implementation of a subset of the PNG specification at +http://www.w3.org/TR/2003/REC-PNG-20031110 in pure Python. It reads +and writes PNG files with 8/16/24/32/48/64 bits per pixel (greyscale, +RGB, RGBA, with 8 or 16 bits per layer), with a number of options. For +help, type "import png; help(png)" in your python interpreter. + +This file can also be used as a command-line utility to convert PNM +files to PNG. The interface is similar to that of the pnmtopng program +from the netpbm package. Type "python png.py --help" at the shell +prompt for usage and a list of options. +""" + + +__revision__ = '$Rev$' +__date__ = '$Date$' +__author__ = '$Author$' + + +import sys, zlib, struct, math +from array import array + + +_adam7 = ((0, 0, 8, 8), + (4, 0, 8, 8), + (0, 4, 4, 8), + (2, 0, 4, 4), + (0, 2, 2, 4), + (1, 0, 2, 2), + (0, 1, 1, 2)) + + +def interleave_planes(ipixels, apixels, ipsize, apsize): + """ + Interleave color planes, e.g. RGB + A = RGBA. + + Return an array of pixels consisting of the ipsize bytes of data + from each pixel in ipixels followed by the apsize bytes of data + from each pixel in apixels, for an image of size width x height. + """ + itotal = len(ipixels) + atotal = len(apixels) + newtotal = itotal + atotal + newpsize = ipsize + apsize + # Set up the output buffer + out = array('B') + # It's annoying that there is no cheap way to set the array size :-( + out.extend(ipixels) + out.extend(apixels) + # Interleave in the pixel data + for i in range(ipsize): + out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize] + for i in range(apsize): + out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize] + return out + + +class Writer: + """ + PNG encoder in pure Python. + """ + + def __init__(self, width, height, + transparent=None, + background=None, + gamma=None, + greyscale=False, + has_alpha=False, + bytes_per_sample=1, + compression=None, + interlaced=False, + chunk_limit=2**20): + """ + Create a PNG encoder object. + + Arguments: + width, height - size of the image in pixels + transparent - create a tRNS chunk + background - create a bKGD chunk + gamma - create a gAMA chunk + greyscale - input data is greyscale, not RGB + has_alpha - input data has alpha channel (RGBA) + bytes_per_sample - 8-bit or 16-bit input data + compression - zlib compression level (1-9) + chunk_limit - write multiple IDAT chunks to save memory + + If specified, the transparent and background parameters must + be a tuple with three integer values for red, green, blue, or + a simple integer (or singleton tuple) for a greyscale image. + + If specified, the gamma parameter must be a float value. + + """ + if width <= 0 or height <= 0: + raise ValueError("width and height must be greater than zero") + + if has_alpha and transparent is not None: + raise ValueError( + "transparent color not allowed with alpha channel") + + if bytes_per_sample < 1 or bytes_per_sample > 2: + raise ValueError("bytes per sample must be 1 or 2") + + if transparent is not None: + if greyscale: + if type(transparent) is not int: + raise ValueError( + "transparent color for greyscale must be integer") + else: + if not (len(transparent) == 3 and + type(transparent[0]) is int and + type(transparent[1]) is int and + type(transparent[2]) is int): + raise ValueError( + "transparent color must be a triple of integers") + + if background is not None: + if greyscale: + if type(background) is not int: + raise ValueError( + "background color for greyscale must be integer") + else: + if not (len(background) == 3 and + type(background[0]) is int and + type(background[1]) is int and + type(background[2]) is int): + raise ValueError( + "background color must be a triple of integers") + + self.width = width + self.height = height + self.transparent = transparent + self.background = background + self.gamma = gamma + self.greyscale = greyscale + self.has_alpha = has_alpha + self.bytes_per_sample = bytes_per_sample + self.compression = compression + self.chunk_limit = chunk_limit + self.interlaced = interlaced + + if self.greyscale: + self.color_depth = 1 + if self.has_alpha: + self.color_type = 4 + self.psize = self.bytes_per_sample * 2 + else: + self.color_type = 0 + self.psize = self.bytes_per_sample + else: + self.color_depth = 3 + if self.has_alpha: + self.color_type = 6 + self.psize = self.bytes_per_sample * 4 + else: + self.color_type = 2 + self.psize = self.bytes_per_sample * 3 + + def write_chunk(self, outfile, tag, data): + """ + Write a PNG chunk to the output file, including length and checksum. + """ + # http://www.w3.org/TR/PNG/#5Chunk-layout + outfile.write(struct.pack("!I", len(data))) + outfile.write(tag) + outfile.write(data) + checksum = zlib.crc32(tag) + checksum = zlib.crc32(data, checksum) + outfile.write(struct.pack("!I", checksum)) + + def write(self, outfile, scanlines): + """ + Write a PNG image to the output file. + """ + # http://www.w3.org/TR/PNG/#5PNG-file-signature + outfile.write(struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10)) + + # http://www.w3.org/TR/PNG/#11IHDR + if self.interlaced: + interlaced = 1 + else: + interlaced = 0 + self.write_chunk(outfile, 'IHDR', + struct.pack("!2I5B", self.width, self.height, + self.bytes_per_sample * 8, + self.color_type, 0, 0, interlaced)) + + # http://www.w3.org/TR/PNG/#11tRNS + if self.transparent is not None: + if self.greyscale: + self.write_chunk(outfile, 'tRNS', + struct.pack("!1H", *self.transparent)) + else: + self.write_chunk(outfile, 'tRNS', + struct.pack("!3H", *self.transparent)) + + # http://www.w3.org/TR/PNG/#11bKGD + if self.background is not None: + if self.greyscale: + self.write_chunk(outfile, 'bKGD', + struct.pack("!1H", *self.background)) + else: + self.write_chunk(outfile, 'bKGD', + struct.pack("!3H", *self.background)) + + # http://www.w3.org/TR/PNG/#11gAMA + if self.gamma is not None: + self.write_chunk(outfile, 'gAMA', + struct.pack("!L", int(self.gamma * 100000))) + + # http://www.w3.org/TR/PNG/#11IDAT + if self.compression is not None: + compressor = zlib.compressobj(self.compression) + else: + compressor = zlib.compressobj() + + data = array('B') + for scanline in scanlines: + data.append(0) + data.extend(scanline) + if len(data) > self.chunk_limit: + compressed = compressor.compress(data.tostring()) + if len(compressed): + # print >> sys.stderr, len(data), len(compressed) + self.write_chunk(outfile, 'IDAT', compressed) + data = array('B') + if len(data): + compressed = compressor.compress(data.tostring()) + else: + compressed = '' + flushed = compressor.flush() + if len(compressed) or len(flushed): + # print >> sys.stderr, len(data), len(compressed), len(flushed) + self.write_chunk(outfile, 'IDAT', compressed + flushed) + + # http://www.w3.org/TR/PNG/#11IEND + self.write_chunk(outfile, 'IEND', '') + + def write_array(self, outfile, pixels): + """ + Encode a pixel array to PNG and write output file. + """ + if self.interlaced: + self.write(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write(outfile, self.array_scanlines(pixels)) + + def convert_ppm(self, ppmfile, outfile): + """ + Convert a PPM file containing raw pixel data into a PNG file + with the parameters set in the writer object. + """ + if self.interlaced: + pixels = array('B') + pixels.fromfile(ppmfile, + self.bytes_per_sample * self.color_depth * + self.width * self.height) + self.write(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write(outfile, self.file_scanlines(ppmfile)) + + def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile): + """ + Convert a PPM and PGM file containing raw pixel data into a + PNG outfile with the parameters set in the writer object. + """ + pixels = array('B') + pixels.fromfile(ppmfile, + self.bytes_per_sample * self.color_depth * + self.width * self.height) + apixels = array('B') + apixels.fromfile(pgmfile, + self.bytes_per_sample * + self.width * self.height) + pixels = interleave_planes(pixels, apixels, + self.bytes_per_sample * self.color_depth, + self.bytes_per_sample) + if self.interlaced: + self.write(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write(outfile, self.array_scanlines(pixels)) + + def file_scanlines(self, infile): + """ + Generator for scanlines from an input file. + """ + row_bytes = self.psize * self.width + for y in range(self.height): + scanline = array('B') + scanline.fromfile(infile, row_bytes) + yield scanline + + def array_scanlines(self, pixels): + """ + Generator for scanlines from an array. + """ + row_bytes = self.width * self.psize + stop = 0 + for y in range(self.height): + start = stop + stop = start + row_bytes + yield pixels[start:stop] + + def old_array_scanlines_interlace(self, pixels): + """ + Generator for interlaced scanlines from an array. + http://www.w3.org/TR/PNG/#8InterlaceMethods + """ + row_bytes = self.psize * self.width + for xstart, ystart, xstep, ystep in _adam7: + for y in range(ystart, self.height, ystep): + if xstart < self.width: + if xstep == 1: + offset = y*row_bytes + yield pixels[offset:offset+row_bytes] + else: + row = array('B') + offset = y*row_bytes + xstart* self.psize + skip = self.psize * xstep + for x in range(xstart, self.width, xstep): + row.extend(pixels[offset:offset + self.psize]) + offset += skip + yield row + + def array_scanlines_interlace(self, pixels): + """ + Generator for interlaced scanlines from an array. + http://www.w3.org/TR/PNG/#8InterlaceMethods + """ + row_bytes = self.psize * self.width + for xstart, ystart, xstep, ystep in _adam7: + for y in range(ystart, self.height, ystep): + if xstart >= self.width: + continue + if xstep == 1: + offset = y * row_bytes + yield pixels[offset:offset+row_bytes] + else: + row = array('B') + # Note we want the ceiling of (self.width - xstart) / xtep + row_len = self.psize * ( + (self.width - xstart + xstep - 1) / xstep) + # There's no easier way to set the length of an array + row.extend(pixels[0:row_len]) + offset = y * row_bytes + xstart * self.psize + end_offset = (y+1) * row_bytes + skip = self.psize * xstep + for i in range(self.psize): + row[i:row_len:self.psize] = \ + pixels[offset+i:end_offset:skip] + yield row + +class _readable: + """ + A simple file-like interface for strings and arrays. + """ + + def __init__(self, buf): + self.buf = buf + self.offset = 0 + + def read(self, n): + r = buf[offset:offset+n] + if isinstance(r, array): + r = r.tostring() + offset += n + return r + +class Reader: + """ + PNG decoder in pure Python. + """ + + def __init__(self, _guess=None, **kw): + """ + Create a PNG decoder object. + + The constructor expects exactly one keyword argument. If you + supply a positional argument instead, it will guess the input + type. You can choose among the following arguments: + filename - name of PNG input file + file - object with a read() method + pixels - array or string with PNG data + + """ + if ((_guess is not None and len(kw) != 0) or + (_guess is None and len(kw) != 1)): + raise TypeError("Reader() takes exactly 1 argument") + + if _guess is not None: + if isinstance(_guess, array): + kw["pixels"] = _guess + elif isinstance(_guess, str): + kw["filename"] = _guess + elif isinstance(_guess, file): + kw["file"] = _guess + + if "filename" in kw: + self.file = file(kw["filename"]) + elif "file" in kw: + self.file = kw["file"] + elif "pixels" in kw: + self.file = _readable(kw["pixels"]) + else: + raise TypeError("expecting filename, file or pixels array") + + def read_chunk(self): + """ + Read a PNG chunk from the input file, return tag name and data. + """ + # http://www.w3.org/TR/PNG/#5Chunk-layout + data_bytes, tag = struct.unpack('!I4s', self.file.read(8)) + data = self.file.read(data_bytes) + checksum = struct.unpack('!i', self.file.read(4))[0] + verify = zlib.crc32(tag) + verify = zlib.crc32(data, verify) + if checksum != verify: + raise ValueError("checksum error in %s chunk: %x != %x" + % (tag, checksum, verify)) + return tag, data + + def _reconstruct_sub(self, offset, xstep, ystep): + """ + Reverse sub filter. + """ + pixels = self.pixels + a_offset = offset + offset += self.psize * xstep + if xstep == 1: + for index in range(self.psize, self.row_bytes): + x = pixels[offset] + a = pixels[a_offset] + pixels[offset] = (x + a) & 0xff + offset += 1 + a_offset += 1 + else: + byte_step = self.psize * xstep + for index in range(byte_step, self.row_bytes, byte_step): + for i in range(self.psize): + x = pixels[offset + i] + a = pixels[a_offset + i] + pixels[offset + i] = (x + a) & 0xff + offset += self.psize * xstep + a_offset += self.psize * xstep + + def _reconstruct_up(self, offset, xstep, ystep): + """ + Reverse up filter. + """ + pixels = self.pixels + b_offset = offset - (self.row_bytes * ystep) + if xstep == 1: + for index in range(self.row_bytes): + x = pixels[offset] + b = pixels[b_offset] + pixels[offset] = (x + b) & 0xff + offset += 1 + b_offset += 1 + else: + for index in range(0, self.row_bytes, xstep * self.psize): + for i in range(self.psize): + x = pixels[offset + i] + b = pixels[b_offset + i] + pixels[offset + i] = (x + b) & 0xff + offset += self.psize * xstep + b_offset += self.psize * xstep + + def _reconstruct_average(self, offset, xstep, ystep): + """ + Reverse average filter. + """ + pixels = self.pixels + a_offset = offset - (self.psize * xstep) + b_offset = offset - (self.row_bytes * ystep) + if xstep == 1: + for index in range(self.row_bytes): + x = pixels[offset] + if index < self.psize: + a = 0 + else: + a = pixels[a_offset] + if b_offset < 0: + b = 0 + else: + b = pixels[b_offset] + pixels[offset] = (x + ((a + b) >> 1)) & 0xff + offset += 1 + a_offset += 1 + b_offset += 1 + else: + for index in range(0, self.row_bytes, self.psize * xstep): + for i in range(self.psize): + x = pixels[offset+i] + if index < self.psize: + a = 0 + else: + a = pixels[a_offset + i] + if b_offset < 0: + b = 0 + else: + b = pixels[b_offset + i] + pixels[offset + i] = (x + ((a + b) >> 1)) & 0xff + offset += self.psize * xstep + a_offset += self.psize * xstep + b_offset += self.psize * xstep + + def _reconstruct_paeth(self, offset, xstep, ystep): + """ + Reverse Paeth filter. + """ + pixels = self.pixels + a_offset = offset - (self.psize * xstep) + b_offset = offset - (self.row_bytes * ystep) + c_offset = b_offset - (self.psize * xstep) + # There's enough inside this loop that it's probably not worth + # optimising for xstep == 1 + for index in range(0, self.row_bytes, self.psize * xstep): + for i in range(self.psize): + x = pixels[offset+i] + if index < self.psize: + a = c = 0 + b = pixels[b_offset+i] + else: + a = pixels[a_offset+i] + b = pixels[b_offset+i] + c = pixels[c_offset+i] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + pixels[offset+i] = (x + pr) & 0xff + offset += self.psize * xstep + a_offset += self.psize * xstep + b_offset += self.psize * xstep + c_offset += self.psize * xstep + + # N.B. PNG files with 'up', 'average' or 'paeth' filters on the + # first line of a pass are legal. The code above for 'average' + # deals with this case explicitly. For up we map to the null + # filter and for paeth we map to the sub filter. + + def reconstruct_line(self, filter_type, first_line, offset, xstep, ystep): + # print >> sys.stderr, "Filter type %s, first_line=%s" % (filter_type, first_line) + filter_type += (first_line << 8) + if filter_type == 1 or filter_type == 0x101 or filter_type == 0x104: + self._reconstruct_sub(offset, xstep, ystep) + elif filter_type == 2: + self._reconstruct_up(offset, xstep, ystep) + elif filter_type == 3 or filter_type == 0x103: + self._reconstruct_average(offset, xstep, ystep) + elif filter_type == 4: + self._reconstruct_paeth(offset, xstep, ystep) + return + + def deinterlace(self, scanlines): + # print >> sys.stderr, "Reading interlaced, w=%s, r=%s, planes=%s, bpp=%s" % (self.width, self.height, self.planes, self.bps) + a = array('B') + self.pixels = a + # Make the array big enough + temp = scanlines[0:self.width*self.height*self.psize] + a.extend(temp) + source_offset = 0 + for xstart, ystart, xstep, ystep in _adam7: + # print >> sys.stderr, "Adam7: start=%s,%s step=%s,%s" % (xstart, ystart, xstep, ystep) + filter_first_line = 1 + for y in range(ystart, self.height, ystep): + if xstart >= self.width: + continue + filter_type = scanlines[source_offset] + source_offset += 1 + if xstep == 1: + offset = y * self.row_bytes + a[offset:offset+self.row_bytes] = scanlines[source_offset:source_offset + self.row_bytes] + source_offset += self.row_bytes + else: + # Note we want the ceiling of (width - xstart) / xtep + row_len = self.psize * ((self.width - xstart + xstep - 1) / xstep) + offset = y * self.row_bytes + xstart * self.psize + end_offset = (y+1) * self.row_bytes + skip = self.psize * xstep + for i in range(self.psize): + a[offset+i:end_offset:skip] = scanlines[source_offset + i: source_offset + row_len: self.psize] + source_offset += row_len + if filter_type: + self.reconstruct_line(filter_type, filter_first_line, offset, xstep, ystep) + filter_first_line = 0 + return a + + def read_flat(self, scanlines): + a = array('B') + self.pixels = a + offset = 0 + source_offset = 0 + filter_first_line = 1 + for y in range(self.height): + filter_type = scanlines[source_offset] + source_offset += 1 + a.extend(scanlines[source_offset: source_offset + self.row_bytes]) + if filter_type: + self.reconstruct_line(filter_type, filter_first_line, offset, 1, 1) + filter_first_line = 0 + offset += self.row_bytes + source_offset += self.row_bytes + return a + + def read(self): + """ + Read a simple PNG file, return width, height, pixels and image metadata + + This function is a very early prototype with limited flexibility + and excessive use of memory. + """ + signature = self.file.read(8) + if (signature != struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10)): + raise Exception("PNG file has invalid header") + compressed = [] + image_metadata = {} + while True: + tag, data = self.read_chunk() + # print >> sys.stderr, tag, len(data) + if tag == 'IHDR': # http://www.w3.org/TR/PNG/#11IHDR + (width, height, bits_per_sample, color_type, + compression_method, filter_method, + interlaced) = struct.unpack("!2I5B", data) + bps = bits_per_sample / 8 + if bps == 0: + raise Exception("unsupported pixel depth") + if bps > 2 or bits_per_sample != (bps * 8): + raise Exception("invalid pixel depth") + if color_type == 0: + greyscale = True + has_alpha = False + planes = 1 + elif color_type == 2: + greyscale = False + has_alpha = False + planes = 3 + elif color_type == 4: + greyscale = True + has_alpha = True + planes = 2 + elif color_type == 6: + greyscale = False + has_alpha = True + planes = 4 + else: + raise Exception("unknown PNG colour type %s" % color_type) + if compression_method != 0: + raise Exception("unknown compression method") + if filter_method != 0: + raise Exception("unknown filter method") + self.bps = bps + self.planes = planes + self.psize = bps * planes + self.width = width + self.height = height + self.row_bytes = width * self.psize + elif tag == 'IDAT': # http://www.w3.org/TR/PNG/#11IDAT + compressed.append(data) + elif tag == 'bKGD': + if greyscale: + image_metadata["background"] = struct.unpack("!1H", data) + else: + image_metadata["background"] = struct.unpack("!3H", data) + elif tag == 'tRNS': + if greyscale: + image_metadata["transparent"] = struct.unpack("!1H", data) + else: + image_metadata["transparent"] = struct.unpack("!3H", data) + elif tag == 'gAMA': + image_metadata["gamma"] = (struct.unpack("!L", data)[0]) / 100000.0 + elif tag == 'IEND': # http://www.w3.org/TR/PNG/#11IEND + break + scanlines = array('B', zlib.decompress(''.join(compressed))) + if interlaced: + pixels = self.deinterlace(scanlines) + else: + pixels = self.read_flat(scanlines) + image_metadata["greyscale"] = greyscale + image_metadata["has_alpha"] = has_alpha + image_metadata["bytes_per_sample"] = bps + image_metadata["interlaced"] = interlaced + return width, height, pixels, image_metadata + +def test_suite(options): + """ + Run regression test and write PNG file to stdout. + """ + + # Below is a big stack of test image generators + + def test_gradient_horizontal_lr(x, y): + return x + + def test_gradient_horizontal_rl(x, y): + return 1-x + + def test_gradient_vertical_tb(x, y): + return y + + def test_gradient_vertical_bt(x, y): + return 1-y + + def test_radial_tl(x, y): + return max(1-math.sqrt(x*x+y*y), 0.0) + + def test_radial_center(x, y): + return test_radial_tl(x-0.5, y-0.5) + + def test_radial_tr(x, y): + return test_radial_tl(1-x, y) + + def test_radial_bl(x, y): + return test_radial_tl(x, 1-y) + + def test_radial_br(x, y): + return test_radial_tl(1-x, 1-y) + + def test_stripe(x, n): + return 1.0*(int(x*n) & 1) + + def test_stripe_h_2(x, y): + return test_stripe(x, 2) + + def test_stripe_h_4(x, y): + return test_stripe(x, 4) + + def test_stripe_h_10(x, y): + return test_stripe(x, 10) + + def test_stripe_v_2(x, y): + return test_stripe(y, 2) + + def test_stripe_v_4(x, y): + return test_stripe(y, 4) + + def test_stripe_v_10(x, y): + return test_stripe(y, 10) + + def test_stripe_lr_10(x, y): + return test_stripe(x+y, 10) + + def test_stripe_rl_10(x, y): + return test_stripe(x-y, 10) + + def test_checker(x, y, n): + return 1.0*((int(x*n) & 1) ^ (int(y*n) & 1)) + + def test_checker_8(x, y): + return test_checker(x, y, 8) + + def test_checker_15(x, y): + return test_checker(x, y, 15) + + def test_zero(x, y): + return 0 + + def test_one(x, y): + return 1 + + test_patterns = { + "GLR" : test_gradient_horizontal_lr, + "GRL" : test_gradient_horizontal_rl, + "GTB" : test_gradient_vertical_tb, + "GBT" : test_gradient_vertical_bt, + "RTL" : test_radial_tl, + "RTR" : test_radial_tr, + "RBL" : test_radial_bl, + "RBR" : test_radial_br, + "RCTR" : test_radial_center, + "HS2" : test_stripe_h_2, + "HS4" : test_stripe_h_4, + "HS10" : test_stripe_h_10, + "VS2" : test_stripe_v_2, + "VS4" : test_stripe_v_4, + "VS10" : test_stripe_v_10, + "LRS" : test_stripe_lr_10, + "RLS" : test_stripe_rl_10, + "CK8" : test_checker_8, + "CK15" : test_checker_15, + "ZERO" : test_zero, + "ONE" : test_one, + } + + def test_pattern(width, height, depth, pattern): + a = array('B') + fw = float(width) + fh = float(height) + pfun = test_patterns[pattern] + if depth == 1: + for y in range(height): + for x in range(width): + a.append(int(pfun(float(x)/fw, float(y)/fh) * 255)) + elif depth == 2: + for y in range(height): + for x in range(width): + v = int(pfun(float(x)/fw, float(y)/fh) * 65535) + a.append(v >> 8) + a.append(v & 0xff) + return a + + def test_rgba(size=256, depth=1, + red="GTB", green="GLR", blue="RTL", alpha=None): + r = test_pattern(size, size, depth, red) + g = test_pattern(size, size, depth, green) + b = test_pattern(size, size, depth, blue) + if alpha: + a = test_pattern(size, size, depth, alpha) + i = interleave_planes(r, g, depth, depth) + i = interleave_planes(i, b, 2 * depth, depth) + if alpha: + i = interleave_planes(i, a, 3 * depth, depth) + return i + + # The body of test_suite() + size = 256 + if options.test_size: + size = options.test_size + depth = 1 + if options.test_deep: + depth = 2 + + kwargs = {} + if options.test_red: + kwargs["red"] = options.test_red + if options.test_green: + kwargs["green"] = options.test_green + if options.test_blue: + kwargs["blue"] = options.test_blue + if options.test_alpha: + kwargs["alpha"] = options.test_alpha + pixels = test_rgba(size, depth, **kwargs) + + writer = Writer(size, size, + bytes_per_sample=depth, + transparent=options.transparent, + background=options.background, + gamma=options.gamma, + has_alpha=options.test_alpha, + compression=options.compression, + interlaced=options.interlace) + writer.write_array(sys.stdout, pixels) + + +def read_pnm_header(infile, supported='P6'): + """ + Read a PNM header, return width and height of the image in pixels. + """ + header = [] + while len(header) < 4: + line = infile.readline() + sharp = line.find('#') + if sharp > -1: + line = line[:sharp] + header.extend(line.split()) + if len(header) == 3 and header[0] == 'P4': + break # PBM doesn't have maxval + if header[0] not in supported: + raise NotImplementedError('file format %s not supported' % header[0]) + if header[0] != 'P4' and header[3] != '255': + raise NotImplementedError('maxval %s not supported' % header[3]) + return int(header[1]), int(header[2]) + + +# FIXME: Somewhere we need support for greyscale backgrounds etc. +def color_triple(color): + """ + Convert a command line color value to a RGB triple of integers. + """ + if color.startswith('#') and len(color) == 4: + return (int(color[1], 16), + int(color[2], 16), + int(color[3], 16)) + if color.startswith('#') and len(color) == 7: + return (int(color[1:3], 16), + int(color[3:5], 16), + int(color[5:7], 16)) + elif color.startswith('#') and len(color) == 13: + return (int(color[1:5], 16), + int(color[5:9], 16), + int(color[9:13], 16)) + + +def _main(): + """ + Run the PNG encoder with options from the command line. + """ + # Parse command line arguments + from optparse import OptionParser + version = '%prog ' + __revision__.strip('$').replace('Rev: ', 'r') + parser = OptionParser(version=version) + parser.set_usage("%prog [options] [pnmfile]") + parser.add_option("-i", "--interlace", + default=False, action="store_true", + help="create an interlaced PNG file (Adam7)") + parser.add_option("-t", "--transparent", + action="store", type="string", metavar="color", + help="mark the specified color as transparent") + parser.add_option("-b", "--background", + action="store", type="string", metavar="color", + help="save the specified background color") + parser.add_option("-a", "--alpha", + action="store", type="string", metavar="pgmfile", + help="alpha channel transparency (RGBA)") + parser.add_option("-g", "--gamma", + action="store", type="float", metavar="value", + help="save the specified gamma value") + parser.add_option("-c", "--compression", + action="store", type="int", metavar="level", + help="zlib compression level (0-9)") + parser.add_option("-T", "--test", + default=False, action="store_true", + help="create a test image") + parser.add_option("-R", "--test-red", + action="store", type="string", metavar="pattern", + help="test pattern for the red image layer") + parser.add_option("-G", "--test-green", + action="store", type="string", metavar="pattern", + help="test pattern for the green image layer") + parser.add_option("-B", "--test-blue", + action="store", type="string", metavar="pattern", + help="test pattern for the blue image layer") + parser.add_option("-A", "--test-alpha", + action="store", type="string", metavar="pattern", + help="test pattern for the alpha image layer") + parser.add_option("-D", "--test-deep", + default=False, action="store_true", + help="use test patterns with 16 bits per layer") + parser.add_option("-S", "--test-size", + action="store", type="int", metavar="size", + help="width and height of the test image") + (options, args) = parser.parse_args() + + # Convert options + if options.transparent is not None: + options.transparent = color_triple(options.transparent) + if options.background is not None: + options.background = color_triple(options.background) + + # Run regression tests + if options.test: + return test_suite(options) + + # Prepare input and output files + if len(args) == 0: + ppmfilename = '-' + ppmfile = sys.stdin + elif len(args) == 1: + ppmfilename = args[0] + ppmfile = open(ppmfilename, 'rb') + else: + parser.error("more than one input file") + outfile = sys.stdout + + # Encode PNM to PNG + width, height = read_pnm_header(ppmfile) + writer = Writer(width, height, + transparent=options.transparent, + background=options.background, + has_alpha=options.alpha is not None, + gamma=options.gamma, + compression=options.compression) + if options.alpha is not None: + pgmfile = open(options.alpha, 'rb') + awidth, aheight = read_pnm_header(pgmfile, 'P5') + if (awidth, aheight) != (width, height): + raise ValueError("alpha channel image size mismatch" + + " (%s has %sx%s but %s has %sx%s)" + % (ppmfilename, width, height, + options.alpha, awidth, aheight)) + writer.convert_ppm_and_pgm(ppmfile, pgmfile, outfile, + interlace=options.interlace) + else: + writer.convert_ppm(ppmfile, outfile, + interlace=options.interlace) + +if __name__ == '__main__': + _main() diff --git a/src/lib/scene/opengl/texture.py b/src/lib/scene/opengl/texture.py new file mode 100644 index 0000000..c022f1a --- /dev/null +++ b/src/lib/scene/opengl/texture.py @@ -0,0 +1,133 @@ +from OpenGL.GL import * +from OpenGL.GLU import * +import png +import array + +class Texture: + """ + """ + # Texture data + __data = None + __format = GL_RGB + __width = None + __height = None + + # Material properties + __ambient = None + __diffuse = None + __specular = None + __emission = None + __shininess = None + + # OpenGL texture ID + __texture = None + + def __init__(self, fileName, + ambient = (0.2, 0.2, 0.2, 1.0), + diffuse = (0.8, 0.8, 0.8, 1.0), + specular = (0.0, 0.0, 0.0, 1.0), + emission = (0.0, 0.0, 0.0, 1.0), + shininess = 0.0): + """Constructor for an openGL texture. + + 'fileName' is the name of the image file to use for the texture (string). + + An IOError is raised if the file does not exist. + This does not need an openGL context. + """ + self.__ambient = ambient + self.__diffuse = diffuse + self.__specular = specular + self.__emission = emission + self.__shininess = shininess + try: + self.__loadPIL(fileName) + except ImportError: + self.__loadPNG(fileName) + + def __loadPNG(self, fileName): + """ + """ + reader = png.Reader(fileName) + (width, height, data, metaData) = reader.read() + + self.__width = width + self.__height = height + self.__data = array.array('B', data).tostring() + + if metaData['has_alpha']: + self.__format = GL_RGBA + else: + self.__format = GL_RGB + + def __loadPIL(self, fileName): + """ + """ + import Image + + # Load the image file + image = Image.open(fileName) + + # Crop the image so it has height/width a multiple of 2 + width = image.size[0] + height = image.size[1] + w = 1 + while 2*w <= width: + w *= 2 + h = 1 + while 2*h <= height: + h *= 2 + (self.__width, self.__height) = (w, h) + image = image.crop((0, 0, w, h)) + + # Convert to a format that OpenGL can access + self.__data = image.tostring('raw', 'RGB', 0, -1) + self.__format = GL_RGB + + def __generate(self): + """ + """ + # FIXME: Can fail + texture = glGenTextures(1) + + glBindTexture(GL_TEXTURE_2D, texture) + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + + #glTexImage2D(GL_TEXTURE_2D, + # 0, # Level + # 3, # Depth + # width, # Width + # height, # Height + # 0, # Border + # GL_RGB, # Format + # GL_UNSIGNED_BYTE, # Type + # data) + + # Generate mipmaps + gluBuild2DMipmaps(GL_TEXTURE_2D, GL_LUMINANCE, self.__width, self.__height, self.__format, GL_UNSIGNED_BYTE, self.__data) + + return texture + + def bind(self): + """Bind this texture to the current surface. + + This requires an openGL context. + """ + if self.__texture is None: + self.__texture = self.__generate() + self.__data = None + + # Use texture + glBindTexture(GL_TEXTURE_2D, self.__texture) + + # Use material properties + glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, self.__ambient) + glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, self.__diffuse) + glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, self.__specular) + glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, self.__emission) + glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, self.__shininess) diff --git a/src/lib/uci.py b/src/lib/uci.py new file mode 100644 index 0000000..a444126 --- /dev/null +++ b/src/lib/uci.py @@ -0,0 +1,161 @@ + +class StateMachine: + """ + """ + + STATE_IDLE = 'IDLE' + STATE_CONNECTING = 'CONNECTING' + + options = None + + buffer = '' + + __readyToConfigure = False + __options = None + + __ready = False + __inCallback = False + __queuedCommands = None + + def __init__(self): + """ + """ + self.options = {} + self.__queuedCommands = [] + + def logText(self, text, style): + """ + """ + pass + + def onOutgoingData(self, data): + """ + """ + pass + + def onMove(self, move): + """Called when the AI makes a move. + + 'move' is the move the AI has decided to make (string). + """ + print 'UCI move: ' + move + + def registerIncomingData(self, data): + """ + """ + self.__inCallback = True + self.buffer += data + while True: + index = self.buffer.find('\n') + if index < 0: + break + line = self.buffer[:index] + self.buffer = self.buffer[index + 1:] + self.parseLine(line) + self.__inCallback = False + + if self.__options is not None and self.__readyToConfigure: + options = self.__options + self.__options = None + self.configure(options) + + # Send queued commands once have OK + if len(self.__queuedCommands) > 0 and self.__ready: + commands = self.__queuedCommands + self.__queuedCommands = [] + for c in commands: + self.__sendCommand(c) + + def __sendCommand(self, command): + """ + """ + if self.__ready and not self.__inCallback: + self.onOutgoingData(command + '\n') + else: + self.__queuedCommands.append(command) + + def start(self): + """ + """ + self.onOutgoingData('uci\n') + + def configure(self, options): + """ + """ + if not self.__readyToConfigure: + self.__options = options + return + + for option in options: + if not hasattr(option, 'name'): + print 'Ignoring unnamed UCI option' + continue + if option.value == '': + continue + self.onOutgoingData('setoption ' + option.name + ' value ' + option.value + '\n') + self.onOutgoingData('isready\n') + + def requestMove(self): + """ + """ + self.__sendCommand('go') + + def reportMove(self, move, isSelf): + """ + """ + self.__sendCommand('position moves ' + move) + + def parseLine(self, line): + """ + """ + words = line.split() + + while True: + if len(words) == 0: + self.logText(line + '\n', 'input') + return + + style = self.parseCommand(words[0], words[1:]) + if style is not None: + self.logText(line + '\n', style) + return + + print 'WARNING: Unknown command: ' + repr(words[0]) + words = words[1:] + + def parseCommand(self, command, args): + """ + """ + if command == 'id': + return 'info' + + elif command == 'uciok': + if len(args) != 0: + print 'WARNING: Arguments on uciok: ' + str(args) + self.__readyToConfigure = True + return 'info' + + elif command == 'readyok': + if len(args) != 0: + print 'WARNING: Arguments on readyok: ' + str(args) + self.__ready = True + return 'info' + + elif command == 'bestmove': + if len(args) == 0: + print 'WARNING: No move with bestmove' + return 'error' + else: + move = args[0] + self.onMove(move) + + # TODO: Check for additional ponder information + return 'move' + + elif command == 'info': + return 'info' + + elif command == 'option': + return 'info' + + return None diff --git a/src/lib/ui/Makefile.am b/src/lib/ui/Makefile.am new file mode 100644 index 0000000..cc0e9cf --- /dev/null +++ b/src/lib/ui/Makefile.am @@ -0,0 +1,4 @@ +glchessdir = $(pythondir)/glchess/ui +glchess_PYTHON = \ + __init__.py \ + ui.py diff --git a/src/lib/ui/__init__.py b/src/lib/ui/__init__.py new file mode 100644 index 0000000..ce30bcc --- /dev/null +++ b/src/lib/ui/__init__.py @@ -0,0 +1 @@ +from ui import * diff --git a/src/lib/ui/ui.py b/src/lib/ui/ui.py new file mode 100644 index 0000000..283c14c --- /dev/null +++ b/src/lib/ui/ui.py @@ -0,0 +1,259 @@ +""" +""" + +__author__ = 'Robert Ancell ' +__license__ = 'GNU General Public License Version 2' +__copyright__ = 'Copyright 2005-2006 Robert Ancell' + +class ViewFeedback: + """Template class for feedback from a view object""" + + def saveGame(self, path): + """Called when the user requests the game in this view to be saved. + + 'path' is the path to the file to save to (string). + """ + print 'Save game to ' + path + + def renderGL(self): + """Render the scene using OpenGL""" + pass + + def renderCairoStatic(self, context): + """Render the static elements of the scene. + + 'context' is the cairo context to modify. + + Return False if the static elements have not changed otherwise True. + """ + return False + + def renderCairoDynamic(self, context): + """Render the dynamic elements of the scene. + + 'context' is the cairo context to modify. + """ + pass + + def reshape(self, width, height): + """This method is called when the UI resizes the scene. + + 'width' is the new width of the scene in pixels (integer). + 'height' is the new height of the scene in pixels (integer). + """ + pass + + def select(self, x, y): + """This method is called when the UI selects a position on the scene. + + 'x' is the horizontal pixel location when the user has selected (integer, 0 = left pixel). + 'y' is the vertical pixel location when the user has selected (integer, 0 = top pixel). + """ + pass + + def deselect(self, x, y): + """This method is called when the UI deselects a position on the scene. + + 'x' is the horizontal pixel location when the user has selected (integer, 0 = left pixel). + 'y' is the vertical pixel location when the user has selected (integer, 0 = top pixel). + """ + pass + + def setMoveNumber(self, moveNumber): + """This method is called when the UI changes the move to render. + + 'moveNumber' is the moveNumber to watch (integer, negative numbers index from latest move). + """ + pass + + def save(self, filename): + """Save the game using this view. + + 'filename' is the file to save to (string). + """ + pass + + def close(self): + """This method is called when the user requests this view be closed""" + pass + +class ViewController: + """Template class for methods to control a view""" + + def addMove(self, move): + """Register a move with this view. + + 'move' TODO + """ + pass + + def render(self): + """Request this view is redrawn""" + pass + + def close(self): + """Close this view""" + pass + +class UI: + """Template class for a glChess UI. + """ + + # Methods for glChess to implement + + def onAnimate(self, timeStep): + """Called when an animation tick occurs. + + 'timeStep' is the time between the last call to this method in seconds (float). + + Return True if animation should continue otherwise False + """ + return False + + def onReadFileDescriptor(self, fd): + """Called when a file descriptor is able to be read. + + 'fds' is the file descriptor with available data to read (integer). + + Return False when finished otherwise True. + """ + pass + + def onGameStart(self, gameName, allowSpectators, whiteName, whiteType, blackName, blackType, moves = None): + """Called when a local game is started. + + 'gameName' is the name of the game to create (string). + 'allowSpectators' is a flag to show if remote clients can watch this game (True or False). + 'whiteName' is the name of the white player. + 'whiteType' is the local player type. PLAYER_* or the AI type (string) or None for open. + 'blackName' is the name of the black player. + 'blackType' is the black player type. PLAYER_* or the AI type (string) or None for open. + 'moves' is a list of moves (strings) to start the game with. + """ + pass + + def loadGame(self, path, returnResult): + """Called when a game is loaded. + + 'path' is the path to the game to load (string). + 'returnResult' is a flag to show if the UI requires the result of the load. + If True call reportGameLoaded() if the game can be loaded. + """ + msg = 'Loading game ' + path + if configureGame: + msg += ' after configuring' + print msg + + def onGameJoin(self, localName, localType, game): + """Called when a network game is started (remote server). + + 'localName' is the name of the local player (string). + 'localType' is the local player type. PLAYER_* or the AI type (string). + 'game' is the game to join (as passed in addNetworkGame). + """ + pass + + def onNetworkServerSearch(self, hostName=None): + """Called when the user searches for servers. + + 'hostName' is the name of the host to look for servers on (string) or + None if search whole network. + """ + pass + + def onNetworkGameStart(self, localName, localType, serverHost, serverId): + """Called when a network game is started (remote server). + + 'localName' is the name of the local player (string). + 'localType' is the local player type. PLAYER_* or the AI type (string). + 'serverHost' is the hostname of the server to connect to. + 'serverId' is the ID of the server to connect to. + """ + pass + + def onNetworkGameServerStart(self, localName, localType, serverName): + """Called when a network game is started (local server). + + 'localName' is the name of the local player (string). + 'localType' is the local player type. PLAYER_* or the AI type (string). + 'serverName' is the name of the server to start. + """ + pass + + def onQuit(self): + """Called when the user quits the program""" + pass + + # Methods for the UI to implement + + def startAnimation(self): + """Start the animation callback""" + pass + + def watchFileDescriptor(self, fd): + """Notify when a file descriptor is able to be read. + + 'fd' is the file descriptor to watch (integer). + + When data is available onReadFileDescriptor() is called. + """ + pass + + def setDefaultView(self, feedback): + """Set the default view to render. + + 'feedback' is a object to report view events with (extends ViewFeedback). + + This will override the previous default view. + + Returns a view controller object (extends ViewController). + """ + return None + + def addView(self, title, feedback): + """Add a view to the UI. + + 'title' is the title for the view (string). + 'feedback' is a object to report view events with (extends ViewFeedback). + + Returns a view controller object (extends ViewController). + """ + return None + + def reportError(self, title, error): + """Report an error. + + 'title' is the title of the error (string). + 'error' is the description of the error (string). + """ + pass + + def reportGameLoaded(self, gameName = None, + whiteName = None, blackName = None, + whiteAI = None, blackAI = None, moves = None): + """Report a loaded game as required by onGameLoad(). + + 'gameName' is the name of the game (string) or None if unknown. + 'whiteName' is the name of the white player (string) or None if unknown. + 'blackName' is the name of the white player (string) or None if unknown. + 'whiteAI' is the type of AI the white player is (string) or None if no AI. + 'blackAI' is the type of AI the black player is (string) or None if no AI. + 'moves' is a list of moves (strings) that the have already been made. + """ + pass + + def addNetworkGame(self, name, game): + """Report a detected network game. + + 'name' is the name of the network game (string). + 'game' is the game detected (user-defined). + """ + pass + + def removeNetworkGame(self, game): + """Report a network game as terminated. + + 'game' is the game that has removed (as registered with addNetworkGame()). + """ + pass + \ No newline at end of file diff --git a/textures/Makefile.am b/textures/Makefile.am new file mode 100644 index 0000000..f709eb9 --- /dev/null +++ b/textures/Makefile.am @@ -0,0 +1,6 @@ +EXTRA_DIST = glchess.svg board.png piece.png +icondir = $(datadir)/pixmaps +icon_DATA = glchess.svg + +pixmapdir = $(datadir)/pixmaps/glchess/ +pixmap_DATA = board.png piece.png diff --git a/textures/board.png b/textures/board.png new file mode 100644 index 0000000000000000000000000000000000000000..dc2e66df4cef8ee81c4a392b3a38d0e8e11da32d GIT binary patch literal 8804 zcmV-qBAeZbP)34fr_rn#KIW-0(a+O*U_O z9&T=QzB3CXZ;02m1)2AgpULmqqRsD{IiAHe_m+#2R)W`>@vue5ojBvxHpDEhv=X#% z+(7xBSys9@-rIa<r4P#qv4P+!o8OrQaJgLWrp$1KUEj>`_(Zy=98eoDnreQ= zOfW6vn0IqW4I|8N(*mYdn$PiGZ7jIR=t}6a@*d`Y+dRyC$S%wUK=Wk_$IKE3BnNGN zGr<>qe7>$1#K?q_4W0^J(_Fa;?8lKMPM5*$9A=tj>qF+%fZP(Zf^4cpO+(IQzdWMe%{APhMwx*>?@H$qCoP=2fx(Wik{LQ>y zqI^!W0XvR=&H?8sUEr3hG|imFmh%ua?c5b4T2_;2R^T&>WY0%39J9su`uh3-JB-OT zvpk=-*u90gGrk$J(M1sWCFzu(Cr3kK$;PRH(>u{O7fO&|j9NZGBQUslT4M1JI|M zf45kYG49G#=L4rnBB9Zkta)9p*JY@h?u(%s?|TOFdc8jED28DSBPWDixwka&E9+;W zwb9?)9AFL8*SuSH1 zJriO~T4hAR&6*oBC!BeV0h&2h19rCoD@-(4a)LL@fy;-OosbVh)0aE8W%6vnnQKTs zBd^`ezj5a@t9-sRvE03e8QL=?-r+(cJDN!`1AoT24xwlFHUM=#v$gopc1|;kjkU^n zx%ux5yUfHV*H7M_zddGd6^~nllfS{J#OPa#a3+=f_V#wTUa!kkF*AhQwtd)SnUbFO z>4J02GLo6!YoI>)kS$ZVM&jfRzP!BXkTv+5ZYq_iJsdIHP+2oPBXRR-;Ho0FdV#*sGXBc zCDn`xcf6581l#jorqNemkp$HC^75koiDZWD+?J?lcp&|-{f4D#ErMt?bY{O=n$*M= zdCxhJnN*@RGL2wbjIpe3F*2pjtRh9TCxK>RQ0YXgl9qC7WWh>kxym1qf;DWAtX1>fT*(AD#)&r#LzAGjSH{v*2DST5 zphb{KKHu0)F3OgXHEqG%YjeG`32h^9&(F`x9b_i9i896nHMv%~O$^XcYmP0DOa+Zp6}pBzT+$j?r=>FIJZEZb@B zau#^X;Q2XLO*JaMiB*`HHCJfzw>uR~(d7Ja{>^C0-1#;TMrO=TTJa``Ygdu2;2-aO4W!xe@`CSqvPb;~9aSaJY4sIRZD4_UmnvsY~8 zN{2|F-b%KtDAf{VW?fqdo?_Auw6G~oo3Mc3!X`K$*pwNcNG#?!Qx51}6ThOr8_+wD;gKoi?)1}RnM zpk@e@J1u{Y2$zc3ZQB&gizRVh@)!A;I(`bP2%nG`a2 zV5mC>IxQiSVRP>l}Yc6>N<7_Ix5 zm5o{S#=jFQjc`)PEyDgqLt@c4<%aOd4}utddOs7{^s2Gwi0=)kg*|WlV*{MY!95DXNY7R zzfsa$K&FmebnqYG)ml^ic4!R)%$0sQ(KtkJFP@c{Hlt!C7c)*4Z3G8Kd-^2Rb&iPU}8xp;|0dw zS`A^|Q!8U5S;HtR!Dy0hE3qqk8{DIShDNO}FiW2eL^b6}hK^i?Mo?H~Zc`syS4^UA z`#Y^2HG4+CwotL@|FTo4wZBbm19x!(&P4J#Khp?;xP|3kTo@T5Z@e%E&L)(Lr1_uL zpeT1t^7K}$xzZfatXiLe7D3DoB>_VdyI0X2x|g zJw5%?Br3zeRzShJe5QQc;E>ItZ(9*X`EZu6Fc8{gQ>#g~OrLcFGXaTx--=dqtV}er zs&8P~nNjYnSD-M2Zs&MB{v!!%gKf;y{5#*BiLe#i`0unZE#*to&w-wWM4;GWV;k@$ ze6>hpm49zN4VfZey`Q@_8&W5r&il4v8M@iY9gyFd-*{*c!2wGmBzOBf?B#O#Pz;)3 zP+JK6*UGuAX|yd;7G<&^m5Yw%x)qjik*-`2!z*it+giINJI7F#zp-uG9bC7%Lz_E> zzz>R`y@rqkCR|0?+<@s#%b2bF7eygCfvxL@5hJW|Y}XvgTVpEODwm7T+RE@A3~*9( zV=^(~AX}(ZDRV)R(8_f2{P&h@R1+>T9Aic<36P8wtgSmk#TKrl+hEAU1+XgeV2vYp z;9lcvDuee*%cfL|Azp@V>p0o@+)LXIKttKpNC`Y7q~)u#BMa-wM?gEy}< zm!@fFCoy)<$}SyttjsDaM}XB7J;_fr^{=UCERJs-L`?@U3}#->D0Gv6H;%xpgEiY= z_Hz|gS#+`e`RAV>(zqKVmgd$k-Avw{+5=ks&@`+;Bhl-!SrFtZ%xB<3&So+xPcRHO8m&@gFxm>h1(V_?ofs&(W-M-t-wJs^G>^c#4UaK*Y>7H7olHnl>T4p1p z@FlZ}tHU!}Gs?8s+m|n2K1ijQ_2X`CEzlWf=ORetKdnl0XI?Is2g{PX#^hP ze~`Th=X0~`+tFuC@>sD$Yb0rCo3Xsr2T;nDxqB{d(*fFXHMud`QFG+(S2=xZ7L6<5 zIN3tEyMULMmxu6yE#Iv@S}VV54Wz9Bf)zRDi}i0jR(1wBqmx!~OI%`ZjWM;=7Uu6{ ztlt`6u3F|1>F1>KR1>Swq~gd#DlN7DumGN(o*s^F$(za0V|74OnoY}VGku#`JStV@ zp)HJ|Z{?!QBni>Iq51)U7XQ!nE|<&hAr(!j%({g=cf>xwD}?{_^z_h5W~JUHn0#mv zGpZ6ww|JN`cdA7RjjC;(w+Z^1fRZ)sY|+MEz)i8kIGeo)8l}u?s0Jjhr%y{Sw_?(j zoGU+Tva{BQ$a)Ol>jI3}$%4;J9GF#{NewlgO0uX4?7H3EE>OEiFv*o(T&kCCry5QPkAJ zRuoDL#*vt5M!&4QPc}oI1am zuA1i@(-rS^4!>Tn|C}7!V*93zpVhajw3$U>((+cd!$ed@BH2}RCXUCxn;gp2G?u%t z{>27%_l}Mwpj#dM*xK*1NXAOUGg7k(ZPr(smERipW5`5bvlshXui@3_X}+vKV4Bsj zJXWA-lCpN_l^xx}1winblV#xVbUJ;AI!q(j8g$#C9aLvq=x2DQoyvA!Kt}5};iOI8 zUiaNsEq!d8bs zQn$}=FNK$^sNzD)U2XS80*V+Z|IHIvSBGA6>^GVe%lH|fAn-}G$>KQV2R= z$gsro^YaJNhcW*ww$>FpS;kykc{Y$WsIalHu4$P_GYY(=%+riB6y<`+5S6~)+S@dF z+=BonfyZZF^MC{@`2g)`6e~NY^)ZntAD9neT|CE>wmL;SYlj6H#@=y9T1RsO!8yCo=FDwx76UZ)Gre**4blEn#}OAg@!2~vt!{}>$-fvWDM<$z1P>* zha7>Ke`*&9tWq(>gC&>*B06Uc{oMi)1< zYH_O74_JPpozzL6%Jwz%e>3r^tG8XBdW2PX5VLr#g)8#|Cbl7yx1#CIQW;0WBwSj~ zEtx*6NpB&AW$27!w+4)@c5OvtGIVMJ%WOSswTM|*(OAdEJy?Guqn4ZSG(>w9EYr8?8mOjOkt~+#H8!nL zvQ{sjyVyQ!(s3f?^qp2WkoT5=)RI%CYiw=bo9p;Y+j-VnS`=o$F(VBolkS_B4`N!f z+&NU!w5A5F(E?FD(K2oPZ6FwZlJZn}b+0LcdwAKF`Bf3KRW<{vXV$%&v zq0e>6p)`SOUL&mcreMs{kC}UJs#ohoW$jH^b-T(gS|bIkFW^B7hHEBsX1R8{2Me2s zcbYreJ79v?h!!siB`)B+_RPsFf)tLV`neH!8!68TpNt910ZR(iiX9o-TL{>aHde&B zLiTJ9ZFqw;^q%y(C63tkE_YU1(JTcs?w>y2sA1D;7=}n|VSR@f1wCMaDbHeS^qh#4 zIVXzdp%95=TfqcP9g`&~^R}Z^&BU2$wmf+2RrHxfuu@A?ACNRSTfbCl zBh$eAd(-|Wo7y_3Fs?W08KZ!9GWneeoG8FDa_M6!hA_m-Uc-$6T(zII2*9`qE`&U8 zZ6p z?M$Sx(zmm*qSXepGAce4Ps48Djr@PQxZyV}HM~;SI1o$Mb)7^WC90Sb3~ zf=+8Wn-4`Ygn52Ws5ChHGz-%XuONv1Omix0t(WWMVkj$%~>;x*_6-@n$8RVo6|YlBv{!tiIX8_hEdA~OsxPm zV`SS=7A9u0rJYP-omX4p%&a2GGLw3>^OEcuBS*gzL5ZZZiKg(}{(TlGTgy-! zLFG?Nc_+qL9eADo+88?45*X{*I0FJevzm6^0uy$3+%x8}of^7gx;`WA%GI6eSM%e0 zok6YIw#nKpgtTs+sYB%Hq?}k2z7Rm?#vz}drhK}fCF{t@iY#hku~l&n@r|jW@6W`3 zc6)hb6N$DWj`_?yptVFXaU?MVQ@V}avX&~DO(Y0i<7Sn1BVAlP!BZRv<-GXKHs z9u?QCPGHtn#&RG8`Pb|9FAG4wYXzXIGiqAJOgE&s=O&_&{ak+U(BD|>0UIq zRQ52fOEYc9%2-aotY@oPwE933+@@rV4Z6s%8n1FXogOVfqdZNvZ41Qc+UfSOYADrO zw-|D7hf0uuvJ|qNSeBExYFwF=GEu)J%$oGQX&_o{WDD<_7?sUES4}Z4m&;$0utv04 z$U?Km5To@RO7#q-Sw@ZWA&s$YE#XGgLZXR-ylQ{Dr|xN?thJ+Cv8?S_T)JE$e2W~? zeVC4rK=|$L?Jqrm@!B&%?eNGnsm+S8yl7QVNGX;j zr<(g=*vX1ui+ZeOO|wR=H*vPfW1$SyM|T&nB4MbX=ZB4Add(wbR)fRb0Cj~{z`}D8Z30Or|C{tQlSC^}N^O9p#`l_Z zHLH9t4B# zFw5K`AOgS^agglVTC?Xwy>>*I`CqQM@tt`D4FQiG^kfpJ7AY7)&qc$wv0SKjY6v-l zpOv2GB#%3x=+>|TPeG+zJFI2h&!PM3| z(xN?{n!bl!MHp98k(;ZS=k>9X0kwy0cVabsO%#~}{$Hf0(|Ubth&#VC$Hg?6nDI94 z4CSqLsz_hk6g6X-62KS>Myq1}n`S>tt_I<<_Km%AD7aV^Tl~!QbQZ0Aj}VulQtBHm zufU|+0~5INwN@}ey7hX!KJ3!gxuOY5jjByfe_EwBs+7qe>y`T>($i)|%mrB!i60r_ zw%my=sx7I*^tC*la*vlM(q3ipxZ@hxkpth_u4FqD4s1iv-zz=M4xr|LSGoS55#sJO zsMvDFldI2jbsJYe%kY^jo}v9r+*rFB=F0Q;=jc0j0!yXM_exJ&*Hgb7)NSed4TEs+* zVMovX*CFoR1z4JwtG|I3hHU>u7Vo#;e)|v;r*QJ$q^FHoX_BwKCYi12%7SCGB5bVw8$K6?5Ii8$qg`&2wGwEXqv)su%i}`pw9$sEvJ~WP|+u}i&-!sJB zEL3S(JWhU7ul&dmhX*Xrq|AE;_Qub$ZH<}!ycZtA*cNtcE1#3HTG$4Jvr|}Fp=3_K zM~J)qf6C(V2s+~s=vuy2iHrU$9a0w=V3}^$pzSwf92YEl>3Pm9luPyo{B{7KY{D^L&GE z(IS4!!sf#K9t(S=z_yNMg9Y|N-0fG*;;|_t&lNVS#cSo_X21SDEn+U-Uuj`;v9|G} z)g$eGw~mu=p&7Cg@gJ4NYb1r$$<6muXJ|2YQ|wT3)wGCt9R07huq_2o9KtdE + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/textures/piece.png b/textures/piece.png new file mode 100644 index 0000000000000000000000000000000000000000..33be584009d7296e0f816572ab36face6971469e GIT binary patch literal 9836 zcmYj%cUTiqvv)!by#)+Sx>S)4V(1_pX;DB>x`6bebV4zq8R=D8K)`@Bm8x_J5~U+u zx`0utNQZBI?|Yy7{jtfjd-lxE{ASL~nVFMdYNAU=%|Q(Uf#~%0w9SEk&T4jhd8>Pwqe`WeNo11?g*RSO(8b6ttzS ze4BViwYW%6BAEYcE4Tha>~pG9g$Y;lKr{?U>0zPQQ~8om+VyI5mZ zem!Y(fYGB#i&_@8^S`=&d~ge6k+pUZ8pQRe#rA<@wh|@@>udKEhB`f0&Hu`U_p_Vy zoAkz=cckA$06sMEtEb!KL7~U^@0TALzn{}2;vc{Hv+D^R$VOi;7Z;i0#?>^h; zo;U4zck&?gW8qiV9w|BF*fRJ~+rl<-IQvAksL6|<9I?Fh!8X_E1=Yzh zLm>8#Kj3f^9-ji{whtX(%S!Yy$gqP5AM3l;SqKE*fE4Y>v;*vg$E^Bp)tWy$ z-fv6}cDHc7(JQR*(d-kb&RGwvXRmJN^!x#Wb--=*Z@6mF(E1E#+3=ytg65V)IE+Ff zNFl!jkh!hq@ob~lY|bidn$E?i$e$rx_yrPulvb&Mx0@lrP2`NNN{)u=vc`!SBe@yQ3bm3Ap@~l57me<~A zXnlV-*j9o~Kdnv*?*|`xoLBBOi^G{zq)Ss+xy?385LR|`%H7B0dZd&vEMyv!L6gMy zII-DAra8xY5Wnr@0laoQOmd@Da_BQd5*8%aS~E5|;y6!{i(0a@ zMDDrk%?++5Er*a~DOzb4dyjgnMPb*sfN@K(6q!xRv!U`ehLJ7nA$p*eXAU_oxl6~v zymcf(Z-^(s@NYq0$v2CJ?57>;7pabRc7w_8JeT(I@!1dvi;a0GdByV^EM4&Pc_e-& zSz0gazQ9u`Bbo~@ogzlp8U0qjx{;`C6pd%w^UucNY$t!WdefQ)r=CaN06F*s)2dRy z_g;3|!B9u}``1zOv$!c*s4HCQd04GUtQbg=~oh0lxTYzy;|rDse6VSy)s@AmFopU zJn?=z?_m75y3E&_YA1;R!-l)>!WAJggf+)mh5WeNPF;4B8kQ)B&K2-i!W#3fMlVja zXsYG);j2c}R%kb0i++;E_{%C>VlC(@Jm(klH1?5xTBi%J@@*yUlZmaUAwF9N46)TZ zYsoO1u=e{2p@G)HuJ&GK|GkG1?`_`Xl}~#&6{$ttp1;b&!+_Sxa`}%nv*}0|4XF%i zjUeO8-T$7dAiv8Q!%)T4c}Y#HP5Z}OtN{7N+Fzg%GL5=obsl2^9C$|WDrJxgO1cMeZg2r?PtelJ&ct)S1ODP&@~_Te`O z4HAaDGD88AK`-!5jAX@RQ<*N-)>%Mcpw)6yg3Du^9LcvDY$Zd+S9Xtfc1#h7ESE{5 zJgt_rCRuby3D`R8IvY*(0BlU|IkB}D5My`PPw3FQXC5`@Tv(h z%!&hSHGbIQwd@jX$sDS)oMFq8(aEX>*10)+t5AdY^Y_}x$GeiE+FbbdZI)p^ZU|?g zKq9{!(=!Ak@~wPdoI@oIKclU5JJ}vxIGK|TU?})d#KMb}97@Oh=^#+vnst^AR~e0Cf3Hw;8(~dTD*Et8oBD9Ls$!p8td6U1 zSU5Sku3@m|@>OqLJ;0#7o5TreDt+1N0iSj|E-y~yjD4Coeb0AxkT#3f`*2~#m0Yxg z6wGH#E+^S*7Tp7u=}{a}9cTP2LrbbmBE!4E3-KicUoZUZ$EA5JXKW_jB2l=xxk+{5 zk=*j2wnvILqTA4kHaMtbAqmTiN0KyT176E0)@yc>nAy=))F5#qo92r4f$vKE1Dk}N z*w~%3BO}HJcaGu&pNSyA?P&bEQ>0oC*aXH3fZA>nSeAlfGmS#G`RJ93hTwJ5DDk%s z_RkL&-adEFqtK|cl^l};+(FxXT9C#`GZ3Vu_(QIHvc%~;GE=XVYp3b&Q84peWcXw> z2CeHZs1q#PK2<#wOI!|DO&THG0rb(-)Fee4LR<5qxJ!lho+cLxyg@l2O6`i44XeL`!ELptEW7Gugn z3n>#x3hU&VLQ?FWfX)%sgh+7+c^h;s$WFJ8YJDk0kB(8Yb2#PmZg8)cvvnVKaDCsg zzVjQOA&J$60$Eiv2bR;5+TxR+1Amqa;3;hRnbfAx)Z+0g=}L;rNWhHvBMA#o?dURdQ=`q7Vye5(mSG1~;z>^wT zxPExbJsAqWx(iPF6k}(WLAuSK($`LGl_cy{6h$txrwK*}csJc|&RP!{T)6nFD19Bu zB2KC@l_scfB}HL|m~egT5Ml{o)IcDz2||#V)|y?k;59rS^A|8tV_Tin+I&pzVS6}V zR^Kmmg+-ESa5J?vl0ybwCOB;${$82_R~}Bzlnaf5FR})SK!k z2e%%R(?%Up#l=KBJz}V%ub71lgLCfY!kax>0-( zUusnZgAMO@`vH!R(S9gY=AZ1hUd9PTEp z#Vnt;cg%b5{;namS3I=J-?%bTE5)HMSb~xisBZKsvG!Eu1l_)ec-AA57?ZTp)h|_w zgTz>hj2DK1H3S!y3Lx1Vav6xA6g(WFZ=E1CY$@Xk!$Z=<@ z>9_mGzgoSmv!t4i!!M4GnynTV%=bGN@65f6dkfWc4WXvR@VONboEZgErH;|p>^uJ#8wP8{$tmedbUey z@6g7LQ2aM6Vh6r(2r~48sWkdc-gXIoCbnLCpU`w) zf^8U1#{c$qZ&{Aj>Hhw{Pt@tG5~hn^io43ON`hb`nJuLp*&o68RWE0>JhiJ~2=~R; z$5O)BB2G!NfknuEP$|$s+$t_>B8W~NsyNXx!X(A5*%#Y=yu$|UY9TRxCOL7GG`Cmc zw}0&4MAe=!*g}}>dz*D%g-y#^A*o-%VJ}IJ9G4iUl`!|J0Fyu7St2#O9TsQjM#X5{ zWdcKLp>}9{V(>iJ35Pa z1q;{fo#L*#agAJ?!)O;Pv1L8muvS=%zk%?9T+?i5!r`zYQrAj^$BCgI&1*=NGpx-Y zp;nXt=UTYVs-qG7Iy_d>W#XlD_)PFlq$i9%CBM7dFDHX`*DKBw4)VZ^AAR<3iQ}zw z0&4^N%xr>-YEdlp_?k5LsKLg?n!m&TsGR)94O1kBS0Xs3F2B2wx^93_iV}ffdr{51 z=aIr{0$0-3k2eGP`&%E>?!9wsrRMDz=wWPdYIN%aY&sOwcY01AxZ`7aCt9+u)P_8e zouPV;zoxK2{m@#bn&XB+d)j*MuwR&|c5k8>p*uyjG2Rnl8@7GrcnQrTK?n^GhuZqG zBJ&rK{jv~})`{`s-hwGAa!OEBgWXhhB*C@#F`Vx1=f8&K&EQRW5Bh0EwM%e3w17`r zp2P1(8g+a^4jWH-F_2{AaF84X2Bi_7V^1b{>vc$cXeSd?kLf}EVYf3aJxfM&e>vb8 z#~R;o41=_hU}T53z>Gy^nXm2C9|}9M`;S#b%e!81GR(26bD<6 zO01WrN*r9sMuDDTgj9vWOpvF?%pq@;VM@;f9iD`@kT&0?>o0fYveC>j)=c1`wwW&U z8;jj28cp3cX6Qq@t3%kK99Qkxe;jsPs>DIn?-dwkb<>L{J>S-m3>IrZ+FU(miAO_< zMoE<6?Q4(-fo~;2cs&ITTWfA{(GN2?y1ut&esA!g9|Gn7 z7#+`-S4Smd59FmhaEkXFVCd^BLQ7@=_RrM|?AiR|gIAQU5gw1qsc_MQ_#sKS#41rO z%0Uq;@;i)l89XlcJiC8~a5$)lJ2MM3D~xVHr0({J*rUCWlF{IrdXZ5 z5V0xk4;}$nI}_#JgH1;Dav1wyve4pLzeYXv(N(qRrOx3g8}%3wgv27S^Urv*^Y@*f zGR6sNl?v;sNfn(mpCxPHWZ|pPsTf{a5x9SDNJy2g=JB;W&7N^O;zVhk$G3{VAT!g- z_{9ZY&A##Q8}Y!qDJ(S9{Ka)uRH4V7H;^91>uJU(Xk3hU>)ZqiIUGc3(=!DaVLv_A z{F2x6RwDlg@!(1dpR(@^8TfsMGPrswZKO0N;47uIyD-9lGmON z;(q++Q8-7jp&h)j7Bff=}L^qs6Kz#hpWyNC=+#- zajcv|O>))cm1`9^Ha+Tsaj8r(NAek{;&plE`F};18GCo`~(LCS8lo;lx*72 zIKzp|FhZcshxrhN-t57*TQN0w zzgyBg!#8CLtjsyk*FYKM722-U=5gnaDyH0#g9duw66@75oz;mH7M;|=m$(A^@Kxgj zp$*azwKHSeEG6~~PA8SHS|k2etj9#NBzEHkCsSqmMNgBOni|gw-6!Nl$#0I@eTJ`* zpY&#d$TN_n!ItguKFKocYPIB;8HSb;&8^J%|6ajsLu(h8t=*z@^Mx*g19y zDk1{WZ^^+&s$F}2)o(?8N2z32SG?^gz%9;cXccFGGy=8 zQs})R$!%@^K4e$QJ8<#hg`O{K13uBcRAgrRux;K%7<6>qPRyV{%e@jzb>rNiOi7Y#MV?tVIdAB!R;BeOf@u7@_ znzoo7Tebs%iw&CVy*g{RV%uZq85Q{Rtdt0A_wVeHf)&8bx@lN>d1GdO)-gTj3s*C? z(BpgR=*3HF?`X|t-UDVFaJ;ZJeI7GlRNN7&8qYY4-msR{ZOy!mSd!3Z&SZBR#RiaV ztXpLb+WsW|*bV97dvTl$eO*E6q%Mh@8pdX zWyB1DECU{UdXuHxh;z4$U?Px})p!NfPAdJ;;8GB_AZz=pnb9@|uB% zHsv}i@%2~hgxse)<7RsO7G_Xozs`>nozDl-_qd1HN8T&-ijl4^jf+r)Vr_HO{7g^x zc_f?Xy{GN$Q}VoEsBzA|J42D$X4>zy#c0EKzi4Cl6~-5W_>;Y2W#SVwt*liCgtw5z z8_xwto>PyG#$fopTO7H=P$jsllAnUwPq%Kt*Oz?lHIrT8z8hC0N~JT}WiR4euo5$R zfAZRk{TQp@w3s-}i92fNWNY^C1~vm@N-W~8$SJ%;5yD3zpJ zWcHU=tq~}X=Dg<#R7W#2wVPw06T{K;EtHH2DbLN6d!(GLQTfs2qCNej!&9V>kuJED z!FuZ{N+#AfGb?o~1*jh(gM0%$OU1EfIC0T@a28OA9i0Sn(@xZz*%8hopCnw!lwHVsr5;sR6bAeEbC5Y`uv;m1Ss< zNMXQ`918iC3o$+?`zbK-9whcgcG4N00jR1=-(xEMB$w)2?+wHlrE7>_O|3wg2ff7y z)W9CY0W0>=>~A*u&kP7lsbsB;OePf^p29XZs9y6nX{8S50?AX?5J!XXD3}%Imj@0y0wPN8EAOY&uCqT@!dnPqVJCz8W+Ov!H{&sL2^15d74g6+)j+IUcZaW;Yc}Sj#5_lF zZ)u(|2x_~{90=;aRJ%MLsu-h6NEupY=3)|hV($p_=W1(f6Z_Tw9)udtUUeGhWkvQ7 ziQ=kA5wipN_sP2($t2`m-BLiQywOWdMMe5oK#Og|Rj6*lhXx7#C3)ijB`z!24t23s zaLx;IEy{D&_T>FQ+klP*Sq33O!~XG?FSM*MxkHjCCQG$A+(GD938oL%W)N-fu(lv-f&yzL8%iZ(?Y(!_F={xID^er1MP;C@7|=1xEQ z>R;?hMiXcJJV|g-t8o2Y+K8~xys}o zNNaPT8m_oiF+N-&qq5k1>wE;O8oepk$RVcR9n!HNUZH(p@My=XG5p5x>*lENczV|I z?5sc%4aOF5XhpA^eCfOGu>Pu+NKG^dM1J}2EWlnMpwdq6NRvmy;|ZS{Q{(LI-Rp4|x z>XNWTFJotr0h}AKU2CpLZIpeLS1&7Ci+QV}sv3E`M?R`U%~n2C_&K4Q_LEy*B34s0 z|ECg$uhy`hB++03MyDx~VlrdC)B1}AoomJX{JPl4CWPtD;A*rqlL>#!4-d*deJNjP zZg&Pg`r-&#kJQm6o6_s0&UeIsv^Z`ulu5jE^;6GMnvO=^tzr$KS3d$m$nCq6^3~cu zYss!$adu@IuCOww@B?%|A`Am(8>jV#U!3Lkp4SdKye$GLTWAJ1qtTQkPPXJlJlQ2d zD3XBL?L%#4bWgMb-Qo)uad)`ncQK;o_abCEm%QdAnM5=E8aBa%L&0brT0icx)S)+RO6d3!EAoXw?Q93F zb`~wHK3rV4XKDsbEqBG%{oog=`&McHuJEgXWmkl#R|2N0^gK}6FM<_D2FZhK7#Mtv znUZm?fV2{;h3(Ykz&H zSvRv##6ER|q*jOLrWnmK%NmHnJPMS{1~Ymo>*x8RJf^*AC;a4(`{@2*#l;1w04R6k z?0n7t=H?+!&g-FtqLWjGFXQXmXa}W#PqMSK_5bhTopV5hj}PvIUZ~=g%l|8EIqGs<>`Z$h%3dzU697x} zo$=v0e--^2Y7eW9MJA}tp0NEmi+|BmO&iE;#zzd+z``?M@uRAU~ zkAYJ}%A3chxYmEFxhQAtML}f=6|3~HW>z@oqm{Dt?SEPw42;9V8QZCNddUQr`h65+XK>9i++BKT?G5-(k!3){| literal 0 HcmV?d00001