--- /dev/null
+CMakeLists.txt.user
+Pentobi.creator.user
--- /dev/null
+cmake_minimum_required(VERSION 3.0.2)
+
+project(Pentobi)
+set(PENTOBI_VERSION 12.2)
+set(PENTOBI_RELEASE_DATE 2017-01-05)
+
+cmake_policy(SET CMP0043 NEW)
+
+include(CheckIncludeFiles)
+include(GNUInstallDirs)
+
+option(PENTOBI_BUILD_TESTS "Build unit tests" OFF)
+option(PENTOBI_BUILD_GTP "Build GTP interface" OFF)
+option(PENTOBI_BUILD_GUI "Build Qt-based GUI" ON)
+option(PENTOBI_BUILD_QML "Build QtQuick-based GUI" OFF)
+option(PENTOBI_BUILD_KDE_THUMBNAILER "Build thumbnailer for KDE" OFF)
+
+if (PENTOBI_BUILD_KDE_THUMBNAILER AND NOT PENTOBI_BUILD_GUI)
+ message(FATAL_ERROR
+ "PENTOBI_BUILD_KDE_THUMBNAILER requires PENTOBI_BUILD_GUI=1")
+endif()
+
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ message(STATUS "No build type selected, default to Release")
+ set(CMAKE_BUILD_TYPE "Release")
+endif()
+set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DLIBBOARDGAME_DEBUG")
+
+if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ OR (CMAKE_CXX_COMPILER_ID MATCHES "Intel"))
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
+endif()
+if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang"))
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffast-math")
+endif()
+if(MSVC)
+ add_definitions(-D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE
+ -D_SCL_SECURE_NO_WARNINGS)
+endif()
+
+check_include_files(unistd.h HAVE_UNISTD_H)
+check_include_files(sys/times.h HAVE_SYS_TIMES_H)
+check_include_files(sys/sysctl.h HAVE_SYS_SYSCTL_H)
+
+if(NOT DEFINED LIBPENTOBI_MCTS_FLOAT_TYPE)
+ set(LIBPENTOBI_MCTS_FLOAT_TYPE float)
+endif()
+
+# Don't set the Pentobi data dirs on Windows. This is currently needed for
+# building a version of Pentobi for the NSIS installer on Windows (see
+# directory windows) such that Pentobi will look for data dirs relative to
+# the installation directory. (It breaks installing Pentobi on Windows with
+# "make install" but we don't support that on Windows anyway.)
+if(UNIX)
+ if(NOT DEFINED PENTOBI_BOOKS_DIR)
+ set(PENTOBI_BOOKS_DIR "${CMAKE_INSTALL_FULL_DATADIR}/pentobi/books")
+ endif()
+ if(NOT DEFINED PENTOBI_HELP_DIR)
+ set(PENTOBI_HELP_DIR "${CMAKE_INSTALL_FULL_DATAROOTDIR}/help")
+ endif()
+ if(NOT DEFINED PENTOBI_TRANSLATIONS)
+ set(PENTOBI_TRANSLATIONS
+ "${CMAKE_INSTALL_FULL_DATADIR}/pentobi/translations")
+ endif()
+endif(UNIX)
+
+configure_file(config.h.in config.h)
+add_definitions(-DHAVE_CONFIG_H)
+include_directories(${CMAKE_CURRENT_BINARY_DIR})
+
+include_directories(${CMAKE_SOURCE_DIR}/src)
+
+if(PENTOBI_BUILD_TESTS)
+ enable_testing()
+endif()
+
+find_package(Threads)
+
+if(PENTOBI_BUILD_GUI)
+ find_package(Qt5Concurrent 5.2 REQUIRED)
+ find_package(Qt5Widgets 5.2 REQUIRED)
+ find_package(Qt5LinguistTools 5.2 REQUIRED)
+ find_package(Qt5Svg 5.2 REQUIRED)
+endif()
+if(PENTOBI_BUILD_QML)
+ # Qt 5.3 is good enough for building but the QML files require Qt 5.6 to run
+ find_package(Qt5Concurrent 5.3 REQUIRED)
+ find_package(Qt5Qml 5.3 REQUIRED)
+ find_package(Qt5Gui 5.3 REQUIRED)
+ find_package(Qt5Svg 5.3 REQUIRED)
+endif()
+
+if(UNIX)
+ add_custom_target(dist
+ COMMAND git archive --prefix=pentobi-${PENTOBI_VERSION}/ HEAD
+ | xz -e > ${CMAKE_BINARY_DIR}/pentobi-${PENTOBI_VERSION}.tar.xz
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
+endif()
+
+add_subdirectory(doc)
+add_subdirectory(src)
+add_subdirectory(data)
+if(WIN32 AND PENTOBI_BUILD_GUI)
+ add_subdirectory(windows)
+endif()
+
--- /dev/null
+Copyright (C) 2011-2017 Markus Enzenberger <enz@users.sourceforge.net>
+
+Pentobi 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 3 of the License, or (at your option)
+any later version.
+
+Pentobi 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.
+
+A copy of the GNU General Public License version 3 is appended below.
+
+Trademark disclaimer: The trademark Blokus and other trademarks referred
+to are property of their respective trademark holders. The trademark
+holders are not affiliated with the author of the program Pentobi.
+
+--------------------------------------------------------------------------
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. 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
+them 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 prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. 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.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey 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;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ 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.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+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.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ 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
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 3 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, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program 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, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU 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 Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null
+This file explains how to compile and install Pentobi from the sources.
+
+
+== Requirements ==
+
+Pentobi requires the Qt libraries (>=5.2). The C++ compiler needs to support
+certain C++11 features (only features that are already implemented by
+GCC 4.9 and MSVC 2015). The build system uses CMake (>=3.0.2).
+
+Ubuntu 16.04 provides suitable versions of the required tools and libraries in
+its package repository. They can be installed with the shell command:
+
+ sudo apt-get install \
+ g++ make cmake qttools5-dev qttools5-dev-tools libqt5svg5-dev
+
+
+== Building ==
+
+Pentobi can be compiled from the source directory with the shell commands:
+
+ cmake -DCMAKE_BUILD_TYPE=Release .
+ make
+
+
+=== Building the KDE thumbnailer plugin ===
+
+A thumbnailer plugin for KDE can be built by using the cmake option
+-DPENTOBI_BUILD_KDE_THUMBNAILER=1. In this case, the KDE development files
+need to be installed (packages kio-dev and extra-cmake-modules on
+Ubuntu 16.04). Note that on Ubuntu 16.04, the plugin will not be found if
+the default installation prefix /usr/local is used. You need to add
+QT_PLUGIN_PATH=/usr/local/lib/plugins to /etc/environment. After that, you
+can enable previews for Blokus game file in the Dolphin file manager in
+"Configure Dolphin/General/Previews".
+
+
+== Installing ==
+
+On Linux, Pentobi can be installed after compilation with the shell command:
+
+ sudo make install
+
+After installation, the system-wide databases should be updated to
+make Pentobi appear in the desktop menu and register it as handler for Blokus
+files (*.blksgf). On Ubuntu 16.04 with install prefix /usr/local, this can be
+done by running:
+
+ sudo update-mime-database /usr/local/share/mime
+ sudo update-desktop-database /usr/local/share/applications
+
+
+== Building the Android version ==
+
+For building the Android app, there is a QtCreator project file in
+src/pentobi_qml/Pentobi.pro. It requires Qt 5.6. Before compilation, the
+binary translation files need to be generated by using File/Release in
+Qt Linguist for all TS files in src/pentobi_qml/qml/translations.
+
+For testing purposes, the GUI that is used for Android can also be built as a
+desktop application by running CMake with -DPENTOBI_BUILD_QML=1.
--- /dev/null
+Version 12.2 (05 Jan 2017)
+==========================
+
+General:
+
+* Added patterns for Nexos and Callisto SGF files to MIME type
+ specification for detecting them independent of the file ending.
+
+Desktop version:
+
+* Game info properties were not removed from file if the corresponding
+ text in the game info dialog was deleted.
+* New Game/Save As was not enabled if no move had been played but game
+ was modified by editing the comment in the root node or the game info.
+* Fixed a race condition that could cause a crash when updating the
+ analysis window while a game analysis was running.
+* Game analysis progress dialog was not closed if analysis was canceled.
+
+Android version:
+
+* Toolbuttons were too small on very high DPI devices.
+* Open/Save did not show error message on failure.
+
+
+Version 12.1 (30 Nov 2016)
+==========================
+
+General:
+
+* Loading a file with a setup position in Nexos did not always work correctly
+ or could cause a crash.
+* SGF files for two-player Callisto did not use B/W properties as documented
+ but 1/2 as in multi-player variants. Files written by Pentobi 12.0 can still
+ be read and will be converted if saved again.
+
+Desktop version:
+
+* Compilation on Windows is no longer tested or supported.
+* Keep Only Position and Keep Only Subtree did not work correctly in Nexos and
+ in multi-player Callisto.
+* Delete All Variations did not mark the file as modified.
+* Missing semicolon in desktop entry file (bug #12).
+* Fixed ambiguous shortcut overload.
+* Saving a file will now remember the directory and use it as a default for
+ file dialogs.
+
+
+Version 12.0 (10 Apr 2016)
+==========================
+
+General:
+
+* New game variant Callisto.
+* Thinking time of level 7 (the highest level supported on Android) was
+ increased in most game variants to better match the CPU speed of
+ typical mobile hardware.
+* Starting points are no longer shown after color played its first piece.
+
+Desktop version:
+
+* The compilation now requires at least Qt 5.2.
+* High-DPI scaling is now automatically used if compiled with Qt 5.6.
+* Setting Move Marking to Last now only marks the last move even if the
+ computer played several moves in a row.
+
+Bug fixes Desktop version:
+
+* Icon for undo did not have a high-DPI version.
+* Option --verbose was broken on Windows.
+
+Android version:
+
+* The compilation now requires Qt 5.6.
+* Support for game variant Nexos.
+* New menu items Edit/Delete All Variations, Edit/Next Color,
+ View/Animate Pieces, Help/About.
+* Actions with buttons in action bar are no longer shown in menu.
+* Forward/backward buttons now support autorepeat.
+
+Bug fixes Android version:
+
+* Fixed crash that could occur when switching game variants while a
+ piece was selected.
+* Level set for game variant Classic3 was ignored, instead the level set
+ for Classic was used.
+* Move generation was not properly aborted if some Edit menu items were
+ selected while the computer was thinking.
+
+
+Version 11.0 (29 Dec 2015)
+==========================
+
+General:
+
+* Slightly increased playing strength, mainly in Trigon.
+* The compilation requires now at least Qt 5.1 and GCC 4.9 or MSVC 2015.
+* The score display now shows stars at scores that contain bonuses.
+
+Desktop version:
+
+* New game variant Nexos (2 or 4 players).
+* If a piece is removed from the board in setup mode, it will now
+ become the selected piece.
+* The command line option --memory was replaced by --maxlevel, which
+ reduces the needed memory and removes higher levels from the menu.
+* The memory requirements are now 1 GB minimum, 4 GB recommended for
+ playing level 9.
+* Added an application metadata file on Linux according to the AppStream
+ specification from freedesktop.org. Added a 64x64 app icon but no
+ longer an xpm icon (Debian AppStream Guidelines).
+
+Bug fixes desktop version:
+
+* Message dialog about discarding unsaved current game was not shown if
+ a file was loaded by clicking on a game in the rating dialog.
+* Last move marking did not work anymore after after interrupting a
+ computer move generation and then using Undo Move.
+* Autosaving unfinished games did not work if game was finished
+ first but then made unfinished again with Undo Move.
+* Selecting pieces in setup mode did no longer work if no legal moves
+ were left, even if setup mode is also intended to be used for
+ setting up illegal positions (e.g. for Blokus art).
+
+Android version:
+
+* Initial support for loading/saving, variations and game tree navigation.
+* The piece area now has enough room for all pieces of one color. It also
+ removes rows that become empty and orders the colors such that the color
+ to play is always on top.
+* Action buttons and menu items are now only shown if the action is
+ enabled in the current position.
+
+
+Version 10.1 (15 Oct 2015)
+==========================
+
+Desktop version:
+
+* New toolbar button for Undo Move.
+* Annotations are now also appended to the move number in the status line.
+* Don't show move number in status line if no moves have been played.
+* Show an error message instead of the crash dialog if the startup
+ fails due to low memory.
+* The Windows installer is now built with Qt 5 and dynamic libraries.
+
+Android version:
+
+* New action bar button for Undo Move.
+* Reduced memory requirements. A meaningful error message is now shown
+ if the startup fails due to low memory.
+* Workaround for a bug that made the back button no longer exit the app
+ after the computer color dialog was shown (QTBUG-48456).
+* Faster startup.
+* Changed snapping behavior of the piece area to make it easier to flick
+ vertically between colors with multiple movements on small screens.
+
+
+Version 10.0 (01 Jul 2015)
+==========================
+
+* Increased playing strength and more opening variety in Trigon.
+* The Backward10/Forward10 toolbar buttons were replaced by autorepeat
+ functionality of the Backward/Forward buttons.
+* The last move is now by default marked with a dot instead of a number.
+* The compilation now requires at least GCC 4.8 and CMake 3.0.2.
+* On Linux, the manual is now installed in $PREFIX/share/help according
+ to the freedesktop.org help specification.
+* The KDE thumbnailer plugin can now be compiled with KDE Frameworks 5.
+* Better support for high resolution displays if compiled with Qt 5.1
+ or newer and environment variable QT_DEVICE_PIXEL_RATIO is used.
+* The Pentobi help browser now uses a larger font on Windows
+* Regional language subvariants en_GB, en_CA are no longer supported.
+
+Bug fixes:
+
+* Fixed a build failure when generating the PNG icons from the SVG sources
+ if the path contained non-ASCII characters.
+* Fixed failure to open a file given as a command line argument to pentobi
+ (including the case when Pentobi is used as a handler for blksgf files
+ in file browsers) if the path contained non-ASCII characters.
+* Changed the file dialog filter for "All files" from *.* to * such that
+ really all files are shown even if they have no file ending.
+ Added an "All files" filter to the Export/ASCII Art file dialog.
+* Remembering the playing level separately for each game variant did not
+ work if the game variant was implicitly changed by opening a file.
+* "View/Move Numbers/Last" did not behave correctly after all colors were
+ enabled in the Computer Colors dialog while a move generation was running.
+* Fixed build failure with MSVC if MinGW was not also installed (because
+ windres.exe was used)
+
+
+Version 9.0 (10 Dec 2014)
+=========================
+
+* Newly supported game variant Classic for 3 players, in which the
+ players take turns playing the fourth color.
+* Increased playing strength, mainly in game variant Trigon.
+* There are now 9 levels and the playing strength increases more evenly
+ with the level. Ratings in rated games are still comparable to previous
+ versions of Pentobi apart from Trigon at lower levels because Trigon
+ starts now with a higher playing strength at level 1.
+* The computer is now better at playing moves that maximize the score
+ as long as they do not lead into riskier positions.
+* The computer now remembers the playing level separately for each game
+ variant and restores it when the game variant is changed.
+* Player ratings now change faster if less than 30 rated games have been
+ played, and slower afterwards.
+* The mouse wheel can no longer be used for game navigation because it
+ was too easy to trigger accidentally while playing a game. This also
+ fixes the bug that the game navigation with the mouse wheel was not
+ disabled in rated games and the game could not be continued after that
+ because the play button is disabled in rated games.
+* It is no longer possible to select and play a piece while the computer
+ is thinking, the thinking must be aborted first with Computer/Stop.
+* Bugfix: program crashed if computer colors dialog was opened and closed
+ with OK while computer was thinking.
+* Experimental support for Android. The Android version supports only a
+ subset of the features of the desktop version and only playing levels
+ 1 to 7. There are still known issues with the user interface due to
+ bugs in Qt for Android. The Android version is currently only available
+ as an APK file for devices with an ARMv7 CPU from the download section
+ of http://pentobi.sourceforge.net
+
+
+Version 8.2 (05 Sep 2014)
+=========================
+
+* Fixed remaining link errors on some platforms (Debian bug #759852)
+
+
+Version 8.1 (31 Aug 2014)
+=========================
+
+* Fixed link error on some platforms if Pentobi is compiled with
+ PENTOBI_BUILD_TESTS (Debian bug #759852)
+* Slightly improved some icons and use icons from theme for more menu items
+
+
+Version 8.0 (02 Mar 2014)
+=========================
+
+* Increased playing strength, especially in game variant Trigon.
+* Improved performance on multi-core CPUs: Previously, the move
+ generation was faster on multi-core CPUs but there was a small drop
+ in playing strength compared to the same playing level on a
+ single-core CPU. This effect has been reduced.
+* New toolbar button for starting a rated game.
+* The interface is now more locked down during rated games, for example
+ it is no longer possible to change the computer colors or take back a
+ move during a rated game.
+* The menu item "Computer Colors" was moved from the Game to the
+ Computer menu.
+* The source code no longer compiles with MSVC 2012 but requires
+ MSVC 2013 because a larger subset of C++11 features is used.
+* The source code distribution now uses xz instead of gzip for
+ compression.
+* The PNG versions of the icons are no longer included in the source
+ code but generated at build time from the SVG icons by a small
+ Qt-based helper program. This adds a build time dependency on QtSvg.
+* A XPM icon is now installed to share/pixmaps.
+* The configure option USE_BOOST_THREAD is no longer supported.
+ For building with MinGW, a version of MinGW with support for
+ std::thread is now required (e.g. from mingwbuilds.sf.net).
+
+
+Version 7.2 (30 Jan 2014)
+=========================
+
+* Hyphens used as minus signs in manpage (bug #9)
+* Added keywords section to desktop entry to silence lintian
+ warning (bug #10)
+* Fixed a compilation error with GCC 4.8.2 on PowerPC (and other
+ big-endian systems)
+* Fixed wrong arguments to update-mime-database/update-desktop-database
+ when running "make post-install"
+* Improved a blurry menu item icon
+* Fixed a compilation warning about a missing translation
+* Reduced the sizes of the generated and installed translation files.
+* Fixed a compilation error on 64-bit Linux with X32 ABI
+* Fixed a compilation error with Cygwin
+
+
+Version 7.1 (13 Aug 2013)
+=========================
+
+* Fixed the version string. The released file pentobi-7.0.tar.gz was
+ erroneously built from git version c5247c56 just before the version
+ tagged with v7.0 and contained the version string 6.UNKNOWN
+* The color played by the human in rated games is now randomly assigned
+* The mouse wheel is now disabled while the computer is thinking
+
+
+Version 7.0 (25 Jun 2013)
+=========================
+
+* Support for compilation with version 5 of the Qt libraries (see INSTALL
+ for details)
+* Slightly increased playing strength at higher levels (mainly in game
+ variant Duo)
+* The default settings in game variants with more than two players are now
+ that the human plays the first color and the computer all other colors
+* Fixed a crash that could occur if the window was put in fullscreen mode
+ by a method of the window manager (e.g. title bar menu on KDE) and then
+ returned to normal mode by a different method (e.g. pressing Escape)
+
+
+Version 6.0 (4 Mar 2013)
+========================
+
+* Increased playing strength at higher levels. The search algorithm used
+ for move generation is now parallelized and can take advantage of
+ multi-core CPUs (up to 4 cores). There is a new playing level 8, which
+ has a 2 GHz dual-core CPU or faster as the recommended system requirement.
+* New menu item Toolbar Text to configure the toolbar button appearance
+ independent of the system settings
+* More SGF game info properties (event, round, time) were added to the
+ game info dialog
+* The source code now requires at least GCC 4.7 (because a larger subset
+ of C++11 features is used)
+* The CMake module GNUInstallDirs is now used for setting the installation
+ directories on Unix. Note that the defaults for bindir and datadir are
+ now CMAKE_INSTALL_PREFIX/bin and CMAKE_INSTALL_PREFIX/share instead of
+ CMAKE_INSTALL_PREFIX/games and CMAKE_INSTALL_PREFIX/share/games.
+ They can be changed by setting CMAKE_INSTALL_BINDIR and
+ CMAKE_INSTALL_DATADIR (bug #7)
+* The source code no longer depends on the Boost libraries. However, it
+ is still possible to use Boost.Thread instead of std::thread by
+ configuring with USE_BOOST_THREAD=ON (e.g. needed on MinGW GCC 4.7,
+ which has no functional implementation of std::thread)
+* Thumbnailer registration for blksgf files is no longer supported for
+ Gnome 2
+
+
+Version 5.0 (10 Dec 2012)
+=========================
+
+* Small increase in overall playing strength at higher levels in all game
+ variants (especially Trigon)
+* The computer now knows about the possibility of rotational-symmetric tied
+ games in game variant Trigon Two-Player (like it already knew in the
+ variants Duo and Junior) and will prevent the second player from enforcing
+ such a tie
+* If the move generation takes longer than 10 seconds, the maximum remaining
+ time is now shown in the status bar
+* Removed less frequently used buttons (Open, Save) from the tool bar
+* Re-organized menu bar
+* The menu bar and tool bar are no longer shown in fullscreen mode
+* Avoided some window flickering at startup
+
+
+Version 4.3 (2 Nov 2012)
+========================
+
+* Setting the computer color for Red with the computer colors dialog did
+ not work for game variant Trigon Three-Player
+* Disable Undo menu item when it is not applicable
+* Fixed an assertion at end of move generation in Trigon Three-Player if
+ Pentobi was compiled in debug mode
+
+
+Version 4.2 (7 Oct 2012)
+========================
+
+* Fixed crash when opening game info dialog in game variants Classic
+ Two-Player or Trigon Two-Player
+
+
+Version 4.1 (5 Oct 2012)
+========================
+
+* Result of rated game was counted wrongly in four-color/two-player game
+ variants if the first player had a higher score than the second player
+ but the first color a lower score than the second color.
+* Fixed potential crash if Undo, Truncate or Truncate Children is selected
+ while the computer is thinking.
+* Automatic continuing of computer play did not work in some cases if the
+ computer was thinking while the Computer Color dialog was used.
+
+
+Version 4.0 (4 Oct 2012)
+========================
+
+* New menu item "Beginning of Branch"
+* The rating dialog now also shows the best previous rating and has
+ a button to reset the rating
+* A thumbnail plugin for KDE can be built by using the CMake option
+ -DPENTOBI_BUILD_KDE_THUMBNAILER=ON
+* Replaced the icons with less colorful ones. All icons are now licensed
+ under the GPLv3+ and include SVG sources. No icons from the Tango icon
+ set are used anymore.
+
+
+Version 3.1 (2 Aug 2012)
+========================
+
+* Fixed a bug in version 3.0 in the replacement of obsolete move properties
+ in old files that corrupted files in game variants with 3 or 4 colors.
+
+
+Version 3.0 (1 Aug 2012)
+========================
+
+* New functionality to compute a player rating for the user by playing
+ rated games against the computer
+* Different options for speed of game analysis
+* New menu item "Play Single Move" to make the computer play a move
+ without changing the colors played by the computer
+* The mouse wheel can now be used to navigate in the current variation
+ if no piece is selected
+* Files written by older versions of Pentobi that use a deprecated format
+ for move properties are now automatically converted to the current format
+ on write
+
+
+Version 2.1 (1 Jul 2012)
+========================
+
+* Bugfix: File was erroneously marked as modified if a multiline comment
+ was shown and the platform that was used to create the file had
+ Windows-style end of line convention and the platform on which the file
+ was shown had Unix-style.
+* Fixed the corruption of non-ASCII characters in game files on some
+ platforms.
+* Fixed a case where the program froze instead of showing an error on
+ certain syntax errors in the SGF file.
+* Fixed duplicate menu shortcut in German translation
+* Fixed too high floating point tolerance in unit tests.
+
+
+Version 2.0 (22 May 2012)
+=========================
+
+* No more popup messages if a color has no more moves;
+ instead, score points of this color are underlined
+ (feature request #3431031)
+* Newly supported game variant Junior
+* Improved playing strength. Number of levels increased to 7.
+ Level 7 is about the same speed as the old level 6 but stronger.
+* New game analysis function that shows a graph with the estimated
+ value of each position in a game (menu item "Computer/Analyze Game")
+* Support for setup properties in blksgf files (note that files
+ with setup properties cannot be read by older versions of
+ Pentobi). A new setup mode can be used to create files that start
+ with a setup position including positions that cannot occur in
+ real games (e.g. for puzzles or Blokus art)
+* New menu items for editing the game tree: "Delete All Variations",
+ "Keep Only Position", "Keep Only Subtree", "Move Variation Up/Down",
+ "Truncate Children"
+* Variations are now displayed by appending a letter to the move number
+ instead of underlining
+* Added a toolbar button for fast selection of the computer colors
+ without having to use the window menu.
+* User manual is no longer compiled into the resources of the
+ executable but installed in the installation data directory
+* Open a console for stderr output on Windows if Pentobi is
+ invoked with option --verbose
+* New option --memory to make Pentobi run on systems with low
+ memory at the cost of reduced playing strength.
+* Use standard icons from theme
+
+
+Version 1.2 (17 Apr 2012)
+=========================
+
+* Bugfix: program sometimes hung or crashed when generating a
+ move in early game Trigon positions especially when there
+ were no legal moves with any of the large pieces
+* Bugfix: file modified marker was not set on certain changes
+ (Make Main Variation, comment changed)
+* Bugfix: game info dialog showed wrong player labels in Trigon
+ and Trigon Three-Player * Minor other bugfixes in the code
+* Reverted the change that used the SVG icon for setting the
+ window icon because it created an unwanted dependency on the
+ Qt SVG plugin.
+* Made Save menu item and tool button active if game is modified
+ even if no file name is associated with the current game
+* Made the code compile without warnings with GCC -Wunused
+* Made "make post-install" continue even if some commands fail.
+
+
+Version 1.1 (10 Mar 2012)
+=========================
+
+* File is now immediately visible in Recent Files menu after
+ saving under a new name.
+* Fixed several cases where the program crashed instead of showing
+ an error message if the opened file was invalid. The error
+ message now also has a Show Details button to show the reason
+ why the file could not be loaded.
+* Fixed a bug that distorted the position values reported with
+ --verbose if a subtree from a previous search was reused
+* Fixed exception in tools/twogtp/analyze.py if option -r was used
+* Minor fixes in computer player engine
+* Added explaining label to computer color dialog because window
+ title is not visible in all L&F's
+* Accept pass moves (empty value) in files. Although the current
+ Blokus SGF documentation does not specify if they should be
+ allowed, they might be used in the future and are used in files
+ written by early (unreleased) versions of Pentobi
+* Extended the file format documentation by a hint how to put
+ blksgf files on web servers
+* Smaller icons for piece manipulation buttons
+* Fixed computation of the font bounding box in the score display
+* Set option -std=c++0x in CMakeLists.txt if compiler is CLang
+* Removed duplicate pentobi.png in directories data and src/pentobi;
+ The file pentobi.svg was moved from data to src/pentobi and is
+ now used for setting the window icon of Pentobi
+
+
+Version 1.0 (1 Jan 2012)
+========================
+
+* Support for game variant Trigon Three-Player
+* Change directory for autosave file to use AppData
+ (on Windows) or XDG_DATA_HOME (on other systems)
+* Changed Back to Main Variation to go to the last move
+ in the main variation that had a variation, not to the
+ last position in the main variation
+* Changed variation string in status bar to contain
+ information about the move numbers at the branching points
+* Fixed small rendering errors
+* New menu item Find Next Comment
+* Added chapters about the main window and menu items to
+ the user manual
+* Fix bug: computer color dialog did not set colors correctly
+ in game variant Trigon
+* Show error message instead of crashing if the SGF file
+ contains invalid move properties
+* Lowered the required version of the Boost libraries in
+ CMakeLists.txt from 1.45 to 1.40 such that Pentobi can
+ be compiled on Debian 6.0.
+ Note: some versions of Boost cause compilation errors if
+ used with certain versions of GCC and option -std=c++0x
+ (e.g. the combinations GCC 4.4/Boost 1.40 in Ubuntu 10.04
+ and GCC 4.4/Boost 1.42 in Debian 6.0 work but the combination
+ GCC 4.5/Boost 1.42 in Ubuntu 11.04 causes errors).
+* Changed installation directories according to Filesystem
+ Hierarchy Standard (/usr/bin to /usr/games, /usr/share to
+ /usr/share/games)
+* New CMake option PENTOBI_REGISTER_GNOME2_THUMBNAILER for
+ disabling the installation of files for registering the Pentobi
+ thumbnailer on Gnome 2
+* Install man pages for pentobi and pentobi-thumbnailer on Unix
+ systems
+
+
+Version 0.3 (2 Dec 2011)
+========================
+
+* Support for the game variants Trigon and Trigon Two-Player
+* Fixed saving/opening files if file name contained non-ASCII characters and
+ the system used an encoding other than Latin1
+* The score numbers now show the total player and color scores instead of
+ on-board and bonus points separately (feature request #3431039)
+* New menu item "Edit/Select Next Color" that allows to enter moves independent
+ of the color to play on the board (feature request #3441299)
+* Slightly changed file format to use single-valued move properties as used in
+ other games supported by SGF. Files written by Pentobi 0.2 can still be read.
+
+
+Version 0.2 (17 Oct 2011)
+=========================
+
+* German translation
+* Display sum score for both player colors in game variant Classic Two-Player
+* Slightly changed file format to conform to the proposed version 5 of SGF that
+ requires digits for move properties in multi-player games. Files written by
+ Pentobi 0.1 can still be read.
+* Support for move annotation symbols
+* Store and edit additional game information (player names, date)
+* New menu items Ten Moves Backward/Forward, Go to Move, Undo Move
+* Underline move numbers if there are alternative variations
+* Show move number, total number of moves and current variation in status bar
+* Faster play in higher levels, especially of opening moves
+* Make thumbnailer for Blokus files work under Gnome 3
+* Fix broken compilation with GCC 4.6.1 (bug #3420555)
+
+
+Version 0.1 (15 Jul 2011)
+=========================
+
+Initial release.
--- /dev/null
+Pentobi is a computer opponent for the board game Blokus.
+
+Copyright (C) 2011-2016 Markus Enzenberger <enz@users.sourceforge.net>
+
+See the file COPYING for license information.
+See the file INSTALL for instructions about how to build and install the
+program from the sources.
+See the file NEWS for release notes.
+The homepage of Pentobi is at http://pentobi.sourceforge.net/
--- /dev/null
+/* Define to 1 if you have the <unistd.h> header file. */
+#cmakedefine01 HAVE_UNISTD_H
+
+/* Define to 1 if you have the <sys/times.h> header file. */
+#cmakedefine01 HAVE_SYS_TIMES_H
+
+/* Define to 1 if you have the <sys/sysctl.h> header file. */
+#cmakedefine01 HAVE_SYS_SYSCTL_H
+
+/* Version number of package */
+#define VERSION "@PENTOBI_VERSION@"
+
+/* Define if the MCTS search does not need to support multi-threading.
+ This makes the search slightly faster on single-threaded systems. */
+#cmakedefine LIBBOARDGAME_MCTS_SINGLE_THREAD
+
+/* Floating type for Monte-Carlo tree search values (float|double) */
+#define LIBPENTOBI_MCTS_FLOAT_TYPE @LIBPENTOBI_MCTS_FLOAT_TYPE@
+
+/* Build for systems with low memory and slow CPU. */
+#cmakedefine01 PENTOBI_LOW_RESOURCES
+
+/* Directory containing opening books. */
+#cmakedefine PENTOBI_BOOKS_DIR "@PENTOBI_BOOKS_DIR@"
+
+/** Location of the Pentobi user manual. */
+#cmakedefine PENTOBI_HELP_DIR "@PENTOBI_HELP_DIR@"
+
+/** Location of the translations directory. */
+#cmakedefine PENTOBI_TRANSLATIONS "@PENTOBI_TRANSLATIONS@"
--- /dev/null
+if(PENTOBI_BUILD_GUI AND NOT WIN32)
+
+add_custom_target(
+ pentobi-64.png ALL
+ COMMAND convert ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi-64.svg pentobi-64.png
+ DEPENDS ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi-64.svg
+ )
+add_custom_target(
+ application-x-blokus-sgf.png ALL
+ COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf.svg application-x-blokus-sgf.png
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf.svg
+ )
+add_custom_target(
+ application-x-blokus-sgf-16.png ALL
+ COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf-16.svg application-x-blokus-sgf-16.png
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/application-x-blokus-sgf-16.svg
+ )
+
+configure_file(pentobi.desktop.in pentobi.desktop @ONLY)
+configure_file(pentobi.thumbnailer.in pentobi.thumbnailer @ONLY)
+configure_file(pentobi.appdata.xml.in pentobi.appdata.xml @ONLY)
+install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps)
+install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi-16.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps
+ RENAME pentobi.png)
+install(FILES ${CMAKE_BINARY_DIR}/src/pentobi/icons/pentobi-32.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/apps
+ RENAME pentobi.png)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi-64.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/apps
+ RENAME pentobi.png)
+install(FILES ${CMAKE_SOURCE_DIR}/src/pentobi/icons/pentobi.svg
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/mimetypes)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf-16.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/mimetypes
+ RENAME application-x-blokus-sgf.png)
+install(FILES application-x-blokus-sgf.svg
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/mimetypes)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.desktop
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.thumbnailer
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/thumbnailers)
+install(FILES pentobi-mime.xml
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mime/packages)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.appdata.xml
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/appdata)
+
+endif(PENTOBI_BUILD_GUI AND NOT WIN32)
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="1.5" y="0.5" width="13" height="15" stroke-linejoin="round" ry="1.15" stroke="#7c7f79" fill="#fff"/>
+ <rect x="3" y="2" width="10" height="12" fill="#eeeeec"/>
+ <rect x="4" y="4" width="4" height="4" fill="#C00"/>
+ <path d="m4 8v-4h4l-1 1h-2v2z" fill="#f14646"/>
+ <rect x="8" y="4" width="4" height="4" fill="#73d216"/>
+ <path d="m8 8v-4h4l-1 1h-2v2z" fill="#b0eb76"/>
+ <rect x="4" y="8" width="4" height="4" fill="#edd400"/>
+ <path d="m4 12v-4h4l-1 1h-2v2z" fill="#fdee76"/>
+ <rect x="8" y="8" width="4" height="4" fill="#3465a4"/>
+ <path d="m8 12v-4h4l-1 1h-2v2z" fill="#6f9dce"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="4.5" y="2.5" width="23" height="27" stroke-linejoin="round" ry="1.15" stroke="#555753" fill="#fff"/>
+ <rect x="6" y="4" width="20" height="24" fill="#eeeeec"/>
+ <g id="h">
+ <rect x="16" y="8" width="4" fill="#73d216" height="4"/>
+ <path d="m16 12v-4h4l-1 1h-2v2z" fill="#b0eb76"/>
+ </g>
+ <use x="4" xlink:href="#h"/>
+ <use y="4" x="4" xlink:href="#h"/>
+ <use y="8" x="4" xlink:href="#h"/>
+ <g id="g">
+ <rect x="12" y="16" height="4" width="4" fill="#3465a4"/>
+ <path d="m12 20v-4h4l-1 1h-2v2z" fill="#6f9dce"/>
+ </g>
+ <use x="4" xlink:href="#g"/>
+ <use y="4" x="4" xlink:href="#g"/>
+ <use y="4" x="8" xlink:href="#g"/>
+ <g id="f">
+ <rect x="8" y="12" width="4" fill="#edd400" height="4"/>
+ <path d="m8 16v-4h4l-1 1h-2v2z" fill="#fdee76"/>
+ </g>
+ <use y="4" xlink:href="#f"/>
+ <use y="8" xlink:href="#f"/>
+ <use y="8" x="4" xlink:href="#f"/>
+ <g id="e">
+ <rect x="8" y="8" width="4" fill="#C00" height="4"/>
+ <path d="m8 12v-4h4l-1 1h-2v2z" fill="#f14646"/>
+ </g>
+ <use x="4" xlink:href="#e"/>
+ <use y="4" x="4" xlink:href="#e"/>
+ <use y="4" x="8" xlink:href="#e"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="6.5" y="3.5" width="47" height="55" stroke-linejoin="round" ry="1.15" stroke="#555753" fill="#fff"/>
+ <rect x="8" y="5" width="44" height="52" fill="#eeeeec"/>
+ <g id="a">
+ <rect height="7" width="7" x="30" y="18" fill="#73d216"/>
+ <path d="m30 25h7v-7l-1 1v 5h-5z" fill="#4e9a06"/>
+ <path d="m30 25v-7h7l-1 1h-5v5z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#a" x="7"/>
+ <use xlink:href="#a" x="7" y="7"/>
+ <use xlink:href="#a" x="7" y="14"/>
+ <g id="b">
+ <rect height="7" width="7" x="23" y="32" fill="#3465a4"/>
+ <path d="m23 39h7v-7l-1 1v 5h-5z" fill="#204a87"/>
+ <path d="m23 39v-7h7l-1 1h-5v5z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#b" x="7"/>
+ <use xlink:href="#b" x="7" y="7"/>
+ <use xlink:href="#b" x="14" y="7"/>
+ <g id="c">
+ <rect height="7" width="7" x="16" y="25" fill="#edd400"/>
+ <path d="m16 32h7v-7l-1 1v 5h-5z" fill="#c4a000"/>
+ <path d="m16 32v-7h7l-1 1h-5v5z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#c" y="7"/>
+ <use xlink:href="#c" y="14"/>
+ <use xlink:href="#c" x="7" y="14"/>
+ <g id="d">
+ <rect height="7" width="7" x="16" y="18" fill="#C00"/>
+ <path d="m16 25h7v-7l-1 1v 5h-5z" fill="#a40000"/>
+ <path d="m16 25v-7h7l-1 1h-5v5z" fill="#ef2929"/>
+ </g>
+ <use xlink:href="#d" x="7"/>
+ <use xlink:href="#d" x="7" y="7"/>
+ <use xlink:href="#d" x="14" y="7"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="6.5" y="3.5" width="35" height="41" stroke-linejoin="round" ry="1.15" stroke="#555753" fill="#fff"/>
+ <rect x="8" y="5" width="32" height="38" fill="#eeeeec"/>
+ <g id="a">
+ <rect height="6" width="6" x="24" y="12" fill="#73d216"/>
+ <path d="m24 18h6v-6l-1 1v4h-4z" fill="#4e9a06"/>
+ <path d="m24 18v-6h6l-1 1h-4v4z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#a" x="6"/>
+ <use xlink:href="#a" x="6" y="6"/>
+ <use xlink:href="#a" x="6" y="12"/>
+ <g id="b">
+ <rect height="6" width="6" x="18" y="24" fill="#3465a4"/>
+ <path d="m18 30h6v-6l-1 1v4h-4z" fill="#204a87"/>
+ <path d="m18 30v-6h6l-1 1h-4v4z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#b" x="6"/>
+ <use xlink:href="#b" x="6" y="6"/>
+ <use xlink:href="#b" x="12" y="6"/>
+ <g id="c">
+ <rect height="6" width="6" x="12" y="18" fill="#edd400"/>
+ <path d="m12 24h6v-6l-1 1v4h-4z" fill="#c4a000"/>
+ <path d="m12 24v-6h6l-1 1h-4v4z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#c" y="6"/>
+ <use xlink:href="#c" y="12"/>
+ <use xlink:href="#c" x="6" y="12"/>
+ <g id="d">
+ <rect height="6" width="6" x="12" y="12" fill="#C00"/>
+ <path d="m12 18h6v-6l-1 1v4h-4z" fill="#a40000"/>
+ <path d="m12 18v-6h6l-1 1h-4v4z" fill="#ef2929"/>
+ </g>
+ <use xlink:href="#d" x="6"/>
+ <use xlink:href="#d" x="6" y="6"/>
+ <use xlink:href="#d" x="12" y="6"/>
+</svg>
--- /dev/null
+<?xml version="1.0"?>
+<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
+<mime-type type="application/x-blokus-sgf">
+<comment>Blokus game</comment>
+<comment xml:lang="de">Blokus-Partie</comment>
+<magic priority="60">
+<match type="string" offset="0:256" value="GM[Blokus]"/>
+<match type="string" offset="0:256" value="GM[Blokus Duo]"/>
+<match type="string" offset="0:256" value="GM[Blokus Junior]"/>
+<match type="string" offset="0:256" value="GM[Blokus Trigon]"/>
+<match type="string" offset="0:256" value="GM[Blokus Trigon Three-Player]"/>
+<match type="string" offset="0:256" value="GM[Blokus Trigon Two-Player]"/>
+<match type="string" offset="0:256" value="GM[Blokus Two-Player]"/>
+<match type="string" offset="0:256" value="GM[Callisto]"/>
+<match type="string" offset="0:256" value="GM[Callisto Three-Player]"/>
+<match type="string" offset="0:256" value="GM[Callisto Two-Player]"/>
+<match type="string" offset="0:256" value="GM[Nexos]"/>
+<match type="string" offset="0:256" value="GM[Nexos Two-Player]"/>
+</magic>
+<sub-class-of type="text/plain"/>
+<glob pattern="*.blksgf"/>
+</mime-type>
+</mime-info>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<component type="desktop">
+ <id>pentobi.desktop</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-3.0+</project_license>
+ <name>Pentobi</name>
+ <name xml:lang="de">Pentobi</name>
+ <summary>Computer opponent for the board game Blokus</summary>
+ <summary xml:lang="de">Computer-Gegner für das Brettspiel Blokus</summary>
+
+ <description>
+ <p>Pentobi is a computer opponent for the board game Blokus. It has a
+ strong Blokus engine with 9 different playing levels. The supported game
+ variants are: Classic, Duo, Trigon, Junior, Nexos, Callisto.</p>
+ <p xml:lang="de">Pentobi ist ein Computer-Gegner für das Brettspiel Blokus.
+ Es hat eine spielstarke Blokus-Engine mit 9 verschiedenen Spielstufen.
+ Die unterstützten Spielvarianten sind : Klassisch, Duo, Trigon, Junior,
+ Nexos, Callisto.</p>
+
+ <p>Players can determine their strength by playing rated games against the
+ computer and use a game analysis function. Games can be saved in Smart Game
+ Format with comments and move variations.</p>
+ <p xml:lang="de">Spieler können ihre Spielstärke ermitteln, indem sie
+ gewertete Spiele gegen den Computer spielen, und eine Spielanalysefunktion
+ benutzen. Spiele können im Smart-Game-Format gespeichert werden mit
+ Kommentaren und Zugvarianten.</p>
+
+ <p>System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz dual-core or
+ faster CPU recommended for playing level 9).</p>
+ <p xml:lang="de">Systemminima: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2 GHz
+ Dual-Core- oder schnellere CPU empfohlen für Spielstufe 9).</p>
+
+ <p>Trademark disclaimer: The trademark Blokus and other trademarks referred
+ to are property of their respective trademark holders. The trademark
+ holders are not affiliated with the author of the program Pentobi.</p>
+ <p xml:lang="de">Hinweis zu Markennamen: Der Markenname Blokus und andere
+ erwähnte Marken sind Eigentum ihrer jeweiligen Markeninhaber. Die
+ Markeninhaber stehen in keiner Verbindung mit dem Autor des Programms
+ Pentobi.</p>
+ </description>
+
+ <screenshots>
+ <screenshot type="default">
+ <image width="1248" height="702">
+ http://pentobi.sourceforge.net/pentobi-classic.png</image>
+ <caption>Game variant Classic</caption>
+ <caption xml:lang="de">Spielvariante Klassisch</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ http://pentobi.sourceforge.net/pentobi-duo.png</image>
+ <caption>Game variant Duo</caption>
+ <caption xml:lang="de">Spielvariante Duo</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ http://pentobi.sourceforge.net/pentobi-trigon.png</image>
+ <caption>Game variant Trigon</caption>
+ <caption xml:lang="de">Spielvariante Trigon</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ http://pentobi.sourceforge.net/pentobi-nexos.png</image>
+ <caption>Game variant Nexos</caption>
+ <caption xml:lang="de">Spielvariante Nexos</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ http://pentobi.sourceforge.net/pentobi-callisto.png</image>
+ <caption>Game variant Callisto</caption>
+ <caption xml:lang="de">Spielvariante Callisto</caption>
+ </screenshot>
+ </screenshots>
+
+ <url type="homepage">http://pentobi.sourceforge.net/</url>
+ <url type="bugtracker">https://sourceforge.net/p/pentobi/bugs/</url>
+ <url type="donation">https://sourceforge.net/p/pentobi/donate/</url>
+ <developer_name>Markus Enzenberger</developer_name>
+ <update_contact>enz@users.sourceforge.net</update_contact>
+
+ <provides>
+ <binary>pentobi</binary>
+ </provides>
+ <mimetypes>
+ <mimetype>application/x-blokus-sgf</mimetype>
+ </mimetypes>
+
+ <releases>
+ <release version="@PENTOBI_VERSION@" date="@PENTOBI_RELEASE_DATE@"/>
+ </releases>
+</component>
--- /dev/null
+[Desktop Entry]
+Name=Pentobi
+GenericName=Computer Opponent for Blokus
+GenericName[de]=Computer-Gegner für Blokus
+Comment=Computer opponent for the board game Blokus
+Comment[de]=Computer-Gegner für das Brettspiel Blokus
+Keywords=Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;
+Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi %f
+Icon=pentobi
+Type=Application
+Categories=Game;BoardGame;
+MimeType=application/x-blokus-sgf;
--- /dev/null
+[Thumbnailer Entry]
+Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi-thumbnailer --size %s %i %o
+MimeType=application/x-blokus-sgf;
--- /dev/null
+add_subdirectory(man)
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<title>Pentobi SGF Files</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<h1>Pentobi SGF Files</h1>
+<div style="font-size:small">Author: Markus Enzenberger<br>
+Last modified: 2016-11-27</div>
+<p>This document describes the file format for <a href=
+"http://en.wikipedia.org/wiki/Blokus">Blokus</a> game records as used by the
+program <a href="http://pentobi.sourceforge.net">Pentobi</a>. The most recent
+version of this document can be found in the source code distribution of
+Pentobi in the folder pentobi/doc/blksgf.</p>
+<h2>Introduction</h2>
+<p>The file format is a derivative of the <a href=
+"http://www.red-bean.com/sgf/">Smart Game Format</a> (SGF). The current SGF
+version 4 does not define standard properties for Blokus. Therefore, a number
+of game-specific properties and value types had to be defined. The definitions
+follow the recommendations of SGF 4 and the proposals for multi-player games
+from the <a href="http://www.red-bean.com/sgf/ff5/ff5.htm">discussions</a>
+about the future SGF version 5.</p>
+<h2>File Extension and MIME Type</h2>
+<p>The file extension <tt>.blksgf</tt> and the <a href=
+"http://en.wikipedia.org/wiki/Internet_media_type">MIME type</a>
+<tt>application/x-blokus-sgf</tt> are used for Blokus SGF files.</p>
+<p style="font-size:small"><b>Note</b><br>
+Since this is a non-standard MIME type, links to Blokus SGF files on web
+servers will not automatically open the file with Pentobi even if Pentobi is
+installed locally and registered as a handler for Blokus SGF files. To make
+this work, you can put a file named <a href=
+"http://en.wikipedia.org/wiki/.htaccess">.htaccess</a> on the web server in the
+same directory that contains the .blksgf files or in one of its parent
+directories. This file needs to contain the line:</p>
+<blockquote style="font-size:small">AddType application/x-blokus-sgf
+blksgf</blockquote>
+<h2>Character Set</h2>
+<p>Although not specific to Blokus, it is recommended to use <a href=
+"http://en.wikipedia.org/wiki/UTF-8">UTF-8</a> as the character set. Pentobi
+always writes files in UTF-8 and indicates that with the <tt>CA</tt> property.
+Pentobi can read SGF files encoded in UTF-8 or ISO-8859-1 (Latin1). Other
+character sets are currently not supported. As specified by the SGF standard,
+ISO-8859-1 is assumed for files without <tt>CA</tt> property.</p>
+<h2>Game Property</h2>
+<p>Since there is no number for Blokus defined in SGF 4, a string instead of a
+number is used as the value for the <tt>GM</tt> property. Currently, the
+following strings are used: Blokus, Blokus Two-Player, Blokus Three-Player,
+Blokus Duo, Blokus Trigon, Blokus Trigon Two-Player, Blokus Trigon
+Three-Player, Blokus Junior, Nexos, Nexos Two-Player, Callisto, Callisto
+Two-Player, Callisto Three-Player.</p>
+The strings are case-sensitive, words are separated by exactly one space and no
+additional whitespace at the beginning or end of the string is allowed.
+<h2>Color and Player Properties</h2>
+<p>In game variants with two players and two colors, <tt>B</tt> denotes the
+first player or color, <tt>W</tt> the second player or color. In game variants
+with three or four players and one color per player, <tt>1</tt>, <tt>2</tt>,
+<tt>3</tt>, <tt>4</tt> denote the first, second, third, and fourth player or
+color. In game variants with two players and four colors, <tt>B</tt> denotes
+the first player, <tt>W</tt> the second player, and <tt>1</tt>, <tt>2</tt>,
+<tt>3</tt>, <tt>4</tt> denote the first, second, third, and fourth color. This
+applies to move properties and properties related to a player or a color.</p>
+<p>Example 1: in the game variant Blokus Two-Player <tt>PW</tt> is the name of
+the first player, and <tt>1</tt> is a move of the first color.</p>
+<p>Example 2: in the game variant Blokus Two-Player, one could either use the
+<tt>BL</tt>, <tt>WL</tt> properties to indicate the time left for a player, if
+the game is played with a time limit for each player, or one could use the
+<tt>1L</tt>, <tt>2L</tt>, <tt>3L</tt>, <tt>4L</tt> properties to indicate the
+time left for a color, if the game is played with a time limit for each color.
+(This is only an example how the properties should be interpreted. Pentobi
+currently has no support for game clocks.)</p>
+<p style="font-size:small"><b>Note</b><br>
+Pentobi versions before 0.2 used the properties <tt>BLUE</tt>, <tt>YELLOW</tt>,
+<tt>RED</tt>, <tt>GREEN</tt> in the four-color game variants, which did not
+reflect the current state of discussion for SGF 5. Pentobi 12.0 erroneously
+used multi-player properties for two-player Callisto. Current versions of
+Pentobi can still read games written by older versions and will convert old
+properties.</p>
+<h2>Coordinate System</h2>
+<p>Fields on the board (called points in SGF) are identified by a
+case-insensitive string with a letter for the column followed by a number for
+the row. The letters start with 'a', the numbers start with '1'. The lower left
+corner of the board is 'a1'. The strings are not allowed to contain
+whitespaces. Note that, unlike the common convention in the game of Go, the
+letter 'i' is used.</p>
+<p>If there are more than 26 columns, the columns continue with 'aa', 'ab',
+..., 'ba', 'bb', ... More than 26 columns are presently required for Trigon and
+could also be required for future game variants on rectangular boards larger
+than 26×26.</p>
+<p>For Trigon, hexagonal boards are mapped to rectangular coordinates as in the
+following example of a hexagon with edge size 3:</p>
+<pre>
+ 6 / \ / \ / \ / \
+ 5 / \ / \ / \ / \ / \
+ 4 / \ / \ / \ / \ / \ / \
+ 3 \ / \ / \ / \ / \ / \ /
+ 2 \ / \ / \ / \ / \ /
+ 1 \ / \ / \ / \ /
+ a b c d e f g h i j k
+</pre>
+<p>In Nexos, the 13×13 line grid is mapped to a 25×25 coordinate system, in
+which rows with horizontal line segments and intersections alternate with rows
+with vertical line segments and holes:</p>
+<pre>
+ 6 | | |
+ 5 + - + - + -
+ 4 | | |
+ 3 + - + - + -
+ 2 | | |
+ 1 + - + - + -
+ a b c d e f
+</pre>
+<h2>Move Properties</h2>
+<p>The value of a move property is a string with the coordinates of the played
+piece on the board separated by commas. No whitespace characters are allowed
+before, after, or in-between the coordinates.</p>
+<p>Pentobi currently does not require a certain order of the coordinates of a
+move. However, move properties should be written with an ordered list of
+coordinates (using the order a1, b1, …, a2, b2, …) such that each move has a
+unique string representation.</p>
+<p>Example: <tt>B[f9,e10,f10,g10,f11]</tt></p>
+<p>In Nexos, moves contain only the coordinates of line segments occupied by
+the piece, no coordinates of junctions.</p>
+<p style="font-size:small"><b>Note</b><br>
+Old versions of Pentobi (before version 0.3) used to represent moves by a list
+of points, which did not follow the convention used by other games in SGF to
+use single-value properties for moves. Current versions of Pentobi can still
+read games containing the old move property values but they are deprecated and
+should no longer be used.</p>
+<h2>Setup Properties</h2>
+<p>The setup properties <tt>AB</tt>, <tt>AW</tt>, <tt>A1</tt>, <tt>A2</tt>,
+<tt>A3</tt>, <tt>A4</tt> can be used to place several pieces simultaneously on
+the board. The setup property <tt>AE</tt> can be used to remove pieces from the
+board. All these properties can have multiple values, each value represents a
+piece by its coordinates as in the move properties. The <tt>PL</tt> can be used
+to set the color to play in a setup position.</p>
+<p>Example:<br>
+<tt>AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]<br>
+AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10]<br>
+PL[B]</tt></p>
+<p style="font-size:small"><b>Note</b><br>
+Older versions of Pentobi (before version 2.0) did not support setup
+properties, you need a newer version of Pentobi to read such files. Currently,
+Pentobi is able to read files with setup properties in any node, but can create
+only files with setup in the root node.</p>
+</body>
+</html>
--- /dev/null
+doxygen_sqlite3.db
+html/
--- /dev/null
+# Doxyfile 1.8.9.1
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+DOXYFILE_ENCODING = UTF-8
+PROJECT_NAME = Pentobi
+PROJECT_NUMBER =
+PROJECT_BRIEF =
+PROJECT_LOGO =
+OUTPUT_DIRECTORY =
+CREATE_SUBDIRS = NO
+ALLOW_UNICODE_NAMES = NO
+OUTPUT_LANGUAGE = English
+BRIEF_MEMBER_DESC = YES
+REPEAT_BRIEF = YES
+ABBREVIATE_BRIEF =
+ALWAYS_DETAILED_SEC = NO
+INLINE_INHERITED_MEMB = NO
+FULL_PATH_NAMES = YES
+STRIP_FROM_PATH =
+STRIP_FROM_INC_PATH =
+SHORT_NAMES = NO
+JAVADOC_AUTOBRIEF = YES
+QT_AUTOBRIEF = NO
+MULTILINE_CPP_IS_BRIEF = NO
+INHERIT_DOCS = YES
+SEPARATE_MEMBER_PAGES = NO
+TAB_SIZE = 8
+ALIASES =
+TCL_SUBST =
+OPTIMIZE_OUTPUT_FOR_C = NO
+OPTIMIZE_OUTPUT_JAVA = NO
+OPTIMIZE_FOR_FORTRAN = NO
+OPTIMIZE_OUTPUT_VHDL = NO
+EXTENSION_MAPPING =
+MARKDOWN_SUPPORT = YES
+AUTOLINK_SUPPORT = YES
+BUILTIN_STL_SUPPORT = NO
+CPP_CLI_SUPPORT = NO
+SIP_SUPPORT = NO
+IDL_PROPERTY_SUPPORT = YES
+DISTRIBUTE_GROUP_DOC = NO
+SUBGROUPING = YES
+INLINE_GROUPED_CLASSES = NO
+INLINE_SIMPLE_STRUCTS = NO
+TYPEDEF_HIDES_STRUCT = NO
+LOOKUP_CACHE_SIZE = 0
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+EXTRACT_ALL = YES
+EXTRACT_PRIVATE = NO
+EXTRACT_PACKAGE = NO
+EXTRACT_STATIC = NO
+EXTRACT_LOCAL_CLASSES = YES
+EXTRACT_LOCAL_METHODS = NO
+EXTRACT_ANON_NSPACES = NO
+HIDE_UNDOC_MEMBERS = NO
+HIDE_UNDOC_CLASSES = NO
+HIDE_FRIEND_COMPOUNDS = NO
+HIDE_IN_BODY_DOCS = NO
+INTERNAL_DOCS = NO
+CASE_SENSE_NAMES = YES
+HIDE_SCOPE_NAMES = NO
+HIDE_COMPOUND_REFERENCE= NO
+SHOW_INCLUDE_FILES = YES
+SHOW_GROUPED_MEMB_INC = NO
+FORCE_LOCAL_INCLUDES = NO
+INLINE_INFO = YES
+SORT_MEMBER_DOCS = YES
+SORT_BRIEF_DOCS = NO
+SORT_MEMBERS_CTORS_1ST = NO
+SORT_GROUP_NAMES = NO
+SORT_BY_SCOPE_NAME = NO
+STRICT_PROTO_MATCHING = NO
+GENERATE_TODOLIST = YES
+GENERATE_TESTLIST = YES
+GENERATE_BUGLIST = YES
+GENERATE_DEPRECATEDLIST= YES
+ENABLED_SECTIONS =
+MAX_INITIALIZER_LINES = 30
+SHOW_USED_FILES = YES
+SHOW_FILES = YES
+SHOW_NAMESPACES = YES
+FILE_VERSION_FILTER =
+LAYOUT_FILE =
+CITE_BIB_FILES =
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+QUIET = NO
+WARNINGS = YES
+WARN_IF_UNDOCUMENTED = YES
+WARN_IF_DOC_ERROR = YES
+WARN_NO_PARAMDOC = NO
+WARN_FORMAT = "$file:$line: $text"
+WARN_LOGFILE =
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+INPUT = ../../src
+INPUT_ENCODING = UTF-8
+FILE_PATTERNS = *.h \
+ *.cpp
+RECURSIVE = YES
+EXCLUDE =
+EXCLUDE_SYMLINKS = NO
+EXCLUDE_PATTERNS =
+EXCLUDE_SYMBOLS =
+EXAMPLE_PATH =
+EXAMPLE_PATTERNS =
+EXAMPLE_RECURSIVE = NO
+IMAGE_PATH =
+INPUT_FILTER =
+FILTER_PATTERNS =
+FILTER_SOURCE_FILES = NO
+FILTER_SOURCE_PATTERNS =
+USE_MDFILE_AS_MAINPAGE =
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+SOURCE_BROWSER = NO
+INLINE_SOURCES = NO
+STRIP_CODE_COMMENTS = YES
+REFERENCED_BY_RELATION = NO
+REFERENCES_RELATION = NO
+REFERENCES_LINK_SOURCE = YES
+SOURCE_TOOLTIPS = YES
+USE_HTAGS = NO
+VERBATIM_HEADERS = YES
+CLANG_ASSISTED_PARSING = NO
+CLANG_OPTIONS =
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+ALPHABETICAL_INDEX = NO
+COLS_IN_ALPHA_INDEX = 5
+IGNORE_PREFIX =
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+GENERATE_HTML = YES
+HTML_OUTPUT = html
+HTML_FILE_EXTENSION = .html
+HTML_HEADER =
+HTML_FOOTER = footer.html
+HTML_STYLESHEET =
+HTML_EXTRA_STYLESHEET =
+HTML_EXTRA_FILES =
+HTML_COLORSTYLE_HUE = 220
+HTML_COLORSTYLE_SAT = 100
+HTML_COLORSTYLE_GAMMA = 80
+HTML_TIMESTAMP = YES
+HTML_DYNAMIC_SECTIONS = NO
+HTML_INDEX_NUM_ENTRIES = 100
+GENERATE_DOCSET = NO
+DOCSET_FEEDNAME = "Doxygen generated docs"
+DOCSET_BUNDLE_ID = org.doxygen.Project
+DOCSET_PUBLISHER_ID = org.doxygen.Publisher
+DOCSET_PUBLISHER_NAME = Publisher
+GENERATE_HTMLHELP = NO
+CHM_FILE =
+HHC_LOCATION =
+GENERATE_CHI = NO
+CHM_INDEX_ENCODING =
+BINARY_TOC = NO
+TOC_EXPAND = NO
+GENERATE_QHP = NO
+QCH_FILE =
+QHP_NAMESPACE = org.doxygen.Project
+QHP_VIRTUAL_FOLDER = doc
+QHP_CUST_FILTER_NAME =
+QHP_CUST_FILTER_ATTRS =
+QHP_SECT_FILTER_ATTRS =
+QHG_LOCATION =
+GENERATE_ECLIPSEHELP = NO
+ECLIPSE_DOC_ID = org.doxygen.Project
+DISABLE_INDEX = NO
+GENERATE_TREEVIEW = NO
+ENUM_VALUES_PER_LINE = 4
+TREEVIEW_WIDTH = 250
+EXT_LINKS_IN_WINDOW = NO
+FORMULA_FONTSIZE = 10
+FORMULA_TRANSPARENT = YES
+USE_MATHJAX = NO
+MATHJAX_FORMAT = HTML-CSS
+MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest
+MATHJAX_EXTENSIONS =
+MATHJAX_CODEFILE =
+SEARCHENGINE = NO
+SERVER_BASED_SEARCH = NO
+EXTERNAL_SEARCH = NO
+SEARCHENGINE_URL =
+SEARCHDATA_FILE = searchdata.xml
+EXTERNAL_SEARCH_ID =
+EXTRA_SEARCH_MAPPINGS =
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+GENERATE_LATEX = NO
+LATEX_OUTPUT = latex
+LATEX_CMD_NAME = latex
+MAKEINDEX_CMD_NAME = makeindex
+COMPACT_LATEX = NO
+PAPER_TYPE = a4wide
+EXTRA_PACKAGES =
+LATEX_HEADER =
+LATEX_FOOTER =
+LATEX_EXTRA_STYLESHEET =
+LATEX_EXTRA_FILES =
+PDF_HYPERLINKS = YES
+USE_PDFLATEX = YES
+LATEX_BATCHMODE = NO
+LATEX_HIDE_INDICES = NO
+LATEX_SOURCE_CODE = NO
+LATEX_BIB_STYLE = plain
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+GENERATE_RTF = NO
+RTF_OUTPUT = rtf
+COMPACT_RTF = NO
+RTF_HYPERLINKS = NO
+RTF_STYLESHEET_FILE =
+RTF_EXTENSIONS_FILE =
+RTF_SOURCE_CODE = NO
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+GENERATE_MAN = NO
+MAN_OUTPUT = man
+MAN_EXTENSION = .3
+MAN_SUBDIR =
+MAN_LINKS = NO
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+GENERATE_XML = NO
+XML_OUTPUT = xml
+XML_PROGRAMLISTING = YES
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+GENERATE_DOCBOOK = NO
+DOCBOOK_OUTPUT = docbook
+DOCBOOK_PROGRAMLISTING = NO
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+GENERATE_AUTOGEN_DEF = NO
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+GENERATE_PERLMOD = NO
+PERLMOD_LATEX = NO
+PERLMOD_PRETTY = YES
+PERLMOD_MAKEVAR_PREFIX =
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+ENABLE_PREPROCESSING = YES
+MACRO_EXPANSION = NO
+EXPAND_ONLY_PREDEF = NO
+SEARCH_INCLUDES = YES
+INCLUDE_PATH =
+INCLUDE_FILE_PATTERNS =
+PREDEFINED = HAVE_BOOST_THREAD
+EXPAND_AS_DEFINED =
+SKIP_FUNCTION_MACROS = YES
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+TAGFILES =
+GENERATE_TAGFILE =
+ALLEXTERNALS = NO
+EXTERNAL_GROUPS = YES
+EXTERNAL_PAGES = YES
+PERL_PATH = /usr/bin/perl
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+CLASS_DIAGRAMS = YES
+MSCGEN_PATH =
+DIA_PATH =
+HIDE_UNDOC_RELATIONS = YES
+HAVE_DOT = NO
+DOT_NUM_THREADS = 0
+DOT_FONTNAME = Helvetica
+DOT_FONTSIZE = 10
+DOT_FONTPATH =
+CLASS_GRAPH = YES
+COLLABORATION_GRAPH = YES
+GROUP_GRAPHS = YES
+UML_LOOK = NO
+UML_LIMIT_NUM_FIELDS = 10
+TEMPLATE_RELATIONS = NO
+INCLUDE_GRAPH = YES
+INCLUDED_BY_GRAPH = YES
+CALL_GRAPH = NO
+CALLER_GRAPH = NO
+GRAPHICAL_HIERARCHY = YES
+DIRECTORY_GRAPH = YES
+DOT_IMAGE_FORMAT = png
+INTERACTIVE_SVG = NO
+DOT_PATH =
+DOTFILE_DIRS =
+MSCFILE_DIRS =
+DIAFILE_DIRS =
+PLANTUML_JAR_PATH =
+PLANTUML_INCLUDE_PATH =
+DOT_GRAPH_MAX_NODES = 50
+MAX_DOT_GRAPH_DEPTH = 0
+DOT_TRANSPARENT = YES
+DOT_MULTI_TARGETS = NO
+GENERATE_LEGEND = YES
+DOT_CLEANUP = YES
--- /dev/null
+<p>
+<hr>
+<div style="text-align:right;">
+$date <a href="http://www.doxygen.org/">Doxygen</a> $doxygenversion
+</div>
+</p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<title>Pentobi GTP Interface</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<h1>Pentobi GTP Interface</h1>
+<div style="font-size:small">Author: Markus Enzenberger</div>
+<p>This document describes the text-based interface to the engine of the Blokus
+program <a href="http://pentobi.sourceforge.net">Pentobi</a>. The interface is
+an adaption of the <a href="http://www.lysator.liu.se/~gunnar/gtp/">Go Text
+Protocol</a> (GTP) and allows controller programs to use the engine in an
+automated way without the GUI. The most recent version of this document can be
+found in the source code distribution of Pentobi in the folder
+pentobi/doc/gtp.</p>
+<h2>Go Text Protocol</h2>
+<p>The Go Text Protocol is a simple text-based protocol. The engine reads
+single-line commands from its standard input stream and writes multi-line
+responses to its standard output stream. The first character of a response is a
+status character: <tt>=</tt> for success, <tt>?</tt> for failure, followed by
+the actual response. The response ends with two consecutive newline characters.
+See the <a href=
+"http://www.lysator.liu.se/~gunnar/gtp/gtp2-spec-draft2/gtp2-spec.html">GTP
+specification</a> for details.</p>
+<h2>Controllers</h2>
+<p>To use the engine from a controller program, the controller typically
+creates a child process by running <tt>pentobi-gtp</tt> and then sends commands
+and receives responses through the input/output streams of the child process.
+How this is done, depends on the platform (programming language and/or
+operating system). In Java, for example, a child process can be created with
+<tt>java.lang.Runtime.exec()</tt>.</p>
+<p>Note that the input/output streams of child processes are often fully
+buffered. You should explicitly flush the output stream after sending a
+command. Another caveat is that <tt>pentobi-gtp</tt> writes debugging
+information to its standard error stream. On some platforms the standard error
+stream of the child process is automatically connected to the standard error
+stream of its parent process. If not (this happens for example in Java), the
+controller needs to read everything from the standard error stream of the child
+process. This can be done for example by running a separate thread in the
+parent process that has a simple read loop, which writes everything that it
+reads to its own standard error stream or discards it. Otherwise the child
+process will block as soon as the buffer for its standard error stream is full.
+Alternatively, you can disable debugging output of <tt>pentobi-gtp</tt> with
+the command line option <tt>--quiet</tt>, but it is generally better to assume
+that a GTP engine writes text to standard error.</p>
+<p>An example for a controller written in C++ for Linux is included in Pentobi
+since version 9.0 in <tt>src/twogtp</tt>. The controller starts two GTP engines
+and plays a number of Blokus games between them. Older versions of Pentobi
+included a Python script with a similar functionality in
+<tt>tools/twogtp/twogtp.py</tt>.</p>
+<h2>Building</h2>
+<p>Since the GTP engine is a developer tool, building it is not enabled by
+default. To enable it, run <tt>cmake</tt> with the option
+<tt>-DPENTOBI_BUILD_GTP=ON</tt>. After building, there will be an executable in
+the build directory named <tt>src/pentobi_gtp/pentobi-gtp</tt>. The GTP engine
+requires only standard C++ and has no dependency on other libraries like Qt,
+which is needed for the GUI version of Pentobi. If you only want to build the
+GTP engine, you can disable building the GUI with
+<tt>-DPENTOBI_BUILD_GUI=OFF</tt>.</p>
+<h2>Options</h2>
+<p>The following command-line options are supported by
+<tt>pentobi-gtp</tt>:</p>
+<dl>
+<dt>--book <i>file</i></dt>
+<dd>Specify a file name for the opening book. Opening books are blksgf files
+containing trees, in which moves that Pentobi should select are marked as good
+moves with the corresponding SGF property (see the files in
+<tt>src/books</tt>). If no opening book is specified and opening books are not
+disabled, <tt>pentobi-gtp</tt> will automatically search for an opening book
+for the current game variant in the directory of the executable using the same
+file name conventions as in <tt>src/books</tt>. If no such file is found it
+will print an error message to standard error and disable the use of opening
+books.</dd>
+<dt>--config,-c <i>file</i></dt>
+<dd>Load a file with GTP commands and execute them before starting the main
+loop, which reads commands from standard input. This can be used for
+configuration files that contain GTP commands for setting parameters of the
+engine (see below).</dd>
+<dt>--color</dt>
+<dd>Use ANSI escape sequences to colorize the text output of boards (for
+example in the response to the <tt>showboard</tt> command or with the
+--showboard command line option).</dd>
+<dt>--cputime</dt>
+<dd>Use CPU time instead of wall time for time measurement. Currently, there is
+no way to make Pentobi play with time limits, the levels are defined by the
+number of simulations in the MCTS search, so this affects only the debugging
+output, which prints the time used after each search.</dd>
+<dt>--game,-g <i>variant</i></dt>
+<dd>Specify the game variant used at start-up. Valid arguments are classic,
+classic_2, duo, trigon, trigon_2, trigon_3, junior or the abbreviations c, c2,
+d, t, t2, t3, j. By default, the initial game variant is classic. The game
+variant can also be changed at run-time with a GTP command. If only a single
+game variant is used, it is slightly faster and saves memory if the engine is
+started in the right variant compared to having it start with classic and then
+changing it.</dd>
+<dt>--help,-h</dt>
+<dd>Print a list of the command-line options and exit.</dd>
+<dt>--level,-l <i>n</i></dt>
+<dd>Set the level of playing strength to n. Valid values are 1 to 9.</dd>
+<dt>--seed,-r <i>n</i></dt>
+<dd>Use <i>n</i> as the seed for the random generator. Specifying a random seed
+will make the move generation deterministic as long as the search is
+single-threaded.</dd>
+<dt>--showboard</dt>
+<dd>Automatically write a text representation of the current position to
+standard error after each command that alters the position.</dd>
+<dt>--nobook</dt>
+<dd>Disable the use of opening books.</dd>
+<dt>--noresign</dt>
+<dd>Disable resignation. If resignation is desabled, the <tt>genmove</tt>
+command will never respond with <tt>resign</tt>. Resignation can speed up the
+playing of test games if only the win/loss information is wanted.</dd>
+<dt>--quiet,-q</dt>
+<dd>Do not print any debugging messages, errors or warnings to standard
+error.</dd>
+<dt>--threads <i>n</i></dt>
+<dd>Use <i>n</i> threads during the search. Note that the default is 1, unlike
+in the GUI version of Pentobi, which sets the default according to the number
+of hardware threads (CPUs, cores or virtual cores) available on the current
+system. The reason is that, for example, using 2 threads makes the search twice
+as fast but may lose a bit of playing strength compared to the single-threaded
+search. Therefore, if the GTP engine is used to play many test games with
+twogtp.py (which supports playing games in parallel), it is better to play the
+games with single-threaded search in parallel than with multi-threaded search
+sequentially. Using a large number of threads (e.g. more than 4 on game
+variants with a small board or more than 8 on large boards) is untested and
+might reduce the playing strength significantly compared to the single-threaded
+search.</dd>
+<dt>--version,-v</dt>
+<dd>Print the version of Pentobi and exit.</dd>
+</dl>
+<h2>Commands</h2>
+<h3>Standard Commands</h3>
+<p>The following GTP commands have the same or an equivalent meaning as
+specified by the GTP standard. Colors or players in arguments or responses are
+represented as in the property IDs of blksgf files (<tt>B</tt>, <tt>W</tt> if
+two players or colors; <tt>1</tt>, <tt>2</tt>, <tt>3</tt>, <tt>4</tt> if more
+than two). Moves in arguments or responses are represented as in the move
+property values of blksgf files. See the specification for <a href=
+"http://pentobi.sourceforge.net/Pentobi-SGF.html">Pentobi SGF files</a> for
+details.</p>
+<dl>
+<dt>all_legal <i>color</i></dt>
+<dd>List all legal moves for a color.</dd>
+<dt>clear_board</dt>
+<dd>Clear the board and start a new game in the current game variant.</dd>
+<dt>final_score</dt>
+<dd>Get the score of a final board position. In two-player game variants, the
+format of the response is as in the result property in the SGF standard for the
+game of Go (e.g. <tt>B+2</tt> if the first player wins with two points, or
+<tt>0</tt> for a draw). In game variants with more than two players, the
+response is a list of the points for each player (e.g.
+<tt>64 69 70 40</tt>). If the current position is not a final
+position, the response is undefined.</dd>
+<dt>genmove <i>color</i></dt>
+<dd>Generate and play a move for a given color in the current position. If the
+color has no more moves, the response is <tt>pass</tt>. If resignation is not
+disabled, the response is <tt>resign</tt> if the players is very likely to
+lose. Otherwise the response is the move.</dd>
+<dt>known_command <i>command</i></dt>
+<dd>The response is <tt>true</tt> if <i>command</i> is a GTP command supported
+by the engine, <tt>false</tt> otherwise.</dd>
+<dt>list_commands</dt>
+<dd>List all supported GTP commands, one command per line.</dd>
+<dt>loadsgf <i>file</i> [<i>move_number</i>]</dt>
+<dd>Load a board position from a blksgf file with name <i>file</i>. If
+<i>move_number</i> is specified, the board position will be set to the position
+in the main variation of the file <u>before</u> the move with the given number
+was played, otherwise to the last position in the main variation.</dd>
+<dt>name</dt>
+<dd>Return the name of the GTP engine (<tt>Pentobi</tt>).</dd>
+<dt>play <i>color</i> <i>move</i></dt>
+<dd>Play a move for a given color in the current board position.</dd>
+<dt>protocol_version</dt>
+<dd>Return the version of the GTP protocol used (currently <tt>2</tt>).</dd>
+<dt>quit</dt>
+<dd>Exit the command loop and quit the engine.</dd>
+<dt>reg_genmove <i>color</i></dt>
+<dd>Like the <tt>genmove</tt> command, but only generates a move and does not
+play it on the board.</dd>
+<dt>showboard</dt>
+<dd>Return a text representation of the current board position.</dd>
+<dt>undo</dt>
+<dd>Undo the last move played.</dd>
+<dt>version</dt>
+<dd>Return the version of Pentobi.</dd>
+</dl>
+<h3>Generally Useful Extension Commands</h3>
+<dl>
+<dt>cputime</dt>
+<dd>Return the CPU time used by the engine since the start of the program.</dd>
+<dt>cputime_diff</dt>
+<dd>Return the CPU time used by the engine since the last call of cputime_diff
+or start of the program if cputime_diff has not been called yet.</dd>
+<dt>g</dt>
+<dd>Shortcut for the <tt>genmove</tt> command with the color argument set to
+the current color to play.</dd>
+<dt>get_place <i>color</i></dt>
+<dd>Get the place of a given color in the list of scores in a final position
+(e.g. in game variant Classic, 1 is the place with the highest score, 4 the one
+with the lowest, if all players have a different score). If some colors have
+the same score, they share the same place and the string <tt>shared</tt> is
+appended to the place number.</dd>
+<dt>get_value</dt>
+<dd>Get an estimated value of the board position from the view point of the
+color of the last generated move. The return value is a win/loss estimation
+between 0 (loss) and 1 (win) as produced by the last search performed by the
+engine. This command should only be used immediately after a
+<tt>reg_genmove</tt> or <tt>genmove</tt> command, otherwise the result is
+undefined. The value is not very meaningful at the lowest playing levels. Note
+that no searches are performed if the opening book is used for a move
+generation and there is currently no way to check if this was so. Therefore,
+the opening book should be disabled if the <tt>get_value</tt> command is
+used.</dd>
+<dt>p <i>move</i></dt>
+<dd>Shortcut for the <tt>play</tt> command with the color argument set to the
+current color to play.</dd>
+<dt>param [<i>key</i> <i>value</i>]</dt>
+<dd>Set or query parameters specific to the Pentobi engine that can be changed
+at run-time. If no arguments are given, the response is a list of the current
+value with one key/value pair per line, otherwise the parameter with the given
+key will be set to the given value. Generally useful parameters are:
+<blockquote>
+<dl>
+<dt>avoid_symmetric_draw 0|1</dt>
+<dd>In some game variants (Duo, Trigon_2), the second player can enforce a tie
+by answering each move by its symmetric counterpart if the first players misses
+the opportunity to break the symmetry in the center. Technically, exploiting
+this mistake by the first player is a good strategy for the second player
+because a draw is a good result considering the first-play advantage. However,
+playing symmetrically could be considered bad style, so this behavior is
+avoided (value <tt>1</tt>) by default.</dd>
+<dt>fixed_simulations <i>n</i></dt>
+<dd>Use exactly <i>n</i> MCTS simulations during a search. By default, the
+search engine uses levels, which determine how many MCTS simulations are run
+during a search, but as a function that increases with the move number (because
+the simulations become much faster at the end of the game). For some
+experiments, it can be desirable to use a fixed number of simulations for each
+move. If this number is specified, the playing level is ignored.</dd>
+<dt>use_book 0|1</dt>
+<dd>Enable or disable the opening book.</dd>
+</dl>
+</blockquote>
+The other parameters are only interesting for developers.</dd>
+<dt>param_base [<i>key</i> <i>value</i>]</dt>
+<dd>Set or query basic parameters that are not specific to the Pentobi engine.
+If no arguments are given, the response is a list of the current value with one
+key/value pair per line, otherwise the parameter with the given key will be set
+to the given value.
+<blockquote>
+<dl>
+<dt>accept_illegal 0|1</dt>
+<dd>Accept move arguments to the <tt>play</tt> command that violate the rules
+of the game. If disabled, the <tt>play</tt> command will respond with an error,
+otherwise it will perform the moves.</dd>
+<dt>resign 0|1</dt>
+<dd>Allow the engine to respond with <tt>resign</tt> to the <tt>genmove</tt>
+command.</dd>
+</dl>
+</blockquote>
+</dd>
+<dt>set_game <i>variant</i></dt>
+<dd>Set the current game variant and clear the board. The argument is the name
+of the game variant as in the game property value of blksgf files (e.g.
+<tt>Blokus Duo</tt>, see the specification for <a href=
+"http://pentobi.sourceforge.net/Pentobi-SGF.html">Pentobi SGF files</a> for
+details).</dd>
+<dt>set_random_seed <i>n</i></dt>
+<dd>Set the seed of the random generator to <i>n</i>. See the documentation for
+the command-line option --seed.</dd>
+</dl>
+<h3>Extension Commands for Developers</h3>
+The remaining commands are only interesting for developers. See Pentobi's
+source code for details.
+<h2>Example</h2>
+<p>The following GTP session queries the engine name and version, plays and
+generates a move in game variant Duo and shows the resulting board position.
+Commands are printed in bold, responses in normal text.</p>
+<pre>
+<i>$ ./pentobi-gtp --quiet</i>
+<b>name</b>
+= Pentobi
+
+<b>version</b>
+= 7.1
+
+<b>set_game Blokus Duo</b>
+=
+
+<b>play b e8,d9,e9,f9,e10</b>
+=
+
+<b>genmove w</b>
+= i4,h5,i5,j5,i6
+
+<b>showboard</b>
+=
+ A B C D E F G H I J K L M N
+14 . . . . . . . . . . . . . . 14 *Blue(X): 5
+13 . . . . . . . . . . . . . . 13 1 F L5 N P T5 U V5 W Y
+12 . . . . . . . . . . . . . . 12 Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+11 . . . . . . . . . . . . . . 11
+10 . . . . X . . . . . . . . . 10 Green(O): 5
+ 9 . . . X X X . . . . . . . . 9 1 F L5 N P T5 U V5 W Y
+ 8 . . . . X . . . . . . . . . 8 Z5 I5 O T4 Z4 L4 I4 V3 I3 2
+ 7 . . . . . . . . . . . . . . 7
+ 6 . . . . . . . .>O . . . . . 6
+ 5 . . . . . . . O O O . . . . 5
+ 4 . . . . . . . . O . . . . . 4
+ 3 . . . . . . . . . . . . . . 3
+ 2 . . . . . . . . . . . . . . 2
+ 1 . . . . . . . . . . . . . . 1
+ A B C D E F G H I J K L M N
+
+<b>quit</b>
+=
+
+</pre>
+</body>
+</html>
--- /dev/null
+configure_file(pentobi.6.in pentobi.6 @ONLY)
+configure_file(pentobi-thumbnailer.6.in pentobi-thumbnailer.6 @ONLY)
+install(FILES
+ ${CMAKE_CURRENT_BINARY_DIR}/pentobi.6
+ ${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer.6
+ DESTINATION ${CMAKE_INSTALL_MANDIR}/man6)
--- /dev/null
+.TH PENTOBI-THUMBNAILER 6 "2013-06-21" "Pentobi @PENTOBI_VERSION@" "Pentobi command reference"
+
+.SH NAME
+pentobi-thumbnailer \- thumbnailer for game records for the board game Blokus as used by the program Pentobi
+
+.SH SYNOPSIS
+.B pentobi-thumbnailer
+.RI [ options ] " input-file output-file"
+.br
+
+.SH DESCRIPTION
+
+.B pentobi-thumbnailer
+is part of the program Pentobi and intended to be used as a thumbnailer for
+the Gnome desktop environment to generate previews of game files written by
+Pentobi.
+
+The input file is a game file in Pentobi's SGF format as documented in
+doc/blksgf/Pentobi-SGF.html in the Pentobi source package.
+The output file is a thumbnail in PNG format.
+
+.SH OPTIONS
+.TP
+.B \-s, \-\-size
+The size of the thumbnail. The default is 128.
+
+.SH EXIT STATUS
+.TP
+0 if the thumbnail generation succeeds, 1 on error.
+
+.SH SEE ALSO
+.BR pentobi (6)
+
+.SH AUTHOR
+Markus Enzenberger <enz@users.sourceforge.net>
--- /dev/null
+.TH PENTOBI 6 "2015-01-04" "Pentobi @PENTOBI_VERSION@" "Pentobi command reference"
+
+.SH NAME
+pentobi \- computer opponent for the board game Blokus
+
+.SH SYNOPSIS
+.B pentobi
+.RI [ options ] " [file]"
+.br
+
+.SH DESCRIPTION
+
+.B pentobi
+is the command to invoke the program Pentobi, which is a graphical user
+interface and computer opponent to play the board game Blokus.
+
+The command can take the name of a game file to open at startup as an optional
+argument.
+The game file is expected to be in Pentobi's SGF format as documented in
+doc/blksgf/Pentobi-SGF.html in the Pentobi source package.
+
+.SH OPTIONS
+.TP
+.B \-\-maxlevel
+Set the maximum playing level. Reducing this value reduces the amount
+of memory used by the search, which can be useful to run Pentobi on systems
+that have low memory or are too slow to use the highest levels.
+By default, Pentobi currently allocates up to 2 GB (but not more than a third
+of the physical memory available on the system).
+Reducing the maximum level to 8 currently reduces this amount by a factor
+of 6 and lower maximum levels even more.
+.TP
+.B \-\-threads
+The number of threads to use in the search. By default, up to 4 threads are
+used in the search depending on the number of hardware threads supported
+by the current system.
+Using more threads will speed up the move generation but using a very high
+number of threads (e.g. more than 8) can degrade the playing strength
+in higher playing levels.
+.TP
+.B \-\-verbose
+Print internal information about the move generation and other debugging
+information to standard error.
+
+.SH SEE ALSO
+.BR pentobi-thumbnailer (6)
+
+.SH AUTHOR
+Markus Enzenberger <enz@users.sourceforge.net>
--- /dev/null
+add_subdirectory(books)
+add_subdirectory(libboardgame_sys)
+add_subdirectory(libboardgame_util)
+add_subdirectory(libboardgame_sgf)
+add_subdirectory(libboardgame_base)
+add_subdirectory(libboardgame_mcts)
+add_subdirectory(libpentobi_base)
+add_subdirectory(libpentobi_mcts)
+
+if (PENTOBI_BUILD_GTP)
+ add_subdirectory(libboardgame_gtp)
+ add_subdirectory(pentobi_gtp)
+ if(HAVE_UNISTD_H AND NOT(WIN32))
+ add_subdirectory(twogtp)
+ else()
+ message(STATUS "Not building twogtp, needs POSIX")
+ endif()
+endif()
+
+if (PENTOBI_BUILD_TESTS)
+ add_subdirectory(libboardgame_test)
+ add_subdirectory(libboardgame_test_main)
+ add_subdirectory(unittest)
+endif()
+
+if (PENTOBI_BUILD_GUI)
+ add_subdirectory(convert)
+ add_subdirectory(libpentobi_gui)
+ add_subdirectory(libpentobi_thumbnail)
+ add_subdirectory(pentobi_thumbnailer)
+ add_subdirectory(pentobi)
+ if(PENTOBI_BUILD_KDE_THUMBNAILER)
+ add_subdirectory(libpentobi_kde_thumbnailer)
+ add_subdirectory(pentobi_kde_thumbnailer)
+ endif()
+endif()
+
+if (PENTOBI_BUILD_QML)
+ add_subdirectory(pentobi_qml)
+endif()
--- /dev/null
+# Install the opening book files. If you change the destination, you need to
+# update the default for PENTOBI_BOOKS_DIR in the main CMakeLists.txt
+install(FILES
+ book_callisto.blksgf
+ book_callisto_2.blksgf
+ book_callisto_3.blksgf
+ book_classic.blksgf
+ book_classic_2.blksgf
+ book_classic_3.blksgf
+ book_duo.blksgf
+ book_junior.blksgf
+ book_nexos.blksgf
+ book_nexos_2.blksgf
+ book_trigon.blksgf
+ book_trigon_2.blksgf
+ book_trigon_3.blksgf
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/books)
--- /dev/null
+(
+;GM[Callisto]
+(
+ ;1[g11]TE[1]
+ (
+ ;2[n10]TE[1]
+ )
+ (
+ ;2[n11]TE[1]
+ )
+)
+(
+ ;1[h12]TE[1]
+ ;2[m9]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Callisto Two-Player]
+(
+ ;B[e9]TE[1]
+ (
+ ;W[l8]TE[1]
+ )
+ (
+ ;W[k7]TE[1]
+ )
+)
+(
+ ;B[f10]TE[1]
+ (
+ ;W[k7]TE[1]
+ )
+ (
+ ;W[j6]TE[1]
+ )
+ (
+ ;W[l8]
+ )
+)
+)
--- /dev/null
+(
+;GM[Callisto Three-Player]
+(
+ ;1[g11]TE[1]
+ (
+ ;2[n10]TE[1]
+ )
+ (
+ ;2[n11]TE[1]
+ )
+)
+(
+ ;1[h12]TE[1]
+ (
+ ;2[m9]TE[1]
+ )
+ (
+ ;2[l8]TE[1]
+ )
+)
+)
--- /dev/null
+(
+;GM[Blokus]
+(
+ ;1[a17,b17,a18,a19,a20]TE[1]
+ ;2[s17,t17,t18,t19,t20]TE[1]
+ ;3[t1,t2,t3,s4,t4]TE[1]
+)
+(
+ ;1[b17,a18,b18,a19,a20]TE[1]
+)
+(
+ ;1[a20,b20,c20,c19,c18]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Blokus Two-Player]
+(
+ ;1[a17,b17,a18,a19,a20]TE[1]
+ (
+ ;2[s17,t17,t18,t19,t20]TE[1]
+ (
+ ;3[t1,t2,s3,t3,s4]TE[1]
+ (
+ ;4[a1,b1,c1,d1,d2]TE[1]
+ (
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ (
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ (
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ ;4[e3,e4,f4,g4,g5]TE[1]
+ ;1[h11,g12,h12,f13,g13]TE[1]
+ ;2[o11,p11,n12,o12,o13]TE[2]
+ ;3[o8,m9,n9,o9,p9]TE[1]
+ ;4[h6,h7,i7,i8,j8]TE[1]
+ ;1[j9,k9,l9,i10,j10]TE[2]
+ ;2[k10,l10,m10,n10,m11]TE[1]
+ ;3[q10,q11,q12,p13,q13]TE[1]
+ )
+ (
+ ;3[r5,q6,r6,p7,q7]TE[1]
+ ;4[e3,f3,f4,g4,g5]TE[1]
+ ;1[g11,f12,g12,h12,f13]TE[1]
+ ;2[o11,p11,n12,o12,o13]TE[1]
+ ;3[n8,o8,n9,m10,n10]TE[1]
+ ;4[h6,h7,i7,j7,i8]TE[1]
+ ;1[k10,l10,i11,j11,k11]TE[1]
+ ;2[l12,k13,l13,m13,l14]TE[1]
+ ;3[p9,q9,q10,r10,q11]TE[1]
+ ;4[l7,k8,l8,m8,l9]TE[1]
+ )
+ )
+ (
+ ;2[p14,p15,q15,q16,r16]TE[1]
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ (
+ ;4[e3,e4,f4,g4,g5]TE[1]
+ ;1[h11,g12,h12,f13,g13]TE[1]
+ ;2[o11,p11,n12,o12,o13]TE[1]
+ ;3[o8,m9,n9,o9,p9]TE[1]
+ ;4[h6,h7,i7,i8,j8]
+ ;1[j9,k9,l9,i10,j10]TE[2]
+ ;2[k10,l10,m10,n10,m11]TE[1]
+ ;3[q10,q11,q12,p13,q13]TE[2]
+ )
+ (
+ ;4[e3,f3,f4,g4,g5]TE[1]
+ )
+ )
+ )
+ (
+ ;1[e14,d15,e15,c16,d16]TE[1]
+ )
+ )
+ (
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ (
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ ;4[d4,d5,e5,e6,f6]TE[1]
+ (
+ ;1[h11,g12,h12,f13,g13]TE[1]
+ ;2[o11,p11,n12,o12,o13]TE[2]
+ ;3[o8,m9,n9,o9,p9]TE[1]
+ ;4[h6,g7,h7,h8,i8]TE[1]
+ (
+ ;1[j9,k9,l9,i10,j10]TE[2]
+ )
+ (
+ ;1[k9,l9,i10,j10,k10]TE[2]
+ )
+ )
+ (
+ ;1[g11,h11,f12,g12,f13]TE[1]
+ ;2[o11,p11,n12,o12,o13]TE[1]
+ ;3[o8,o9,n10,o10,p10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[i10,j10,k10,l10,m10]TE[1]
+ )
+ )
+ (
+ ;3[r5,q6,r6,p7,q7]TE[1]
+ ;4[d4,d5,e5,e6,f6]TE[1]
+ ;1[g11,f12,g12,h12,f13]TE[1]
+ ;2[m11,n11,n12,o12,o13]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[k10,l10,i11,j11,k11]TE[1]
+ ;2[i12,j12,k12,l12,j13]TE[1]
+ ;3[p11,p12,q12,r12,p13]TE[1]
+ ;4[k7,l7,m7,n7,o7]TE[1]
+ )
+ )
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ )
+ (
+ ;4[a1,a2,a3,b3,c3]TE[1]
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ ;4[d4,e4,e5,e6,f6]TE[1]
+ ;1[h11,g12,h12,f13,g13]TE[1]
+ ;2[o11,p11,n12,o12,o13]TE[2]
+ ;3[o8,m9,n9,o9,p9]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ (
+ ;1[j9,k9,l9,i10,j10]TE[2]
+ ;2[k10,l10,m10,n10,m11]TE[1]
+ ;3[q10,q11,q12,p13,q13]TE[1]
+ )
+ (
+ ;1[k9,l9,i10,j10,k10]TE[2]
+ ;2[l10,m10,n10,m11]TE[1]
+ ;3[q10,q11,q12,p13,q13]TE[1]
+ )
+ )
+ )
+ (
+ ;3[t1,t2,t3,s4,t4]TE[1]
+ (
+ ;4[a1,b1,c1,d1,d2]TE[1]
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ (
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[q5,r5,q6,p7,q7]TE[1]
+ ;4[e3,e4,f4,g4,g5]TE[1]
+ ;1[g11,h11,f12,g12,f13]TE[1]
+ ;2[m11,n11,n12,o12,o13]TE[1]
+ ;3[o8,o9,p9,n10,o10]TE[1]
+ ;4[h6,h7,i7,i8,j8]TE[1]
+ ;1[i10,j10,k10,l10,m10]TE[1]
+ ;2[r10,p11,q11,r11,q12]TE[1]
+ ;3[k7,k8,l8,l9,m9]TE[1]
+ ;4[l5,j6,k6,l6,m6]TE[1]
+ )
+ (
+ ;2[q14,p15,q15,q16,r16]TE[1]
+ ;3[r5,q6,r6,p7,q7]TE[1]
+ ;4[e3,f3,f4,g4,g5]TE[1]
+ ;1[g11,h11,f12,g12,f13]TE[1]
+ ;2[o12,n13,o13,p13,o14]TE[1]
+ ;3[o8,o9,n10,o10,p10]TE[1]
+ ;4[h6,h7,i7,j7,i8]TE[1]
+ ;1[i10,j10,k10,l10,m10]TE[1]
+ ;2[m11,n11,l12,m12]TE[1]
+ ;3[k7,k8,l8,m8,m9]TE[1]
+ ;4[h9,f10,g10,h10,f11]TE[1]
+ )
+ )
+ (
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ ;1[e14,c15,d15,e15,c16]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[r5,q6,r6,p7,q7]TE[1]
+ ;4[d4,d5,e5,e6,f6]TE[1]
+ ;1[g11,h11,f12,g12,f13]TE[1]
+ (
+ ;2[o11,p11,n12,o12,o13]TE[1]
+ ;3[o8,o9,n10,o10,p10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[i10,j10,k10,l10,m10]TE[1]
+ )
+ (
+ ;2[n11,o11,p11,o12,o13]TE[1]
+ ;3[n8,o8,n9,m10,n10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[i10,j10,k10,l10]TE[1]
+ ;2[l11,k12,l12,m12,m13]TE[1]
+ ;3[p9,q9,q10,r10,q11]TE[1]
+ )
+ )
+ (
+ ;4[a1,a2,a3,b3,c3]TE[1]
+ )
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[q5,r5,q6,p7,q7]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ ;2[m11,n11,n12,o12,o13]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[i12,j12,k12,l12,k13]TE[1]
+ ;3[k7,k8,j9,k9,l9]TE[1]
+ )
+ (
+ ;4[a1,a2,a3,a4,b4]TE[1]
+ ;1[e14,c15,d15,e15,c16]TE[1]
+ ;2[p14,q14,q15,r15,r16]TE[1]
+ ;3[r5,q6,r6,p7,q7]TE[1]
+ ;4[c5,d5,d6,d7,e7]TE[1]
+ ;1[g11,h11,f12,g12,f13]TE[1]
+ ;2[n11,m12,n12,n13,o13]TE[1]
+ ;3[o8,o9,p9,n10,o10]TE[1]
+ ;4[f8,g8,h8,h9,i9]TE[1]
+ ;1[i10,j10,k10,l10,m10]TE[1]
+ ;2[i11,j11,k11,l11,j12]TE[1]
+ )
+ )
+ )
+ (
+ ;2[r18,r19,s19,t19,t20]TE[1]
+ ;3[t1,t2,t3,s4,t4]TE[1]
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ ;2[o15,p15,p16,q16,q17]TE[1]
+ ;3[r5,q6,r6,p7,q7]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[g11,h11,f12,g12,f13]TE[1]
+ ;2[m13,l14,m14,n14,m15]TE[1]
+ ;3[o8,m9,n9,o9,o10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[j9,k9,l9,i10,j10]TE[1]
+ ;2[m11,n11,o11,p11,n12]TE[1]
+ ;3[l5,k6,l6,l7,l8]TE[1]
+ ;4[j4,j5,k5,i6,j6]TE[1]
+ )
+ (
+ ;4[a1,b1,c1,d1,d2]TE[1]
+ ;1[d14,e14,c15,d15,c16]TE[1]
+ ;2[o15,p15,p16,q16,q17]TE[1]
+ ;3[q5,r5,q6,p7,q7]TE[1]
+ ;4[e3,e4,f4,f5,g5]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ ;2[m13,l14,m14,n14,m15]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[h6,i6,i7,j7,k7]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[l11,m11,n11,o11,n12]TE[1]
+ ;3[p11,q11,q12,r12,q13]TE[1]
+ ;4[m7,l8,m8,n8,l9]TE[1]
+ )
+ )
+ (
+ ;2[r18,s18,s19,s20,t20]TE[1]
+ ;3[t1,t2,t3,s4,t4]TE[1]
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ (
+ ;1[e14,c15,d15,e15,c16]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[r5,q6,r6,p7,q7]TE[1]
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ ;1[g11,h11,f12,g12,f13]TE[1]
+ ;2[n12,o12,m13,n13,n14]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[f8,f9,g9,h9,i9]TE[1]
+ ;1[e8,d9,e9,e10,e11]TE[1]
+ ;2[l9,m9,l10,l11,m11]TE[1]
+ ;3[p11,p12,o13,p13,o14]TE[1]
+ ;4[c7,b8,c8,c9,c10]TE[1]
+ )
+ (
+ ;1[d14,e14,d15,c16,d16]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[q5,r5,q6,p7,q7]TE[1]
+ ;4[d4,d5,e5,e6,f6]TE[1]
+ ;1[g11,g12,h12,f13,g13]TE[1]
+ ;2[n12,o12,m13,n13,n14]TE[1]
+ ;3[o8,n9,o9,m10,n10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[i12,j12,k12,l12,i13]TE[1]
+ ;3[l6,l7,k8,l8,l9]TE[1]
+ ;4[j4,j5,i6,j6,k6]TE[1]
+ )
+ )
+)
+(
+ ;1[d19,a20,b20,c20,d20]TE[1]
+ ;2[r18,s18,s19,s20,t20]TE[1]
+ (
+ ;3[q1,r1,s1,t1,q2]TE[1]
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[f16,g16,e17,f17,e18]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[p3,o4,p4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[j13,h14,i14,j14,h15]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[l6,m6,k7,l7,l8]TE[1]
+ ;4[g7,f8,g8,h8,h9]TE[1]
+ ;1[l9,l10,k11,l11,k12]TE[1]
+ ;2[m8,m9,n9,o9,n10]TE[1]
+ ;3[k9,j10,k10,j11,j12]TE[1]
+ ;4[i10,i11,i12,h13,i13]TE[1]
+ )
+ (
+ ;3[t1,t2,t3,s4,t4]TE[1]
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[g16,e17,f17,g17,e18]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[q5,r5,q6,p7,q7]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[j13,i14,j14,h15,i15]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ )
+)
+(
+ ;1[b18,c18,b19,a20,b20]TE[1]
+ (
+ ;2[r18,s18,s19,s20,t20]TE[1]
+ (
+ ;3[s1,t1,s2,r3,s3]TE[1]
+ (
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ (
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ (
+ ;1[j13,i14,j14,h15,i15]TE[1]
+ (
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ (
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[k9,k10,k11,k12]TE[1]
+ (
+ ;2[l8,m8,n8,l9,l10]TE[1]
+ ;3[j9,j10,j11,j12]TE[1]
+ ;4[h11,g12,h12,h13,h14]TE[1]
+ )
+ (
+ ;2[l8,l9,m9,l10]TE[1]
+ ;3[i9,j9,j10,j11,j12]TE[1]
+ ;4[h11,g12,h12,h13,h14]TE[1]
+ )
+ )
+ (
+ ;3[l5,k6,l6,m6,k7]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[k8,k9,k10,k11,k12]TE[1]
+ ;2[l7,l8,m8,l9,l10]TE[1]
+ ;3[j8,j9,j10,j11,j12]TE[1]
+ ;4[h11,i11,h12,i12,i13]TE[1]
+ )
+ )
+ (
+ ;2[k13,l13,m13,m14,n14]TE[1]
+ (
+ ;3[l5,k6,l6,m6,k7]TE[1]
+ (
+ ;4[f8,e9,f9,g9,g10]TE[1]
+ ;1[k8,k9,k10,k11,k12]TE[1]
+ ;2[o10,m11,n11,o11,n12]TE[1]
+ ;3[j8,j9,j10,j11,j12]TE[1]
+ ;4[h11,g12,h12,h13,h14]TE[1]
+ )
+ (
+ ;4[h7,f8,g8,h8,h9]TE[1]
+ ;1[k8,k9,k10,k11,k12]TE[1]
+ ;2[n9,n10,o10,n11,n12]TE[1]
+ ;3[j8,j9,j10,j11,j12]TE[1]
+ ;4[i10,i11,h12,i12,i13]TE[1]
+ )
+ )
+ (
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[k9,k10,k11,k12]TE[1]
+ ;2[i9,j9,j10,j11,j12]TE[1]
+ ;3[j4,i5,j5,k5,j6]TE[1]
+ ;4[f4,f5,g5,g6,h6]TE[1]
+ )
+ )
+ )
+ (
+ ;1[j14,h15,i15,j15,j16]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[k9,k10,k11,k12,k13]TE[1]
+ ;2[l8,m8,l9,m9,l10]TE[1]
+ ;3[j9,j10,j11,j12,j13]TE[1]
+ ;4[h11,h12,g13,h13,h14]TE[1]
+ )
+ (
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[k13,l13,m13,m14,n14]TE[1]
+ ;3[m6,l7,m7,n7,m8]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[i11,h12,i12,j12,i13]TE[1]
+ ;2[n10,m11,n11,o11,n12]TE[1]
+ ;3[n9,o9,o10,p10,p11]TE[1]
+ ;4[k9,i10,j10,k10,k11]TE[1]
+ )
+ )
+ (
+ ;3[n4,o4,p4,q4,n5]TE[1]
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ ;1[j13,i14,j14,h15,i15]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[l5,k6,l6,m6,k7]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[k8,k9,k10,k11,k12]TE[1]
+ ;2[l7,l8,m8,l9,l10]TE[1]
+ ;3[j8,j9,j10,j11,j12]TE[1]
+ ;4[h11,h12,g13,h13,h14]TE[1]
+ )
+ )
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ (
+ ;1[j14,h15,i15,j15,j16]TE[1]
+ ;2[k13,l13,l14,m14,n14]TE[1]
+ ;3[l6,m6,k7,l7,l8]TE[1]
+ ;4[g7,h7,h8,i8,h9]TE[1]
+ ;1[j11,i12,j12,h13,i13]TE[1]
+ ;2[m10,l11,m11,m12,n12]TE[1]
+ ;3[h5,g6,h6,i6,j6]TE[1]
+ ;4[g10,e11,f11,g11,g12]TE[1]
+ )
+ (
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[k13,l13,m13,m14,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[j11,i12,j12,h13,i13]TE[1]
+ ;2[n9,n10,n11,o11,n12]TE[1]
+ ;3[h5,g6,h6,i6,j6]TE[1]
+ ;4[i9,j9,k9,l9,m9]TE[1]
+ )
+ (
+ ;1[j13,i14,j14,h15,i15]TE[1]
+ (
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ (
+ ;4[g7,g8,h8,h9,h10]TE[1]
+ ;1[k9,k10,k11,k12]TE[1]
+ ;2[l8,l9,m9,l10]TE[1]
+ ;3[j9,j10,i11,j11,j12]TE[1]
+ ;4[g11,f12,g12,h12,h13]TE[1]
+ )
+ (
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[k9,k10,k11,k12]TE[1]
+ ;2[l8,l9,m9,l10]TE[1]
+ ;3[j9,j10,j11,j12]TE[1]
+ ;4[h10,h11,g12,h12,h13]TE[1]
+ )
+ )
+ (
+ ;2[k13,l13,l14,m14,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[k9,k10,k11,l11,k12]TE[1]
+ ;2[j9,j10,i11,j11,j12]TE[1]
+ ;3[h5,g6,h6,i6,j6]TE[1]
+ ;4[g4,h4,i4,i5,j5]TE[1]
+ )
+ )
+ )
+ (
+ ;4[a1,a2,a3,b3,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,e4,e5,e6,f6]TE[1]
+ ;1[j13,i14,j14,h15,i15]TE[1]
+ ;2[k13,l13,l14,m14,n14]TE[1]
+ ;3[m6,l7,m7,n7,m8]TE[1]
+ ;4[h6,g7,h7,i7,i8]TE[1]
+ ;1[i10,h11,i11,j11,i12]TE[1]
+ ;2[l10,k11,l11,m11,m12]TE[1]
+ ;3[n9,n10,o10,p10,o11]TE[1]
+ ;4[l5,m5,j6,k6,l6]TE[1]
+ )
+ )
+ (
+ ;3[t1,t2,t3,s4,t4]TE[1]
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[f15,e16,f16,d17,e17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[q5,r5,q6,p7,q7]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ (
+ ;2[n12,o12,m13,n13,n14]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[g7,g8,h8,i8,h9]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[i12,j12,k12,l12,i13]TE[1]
+ ;3[p11,p12,o13,p13,o14]TE[1]
+ ;4[m8,j9,k9,l9,m9]TE[1]
+ )
+ (
+ ;2[o12,m13,n13,o13,n14]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[i12,j12,k12,l12,i13]TE[1]
+ ;3[p11,p12,q12,r12,q13]TE[1]
+ ;4[k7,l7,m7,n7,o7]TE[1]
+ )
+ )
+ (
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ ;1[f15,e16,f16,d17,e17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[q5,r5,p6,q6,p7]TE[1]
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ ;2[n12,o12,m13,n13,n14]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[f8,f9,g9,h9,f10]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[p8,p9,q9,p10,p11]TE[1]
+ ;3[k11,l11,i12,j12,k12]TE[1]
+ ;4[e11,e12,e13,e14,f14]TE[1]
+ )
+ )
+ (
+ ;3[q1,r1,s1,t1,q2]TE[1]
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ (
+ ;3[p3,o4,p4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[n12,m13,n13,o13,n14]TE[1]
+ ;3[m6,k7,l7,m7,k8]TE[1]
+ ;4[g7,h7,h8,i8,j8]TE[1]
+ ;1[k9,k10,k11,k12,k13]TE[1]
+ ;2[m8,m9,m10,l11,m11]TE[1]
+ ;3[j9,j10,j11,j12,j13]TE[1]
+ ;4[j5,i6,j6,k6,l6]TE[1]
+ )
+ (
+ ;3[o3,p3,o4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[k13,l13,l14,m14,n14]TE[1]
+ ;3[m6,l7,m7,n7,m8]TE[1]
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[i11,h12,i12,j12,i13]TE[1]
+ ;2[m10,l11,m11,n11,m12]TE[1]
+ ;3[n9,n10,o10,p10,o11]TE[1]
+ ;4[f10,f11,g11,e12,f12]TE[1]
+ )
+ )
+ (
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[o3,p3,o4,n5,o5]TE[1]
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[k13,l13,m13,m14,n14]TE[1]
+ ;3[m6,l7,m7,n7,m8]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[j11,i12,j12,h13,i13]TE[1]
+ ;2[n10,m11,n11,o11,n12]TE[1]
+ ;3[i8,j8,k8,j9,j10]TE[1]
+ ;4[g6,i6,g7,h7,i7]TE[1]
+ )
+ )
+ (
+ ;3[t1,t2,s3,t3,s4]TE[1]
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[f15,e16,f16,d17,e17]TE[1]
+ (
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ (
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ ;2[n12,o12,m13,n13,n14]TE[1]
+ ;3[o8,n9,o9,m10,n10]TE[1]
+ ;4[g7,g8,h8,i8,h9]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[i12,j12,k12,l12,i13]
+ ;3[p10,p11,q11,p12,p13]TE[1]
+ ;4[l8,j9,k9,l9,m9]TE[1]
+ )
+ (
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ ;2[n12,o12,m13,n13,n14]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ )
+ )
+ (
+ ;2[o15,p15,p16,q16,q17]TE[1]
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ ;2[m13,l14,m14,n14,m15]TE[1]
+ ;3[o8,n9,o9,m10,n10]TE[1]
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[k11,l11,m11,n11,l12]TE[1]
+ ;3[p10,o11,p11,q11,p12]TE[1]
+ ;4[i9,j9,k9,l9,m9]TE[1]
+ )
+ )
+ (
+ ;4[a1,b1,c1,d1,d2]TE[1]
+ ;1[f15,e16,f16,d17,e17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[r5,p6,q6,r6,p7]TE[1]
+ ;4[e3,e4,f4,g4,g5]TE[1]
+ ;1[g12,h12,f13,g13,g14]TE[1]
+ ;2[n12,o12,m13,n13,n14]TE[1]
+ ;3[o8,o9,m10,n10,o10]TE[1]
+ ;4[h6,h7,i7,i8,j8]TE[1]
+ ;1[j10,k10,l10,i11,j11]TE[1]
+ ;2[p8,q8,p9,p10,p11]TE[1]
+ ;3[l11,l12,k13,l13,l14]TE[1]
+ ;4[h9,h10,f11,g11,h11]TE[1]
+ )
+ )
+ )
+ (
+ ;2[r18,r19,r20,s20,t20]TE[1]
+ ;3[s1,t1,s2,r3,s3]TE[1]
+ ;4[a1,b1,c1,c2,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[o15,o16,p16,q16,q17]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,d5,e5,f5,f6]TE[1]
+ ;1[j13,i14,j14,h15,i15]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[k9,k10,k11,k12]TE[1]
+ )
+ (
+ ;2[s17,t17,t18,t19,t20]
+ ;3[q1,r1,s1,t1,q2]TE[1]
+ ;4[a1,a2,a3,a4,b4]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[p14,q14,q15,r15,r16]
+ ;3[o3,p3,n4,o4,n5]TE[1]
+ ;4[c5,d5,d6,d7,e7]
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[m11,m12,n12,o12,o13]
+ ;3[m6,k7,l7,m7,k8]TE[1]
+ ;4[f8,g8,g9,h9,h10]
+ ;1[k9,k10,k11,k12,k13]TE[1]
+ ;2[l8,m8,n8,l9,l10]
+ ;3[j9,j10,j11,j12,j13]TE[1]
+ )
+ (
+ ;2[s17,s18,t18,t19,t20]TE[1]
+ ;3[s1,t1,s2,r3,s3]TE[1]
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[p14,p15,q15,q16,r16]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[m11,m12,n12,o12,o13]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[g7,g8,h8,h9,h10]TE[1]
+ ;1[k9,k10,k11,k12,k13]TE[1]
+ ;2[n6,n7,n8,n9,n10]TE[1]
+ ;3[j9,j10,j11,j12,j13]TE[1]
+ ;4[i11,i12,i13,h14,i14]TE[1]
+ )
+)
+(
+ ;1[a20,b20,c20,d20,e20]TE[1]
+ (
+ ;2[s17,t17,t18,t19,t20]TE[1]
+ ;3[s1,t1,s2,r3,s3]TE[1]
+ ;4[a1,a2,a3,b3,c3]TE[1]
+ ;1[h17,g18,h18,f19,g19]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[i13,i14,h15,i15,i16]TE[1]
+ ;2[m11,m12,n12,n13,o13]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[g7,g8,h8,h9,h10]TE[1]
+ ;1[g10,g11,h11,i11,h12]TE[1]
+ ;2[j10,k10,l10,k11,k12]TE[1]
+ ;3[h5,g6,h6,i6,j6]TE[1]
+ ;4[f9,e10,f10,f11,f12]TE[1]
+ )
+ (
+ ;2[r18,s18,s19,s20,t20]TE[1]
+ ;3[s1,t1,s2,r3,s3]TE[1]
+ (
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ ;1[h17,g18,h18,f19,g19]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ ;1[j13,j14,i15,j15,i16]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[k9,k10,k11,l11,k12]TE[1]
+ ;2[m8,m9,n9,o9,n10]TE[1]
+ ;3[i9,j9,j10,j11,j12]TE[1]
+ ;4[j5,j6,j7,i8,j8]TE[1]
+ )
+ (
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[h17,g18,h18,f19,g19]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[i13,i14,h15,i15,i16]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ (
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[k9,j10,k10,j11,j12]TE[1]
+ ;2[l8,m8,l9,m9,l10]TE[1]
+ ;3[i9,j9,i10,i11,i12]TE[1]
+ ;4[e9,e10,f10,f11,f12]TE[1]
+ )
+ (
+ ;4[g7,g8,h8,h9,h10]TE[1]
+ ;1[k9,j10,k10,j11,j12]TE[1]
+ ;2[l8,l9,m9,l10]TE[1]
+ ;3[i9,j9,i10,i11,i12]TE[1]
+ ;4[g11,f12,g12,h12,h13]TE[1]
+ )
+ )
+ )
+)
+(
+ ;1[a16,a17,a18,a19,a20]TE[1]
+ ;2[s17,t17,t18,t19,t20]TE[1]
+ (
+ ;3[t1,t2,t3,t4,t5]TE[1]
+ ;4[a1,b1,c1,d1,d2]TE[1]
+ ;1[c13,d13,b14,c14,b15]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[s6,r7,s7,q8,r8]TE[1]
+ ;4[e3,e4,f4,g4,g5]TE[1]
+ ;1[f11,g11,h11,e12,f12]TE[1]
+ ;2[m11,n11,n12,o12,o13]TE[1]
+ ;3[o9,p9,m10,n10,o10]TE[1]
+ ;4[h6,h7,i7,i8,j8]TE[1]
+ ;1[k9,i10,j10,k10,l10]TE[1]
+ ;2[i11,j11,k11,k12,l12]TE[1]
+ ;3[p11,p12,p13,q13,r13]TE[1]
+ ;4[k7,l7,l8,m8,n8]TE[1]
+ )
+ (
+ ;3[p1,q1,r1,s1,t1]TE[1]
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[c13,d13,b14,c14,b15]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[n2,o2,n3,m4,n4]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[f11,g11,h11,e12,f12]TE[1]
+ ;2[m11,m12,n12,n13,o13]TE[1]
+ ;3[k5,l5,j6,k6,k7]TE[1]
+ ;4[g7,g8,f9,g9,h9]TE[1]
+ ;1[k11,i12,j12,k12,k13]TE[1]
+ ;2[i9,j9,k9,k10,l10]TE[1]
+ ;3[l8,l9,m9,m10,n10]TE[1]
+ ;4[e10,c11,d11,e11,d12]TE[1]
+ )
+)
+(
+ ;1[a18,b18,c18,a19,a20]TE[1]
+ (
+ ;2[s17,t17,t18,t19,t20]TE[1]
+ (
+ ;3[q1,r1,s1,t1,q2]TE[1]
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[p3,o4,p4,n5,o5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[j14,h15,i15,j15,j16]TE[1]
+ ;2[n12,m13,n13,o13,n14]TE[1]
+ ;3[k6,l6,m6,k7,k8]TE[1]
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[k9,k10,k11,k12,k13]TE[1]
+ ;2[l8,l9,l10,m10,m11]TE[1]
+ ;3[j9,j10,j11,j12,j13]TE[1]
+ ;4[h10,h11,h12,h13,h14]TE[1]
+ )
+ (
+ ;3[s1,t1,s2,r3,s3]TE[1]
+ ;4[a1,a2,b2,c2,c3]TE[1]
+ ;1[e15,f15,e16,d17,e17]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[d4,d5,d6,e6,e7]TE[1]
+ ;1[i13,g14,h14,i14,h15]TE[1]
+ ;2[m11,m12,n12,n13,o13]TE[1]
+ ;3[l6,m6,l7,l8,l9]TE[1]
+ ;4[f8,f9,g9,h9,g10]TE[1]
+ ;1[l10,k11,l11,j12,k12]TE[1]
+ ;2[m8,m9,n9,o9,n10]TE[1]
+ ;3[j10,k10,i11,j11,i12]TE[1]
+ ;4[f11,f12,e13,f13,f14]TE[1]
+ )
+ )
+ (
+ ;2[r18,s18,s19,s20,t20]TE[1]
+ (
+ ;3[q1,r1,s1,t1,q2]TE[1]
+ ;4[a1,a2,a3,b3,c3]TE[1]
+ ;1[e15,f15,e16,d17,e17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[p3,n4,o4,p4,n5]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[j13,g14,h14,i14,j14]TE[1]
+ ;2[n12,m13,n13,o13,n14]TE[1]
+ ;3[l6,m6,k7,l7,k8]TE[1]
+ ;4[g7,f8,g8,h8,g9]TE[1]
+ ;1[k9,k10,k11,k12]TE[1]
+ ;2[l8,l9,l10,m10,m11]TE[1]
+ ;3[j9,i10,j10,j11,j12]TE[1]
+ ;4[j6,k6,i7,j7,j8]TE[1]
+ )
+ (
+ ;3[t1,t2,r3,s3,t3]TE[1]
+ ;4[a1,b1,b2,b3,c3]TE[1]
+ ;1[e15,f15,e16,d17,e17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[p4,q4,p5,o6,p6]TE[1]
+ ;4[d4,e4,e5,f5,f6]TE[1]
+ ;1[h12,i12,g13,h13,g14]TE[1]
+ ;2[m11,m12,m13,n13,n14]TE[1]
+ ;3[n7,n8,n9,m10,n10]TE[1]
+ ;4[g7,g8,h8,i8,h9]TE[1]
+ ;1[j10,k10,l10,j11,k11]TE[1]
+ )
+ )
+)
+(
+ ;1[c18,c19,a20,b20,c20]TE[1]
+ ;2[s17,t17,t18,t19,t20]TE[1]
+ ;3[t1,t2,t3,s4,t4]TE[1]
+ ;4[a1,b1,c1,d1,d2]TE[1]
+ ;1[f15,d16,e16,f16,d17]TE[1]
+ ;2[p14,q14,q15,q16,r16]TE[1]
+ ;3[q5,r5,q6,p7,q7]TE[1]
+ ;4[e3,e4,f4,g4,g5]TE[1]
+ ;1[i13,g14,h14,i14,i15]TE[1]
+ ;2[o11,p11,n12,o12,o13]TE[1]
+ ;3[k8,l8,m8,n8,o8]TE[1]
+ ;4[h6,i6,i7,j7,j8]TE[1]
+ ;1[k11,j12,k12,l12,k13]TE[1]
+ ;2[p8,p9,q9,q10,r10]TE[1]
+ ;3[j9,i10,j10,i11,i12]TE[1]
+ ;4[h8,g9,h9,h10,h11]TE[1]
+)
+(
+ ;1[c18,a19,b19,c19,a20]TE[1]
+ ;2[r18,s18,s19,s20,t20]TE[1]
+ ;3[t1,r2,s2,t2,r3]TE[1]
+ ;4[a1,b1,c1,d1,d2]TE[1]
+ ;1[f16,g16,d17,e17,f17]TE[1]
+ ;2[o15,o16,p16,p17,q17]TE[1]
+ ;3[o4,p4,q4,n5,o5]TE[1]
+ ;4[e3,f3,f4,g4,g5]TE[1]
+ ;1[j14,h15,i15,j15,i16]TE[1]
+ ;2[k13,l13,m13,m14,n14]TE[1]
+ ;3[m6,l7,m7,n7,m8]TE[1]
+ ;4[h6,g7,h7,i7,h8]TE[1]
+ ;1[i11,h12,i12,j12,i13]TE[1]
+ ;2[k15,l15,j16,k16,k17]TE[1]
+ ;3[n9,n10,o10,p10,n11]TE[1]
+ ;4[i9,i10,j10,k10,k11]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Blokus Three-Player]
+(
+ ;1[a17,b17,a18,a19,a20]TE[1]
+ ;2[s17,t17,t18,t19,t20]TE[1]
+ ;3[t1,t2,t3,s4,t4]TE[1]
+)
+(
+ ;1[b17,a18,b18,a19,a20]TE[1]
+)
+(
+ ;1[a20,b20,c20,c19,c18]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Blokus Duo]
+(
+ ;B[f9,e10,f10,g10,f11]TE[1]
+ (
+ ;W[i4,h5,i5,j5,i6]TE[1]
+ (
+ ;B[h7,g8,h8,h9,i9]TE[1]
+ (
+ ;W[f5,e6,f6,g6,e7]TE[1]
+ )
+ (
+ ;W[f5,f6,g6,e7,f7]TE[1]
+ )
+ )
+ (
+ ;B[g7,g8,h8,i8,h9]TE[1]
+ )
+ (
+ ;B[e7,c8,d8,e8,d9]TE[1]
+ (
+ ;W[h7,h8,i8,h9,h10]TE[1]
+ )
+ (
+ ;W[e5,d6,e6,f6,g6]TE[1]
+ )
+ (
+ ;W[h7,h8,h9,i9,h10]TE[1]
+ )
+ )
+ (
+ ;B[e6,e7,d8,e8,d9]TE[1]
+ )
+ (
+ ;B[g6,g7,h7,i7,g8]TE[1]
+ (
+ ;W[k6,j7,k7,j8,j9]TE[1]
+ )
+ (
+ ;W[f4,g4,e5,f5,e6]TE[1]
+ )
+ )
+ (
+ ;B[h6,h7,i7,g8,h8]TE[1]
+ )
+ (
+ ;B[g6,g7,g8,h8,h9]
+ ;W[j7,j8,j9,k9,j10]TE[1]
+ ;B[h3,f4,g4,h4,f5]TE[1]
+ ;W[h10,h11,i11,g12,h12]TE[1]
+ )
+ (
+ ;B[g6,g7,g8,h8,i8]
+ ;W[f4,g4,e5,f5,e6]TE[1]
+ )
+ (
+ ;B[i7,g8,h8,i8,h9]BM[1]
+ ;W[g6,f7,g7,h7,f8]TE[1]
+ )
+ )
+ (
+ ;W[j5,i6,j6,k6,j7]TE[1]
+ )
+)
+(
+ ;B[e9,d10,e10,f10,e11]TE[1]
+ ;W[j4,i5,j5,k5,j6]TE[1]
+ (
+ ;B[h6,g7,h7,f8,g8]TE[1]
+ )
+ (
+ ;B[h7,f8,g8,h8,g9]TE[1]
+ )
+)
+(
+ ;B[f8,e9,f9,g9,e10]TE[1]
+ ;W[i4,h5,i5,j5,i6]TE[1]
+ (
+ ;B[h8,i8,j8,k8,j9]TE[1]
+ (
+ ;W[k6,k7,l7,l8,l9]TE[1]
+ )
+ (
+ ;W[f6,g6,f7,g7,g8]TE[1]
+ )
+ )
+ (
+ ;B[g4,h4,g5,g6,g7]TE[1]
+ )
+ (
+ ;B[g5,g6,g7,h7,i7]TE[1]
+ (
+ ;W[k6,j7,k7,k8,l8]TE[1]
+ )
+ (
+ ;W[k6,j7,k7,l7,j8]TE[1]
+ )
+ )
+ (
+ ;B[g6,g7,h7,h8,i8]
+ ;W[j7,j8,i9,j9,i10]TE[1]
+ )
+ (
+ ;B[g4,g5,f6,g6,g7]
+ ;W[f3,g3,h3,e4,f4]TE[1]
+ ;B[i7,j7,k7,h8,i8]
+ ;W[k6,l6,l7,k8,l8]TE[1]
+ )
+)
+(
+ ;B[e8,e9,f9,d10,e10]TE[1]
+ ;W[i4,h5,i5,j5,i6]TE[1]
+ ;B[g6,f7,g7,h7,g8]TE[1]
+ ;W[j7,j8,j9,k9,j10]TE[1]
+)
+(
+ ;B[e8,f8,d9,e9,e10]TE[1]
+ (
+ ;W[j5,j6,k6,i7,j7]TE[1]
+ (
+ ;B[g6,g7,h7,h8,i8]TE[1]
+ (
+ ;W[i3,h4,i4,g5,h5]TE[1]
+ )
+ (
+ ;W[h4,i4,f5,g5,h5]TE[1]
+ )
+ )
+ (
+ ;B[h5,h6,g7,h7,h8]TE[1]
+ ;W[f3,f4,g4,h4,i4]TE[1]
+ )
+ )
+ (
+ ;W[j5,i6,j6,k6,j7]TE[1]
+ ;B[h5,i5,h6,g7,h7]
+ (
+ ;W[g8,h8,i8,h9,i9]TE[1]
+ )
+ (
+ ;W[i8,i9,h10,i10,h11]TE[1]
+ )
+ )
+ (
+ ;W[i4,h5,i5,j5,i6]
+ ;B[g4,g5,g6,g7,h7]TE[1]
+ ;W[g2,f3,g3,h3,f4]
+ ;B[k6,j7,k7,i8,j8]TE[1]
+ )
+)
+(
+ ;B[f8,d9,e9,f9,e10]TE[1]
+ ;W[j5,i6,j6,k6,i7]TE[1]
+ ;B[h5,h6,g7,h7,h8]TE[1]
+ (
+ ;W[g3,f4,g4,h4,i4]TE[1]
+ )
+ (
+ ;W[g4,h4,i4,f5,g5]
+ ;B[j8,k8,l8,i9,j9]TE[1]
+ )
+)
+(
+ ;B[e8,d9,e9,e10,f10]TE[1]
+ (
+ ;W[j5,i6,j6,k6,j7]TE[1]
+ ;B[f4,e5,f5,f6,f7]TE[1]
+ (
+ ;W[f3,g3,g4,g5,h5]TE[1]
+ )
+ (
+ ;W[h7,h8,h9,i9,h10]TE[1]
+ )
+ (
+ ;W[g7,h7,f8,g8,f9]TE[1]
+ )
+ )
+ (
+ ;W[i4,h5,i5,j5,i6]TE[1]
+ ;B[g4,g5,f6,g6,f7]TE[1]
+ (
+ ;W[g2,f3,g3,h3,f4]TE[1]
+ )
+ (
+ ;W[g7,h7,f8,g8,f9]TE[1]
+ )
+ )
+)
+(
+ ;B[f7,f8,e9,f9,e10]TE[1]
+ ;W[i4,i5,j5,h6,i6]TE[1]
+ ;B[h4,f5,g5,h5,g6]TE[1]
+)
+(
+ ;B[e7,e8,d9,e9,e10]
+ ;W[j5,i6,j6,h7,i7]TE[1]
+ ;B[h4,f5,g5,h5,f6]
+ ;W[f7,f8,g8,f9,f10]TE[1]
+)
+(
+ ;B[g9,e10,f10,g10,g11]
+ ;W[i4,h5,i5,j5,i6]TE[1]
+ ;B[j6,h7,i7,j7,h8]
+ ;W[f6,g6,f7,g7,g8]TE[1]
+)
+(
+ ;B[e10,f10,g10,h10,g11]
+ ;W[i4,h5,i5,j5,i6]TE[1]
+ ;B[j6,i7,j7,i8,i9]
+ ;W[e5,e6,f6,g6,g7]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Blokus Junior]
+(
+ ;B[f9,e10,f10,e11,f11]TE[1]
+)
+(
+ ;B[g9,d10,e10,f10,g10]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Nexos]
+(
+ ;1[g16,g18,f19,e20]TE[1]
+)
+(
+ ;1[h17,g18,g20,f21]TE[1]
+)
+(
+ ;1[h17,g18,f19,e20]TE[1]
+)
+(
+ ;1[g16,g18,g20,f21]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Nexos Two-Player]
+(
+ ;1[g16,g18,f19,e20]TE[1]
+)
+(
+ ;1[h17,g18,g20,f21]TE[1]
+)
+(
+ ;1[h17,g18,f19,e20]TE[1]
+)
+(
+ ;1[g16,g18,g20,f21]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Blokus Trigon]
+(
+ ;1[r12,r13,s13,r14,s14,r15]TE[1]
+)
+(
+ ;1[t12,s13,t13,r14,s14,r15]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Blokus Trigon Two-Player]
+(
+ ;1[r12,r13,s13,r14,s14,r15]TE[1]
+ (
+ ;2[r4,q5,r5,q6,r6,r7]TE[1]
+ ;3[j7,k7,l7,m7,m8,n8]TE[1]
+ ;4[v11,w11,w12,x12,y12,z12]TE[1]
+ ;1[n9,o9,o10,p10,p11,q11]BM[1]
+ ;2[j6,k6,l6,m6,n6,o6]TE[1]
+ )
+ (
+ ;2[r4,r5,s5,r6,s6,r7]
+ )
+ (
+ ;2[r4,q5,r5,p6,q6,p7]TE[1]
+ )
+)
+(
+ ;1[t12,s13,t13,r14,s14,r15]TE[1]
+ ;2[r4,q5,r5,p6,q6,p7]TE[1]
+ ;3[j7,k7,l7,m7,n7,o7]TE[1]
+ ;4[u12,v12,w12,x12,y12,z12]TE[1]
+)
+)
--- /dev/null
+(
+;GM[Blokus Trigon Three-Player]
+(
+ ;1[p11,o12,p12,o13,p13,p14]TE[1]
+)
+(
+ ;1[r11,q12,r12,p13,q13,p14]TE[1]
+)
+)
--- /dev/null
+<RCC>
+ <qresource prefix="/pentobi_books">
+ <file>book_callisto.blksgf</file>
+ <file>book_callisto_2.blksgf</file>
+ <file>book_callisto_3.blksgf</file>
+ <file>book_classic.blksgf</file>
+ <file>book_classic_2.blksgf</file>
+ <file>book_classic_3.blksgf</file>
+ <file>book_duo.blksgf</file>
+ <file>book_junior.blksgf</file>
+ <file>book_nexos.blksgf</file>
+ <file>book_nexos_2.blksgf</file>
+ <file>book_trigon.blksgf</file>
+ <file>book_trigon_2.blksgf</file>
+ <file>book_trigon_3.blksgf</file>
+ </qresource>
+</RCC>
--- /dev/null
+add_executable(convert Main.cpp)
+
+target_link_libraries(convert Qt5::Widgets)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file convert/Main.cpp
+ Utility program for converting icons between image formats.
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <iostream>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QImageReader>
+#include <QImageWriter>
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+ QCoreApplication app(argc, argv);
+ try
+ {
+ QCommandLineParser parser;
+ QCommandLineOption optionHdpi("hdpi");
+ parser.addOption(optionHdpi);
+ parser.process(app);
+ auto args = parser.positionalArguments();
+ if (args.size() != 2)
+ throw QString("Need two arguments");
+ auto in = args.at(0);
+ auto out = args.at(1);
+ QImageReader reader(in);
+ QImage image = reader.read();
+ if (image.isNull())
+ throw QString("%1: %2").arg(in, reader.errorString());
+ if (parser.isSet(optionHdpi))
+ {
+ QImageReader reader(in);
+ reader.setScaledSize(2 * image.size());
+ image = reader.read();
+ if (image.isNull())
+ throw QString("%1: %2").arg(in, reader.errorString());
+ }
+ QImageWriter writer(out);
+ if (! writer.write(image))
+ throw QString("%1: %2").arg(out, writer.errorString());
+ }
+ catch (const QString& msg)
+ {
+ std::cerr << msg.toLocal8Bit().constData() << '\n';
+ return 1;
+ }
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+/**
+
+@page libboardgame_doc_tags Tags used in documentation
+This page defines attributes of documentation elements that are in
+widespread use. For brevity, the documentation block contains only
+a reference to the section of this page.
+
+@section libboardgame_avoid_stack_allocation Class size is large
+The size of this class is large because it contains large members that are not
+allocated on the heap to avoid dereferencing pointers for speed reasons. It
+should be avoided to create instances of this class on the stack.
+
+@section libboardgame_doc_obj_ref_opt Object reference optimization
+This class uses a reference to a certain object several times but does not
+store the reference at construction time for memory and/or speed optimization.
+The reference is passed as an argument to the functions that need it. The
+class instance assumes (and might check with assertions) that the reference
+always refers to the same object .
+
+@section libboardgame_doc_storesref Stores a reference
+Used for parameters to indicate that the class will store a reference to the
+parameter. The lifetime of the parameter must exceed the lifetime of the
+constructed class.
+
+@section libboardgame_doc_threadsafe_after_construction Thread-safe after
+construction
+Used for classes that, that are thread-safe (w.r.t. different instances) after
+construction. The constructor (and potentially also the destructor) is not
+thread-safe, for example because it modifies non-const static class members.
+
+
+@page libboardgame_doc_glossary Glossary
+This page explains and defines terms used in the documentation.
+
+@section libboardgame_doc_gogui GoGui
+Graphical interface for Go engines using GTP. Defines several GTP extension
+commands. http://gogui.sf.net
+
+@section libboardgame_doc_gnugo GNU Go
+GNU Go program http://www.gnu.org/s/gnugo/
+
+@section libboardgame_doc_gtp GTP
+Go Text Protocol http://www.lysator.liu.se/~gunnar/gtp/
+
+@section libboardgame_doc_uct UCT
+Upper Confidence bounds applied to Tree: a Monte-Carlo tree search algorithm
+that applies bandit ideas to the move selection at tree nodes.
+See @ref libboardgame_doc_kocsis_szepesvari_2006
+
+@section libboardgame_doc_rave RAVE
+Rapid Action Value Estimation: Keeps track of the value of a move averaged
+over all simulations in the subtree of a node in which the move was played
+by a player in a position following the node (inclusive).
+See @ref libboardgame_doc_gelly_silver_2007
+
+@section libboardgame_doc_sgf SGF
+Smart Game Format http://www.red-bean.com/sgf/
+
+@page libboardgame_doc_bibliography Bibliography
+List of publications.
+
+@section libboardgame_doc_enz_2009 A Lock-free Multithreaded Monte-Carlo Tree Search Algorithm.
+M. Enzenberger, M. Mueller. Advances in Computer Games 2009.
+<a href="http://webdocs.cs.ualberta.ca/~mmueller/ps/enzenberger-mueller-acg12.pdf">(PDF)</a>
+
+@section libboardgame_doc_gelly_silver_2007 Combining Online and Offline Knowledge in UCT.
+S. Gelly, D. Silver. Proceedings of the 24th international conference on Machine learning, pp. 273-280, 2007.
+<a href="http://www.machinelearning.org/proceedings/icml2007/papers/387.pdf">(PDF)</a>
+
+@section libboardgame_doc_kocsis_szepesvari_2006 Bandit Based Monte-Carlo Planning
+L. Kocsis, Cs. Szepesvári. Proceedings of the 17th European Conference on
+Machine Learning, Springer-Verlag, Berlin, LNCS/LNAI 4212, September 18-22,
+pp. 282-293, 2006
+<a href="http://www.sztaki.hu/~szcsaba/papers/ecml06.pdf">(PDF)</a>
+
+*/
--- /dev/null
+/** @mainpage notitle
+
+ @section mainpage_libboardgame LibBoardGame Modules
+
+ The LibBoardGame modules contain code that is not specific to the board
+ game Blokus and could be reused for other projects:
+
+ - libboardgame_gtp -
+ Implementation of the Go Text Protocol GTP (@ref libboardgame_doc_gtp)
+ - libboardgame_sys -
+ Platform-dependent functionality
+ - libboardgame_util -
+ General utilities not specific to board games
+ - libboardgame_sgf -
+ Implementation of the Smart Game Format (@ref libboardgame_doc_sgf)
+ - libboardgame_base -
+ Utility classes and functions specific to board games
+ - libboardgame_mcts -
+ Monte-Carlo tree search
+ - libboardgame_test -
+ Functionality for unit tests similar to Boost::Test
+
+ @section mainpage_pentobi Pentobi Modules
+
+ The Pentobi modules are specific to the board game Blokus:
+
+ - libpentobi_base -
+ General Blokus-specific functionality
+ - libpentobi_mcts -
+ Blokus player based on Monte-Carlo tree search
+ - pentobi_gtp -
+ GTP interface to the player in libpentobi_mcts
+ - twogtp -
+ Tool for playing games between two GTP engines
+ (currently only supported on Linux/GCC)
+
+ @section mainpage_gui Pentobi QWidgets GUI Modules
+
+ The Pentobi QWidgets GUI modules implement a user interface based on
+ Qt/QWidgets and targeted at desktops.
+ They have a dependency on the following
+ <a href="http://qt.digia.com/">Qt</a> libraries: QtCore4, QtGui4.
+ They are currently used for the desktop versions of Pentobi.
+ They may become obsolete in the future, once the QML GUI Modules
+ (@ref mainpage_gui_qml) provide the same functionality.
+
+ - convert -
+ Small helper program to convert SVG icons to bitmaps at build time
+ - libpentobi_gui -
+ GUI functionality that could be reused for other projects
+ - libpentobi_thumbnail -
+ Common functionality for file preview thumbnailers
+ - pentobi -
+ Main program that provides a GUI for the player in libpentobi_mcts
+ - pentobi_thumbnailer -
+ Generates file preview thumbnails for the
+ <a href="http://www.gnome.org/">Gnome</a> desktop
+ - pentobi_kde_thumbnailer -
+ Plugin for file preview thumbnails for the
+ <a href="http://www.kde.org/">KDE</a> desktop
+
+ @section mainpage_gui_qml Pentobi QML GUI Modules
+
+ The Pentobi QML GUI modules implement a user interface based on
+ Qt Quick / QML. They currently support only a subset of the features
+ of the QWidgets-based GUI (@ref mainpage_gui) but provide fluid
+ animations and are usable on touch-screens. They are currently
+ used for the Android version of Pentobi.
+
+ - pentobi_qml -
+ Main program that provides a GUI for the player in libpentobi_mcts
+*/
--- /dev/null
+add_library(boardgame_base STATIC
+ CoordPoint.h
+ CoordPoint.cpp
+ Engine.h
+ Engine.cpp
+ Geometry.h
+ GeometryUtil.h
+ Grid.h
+ Marker.h
+ Point.h
+ PointTransform.h
+ Rating.h
+ Rating.cpp
+ RectGeometry.h
+ RectTransform.h
+ RectTransform.cpp
+ StringRep.h
+ StringRep.cpp
+ Transform.h
+ Transform.cpp
+)
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CoordPoint.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "CoordPoint.h"
+
+#include <iostream>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, const CoordPoint& p)
+{
+ if (! p.is_null())
+ out << '(' << p.x << ',' << p.y << ')';
+ else
+ out << "NULL";
+ return out;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CoordPoint.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_COORD_POINT_H
+#define LIBBOARDGAME_BASE_COORD_POINT_H
+
+#include <limits>
+#include <iosfwd>
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** %Point stored as x,y coordinates. */
+struct CoordPoint
+{
+ int x;
+
+ int y;
+
+ static bool is_onboard(int x, int y, unsigned width, unsigned height);
+
+ static CoordPoint null();
+
+ CoordPoint() = default;
+
+ CoordPoint(int x, int y);
+
+ bool operator==(const CoordPoint& p) const;
+
+ bool operator!=(const CoordPoint& p) const;
+
+ bool operator<(const CoordPoint& p) const;
+
+ CoordPoint operator+(const CoordPoint& p) const;
+
+ CoordPoint operator-(const CoordPoint& p) const;
+
+ CoordPoint& operator+=(const CoordPoint& p);
+
+ CoordPoint& operator-=(const CoordPoint& p);
+
+ bool is_null() const;
+
+ bool is_onboard(unsigned width, unsigned height) const;
+};
+
+inline CoordPoint::CoordPoint(int x, int y)
+{
+ this->x = x;
+ this->y = y;
+}
+
+inline bool CoordPoint::operator==(const CoordPoint& p) const
+{
+ return x == p.x && y == p.y;
+}
+
+inline bool CoordPoint::operator<(const CoordPoint& p) const
+{
+ if (y != p.y)
+ return y < p.y;
+ return x < p.x;
+}
+
+inline bool CoordPoint::operator!=(const CoordPoint& p) const
+{
+ return ! operator==(p);
+}
+
+inline CoordPoint CoordPoint::operator+(const CoordPoint& p) const
+{
+ return CoordPoint(x + p.x, y + p.y);
+}
+
+inline CoordPoint& CoordPoint::operator+=(const CoordPoint& p)
+{
+ *this = *this + p;
+ return *this;
+}
+
+inline CoordPoint CoordPoint::operator-(const CoordPoint& p) const
+{
+ return CoordPoint(x - p.x, y - p.y);
+}
+
+inline CoordPoint& CoordPoint::operator-=(const CoordPoint& p)
+{
+ *this = *this - p;
+ return *this;
+}
+
+inline CoordPoint CoordPoint::null()
+{
+ return CoordPoint(numeric_limits<int>::max(), numeric_limits<int>::max());
+}
+
+inline bool CoordPoint::is_onboard(int x, int y, unsigned width,
+ unsigned height)
+{
+ return x >= 0 && x < static_cast<int>(width)
+ && y >= 0 && y < static_cast<int>(height);
+}
+
+inline bool CoordPoint::is_onboard(unsigned width, unsigned height) const
+{
+ return is_onboard(x, y, width, height);
+}
+
+inline bool CoordPoint::is_null() const
+{
+ return x == numeric_limits<int>::max();
+}
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, const CoordPoint& p);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_COORD_POINT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Engine.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Engine.h"
+
+#include "libboardgame_sys/CpuTime.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/RandomGenerator.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+using libboardgame_gtp::Failure;
+using libboardgame_util::flush_log;
+using libboardgame_util::RandomGenerator;
+
+//-----------------------------------------------------------------------------
+
+Engine::Engine()
+{
+ add("cputime", &Engine::cmd_cputime);
+ add("set_random_seed", &Engine::cmd_set_random_seed);
+}
+
+Engine::~Engine() = default;
+
+void Engine::cmd_cputime(Response& response)
+{
+ double time = libboardgame_sys::cpu_time();
+ if (time == -1)
+ throw Failure("cannot determine cpu time");
+ response << time;
+}
+
+/** Set global random seed.
+ Compatible with @ref libboardgame_doc_gnugo <br>
+ Arguments: random seed */
+void Engine::cmd_set_random_seed(const Arguments& args)
+{
+ RandomGenerator::set_global_seed(args.parse<RandomGenerator::ResultType>());
+}
+
+void Engine::on_handle_cmd_begin()
+{
+ flush_log();
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Engine.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_ENGINE_H
+#define LIBBOARDGAME_BASE_ENGINE_H
+
+#include "libboardgame_gtp/Engine.h"
+
+namespace libboardgame_base {
+
+using libboardgame_gtp::Arguments;
+using libboardgame_gtp::Response;
+
+//-----------------------------------------------------------------------------
+
+class Engine
+ : public libboardgame_gtp::Engine
+{
+public:
+ void cmd_cputime(Response&);
+ void cmd_set_random_seed(const Arguments&);
+
+ Engine();
+
+ ~Engine();
+
+protected:
+ void on_handle_cmd_begin() override;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_ENGINE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Geometry.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_GEOMETRY_H
+#define LIBBOARDGAME_BASE_GEOMETRY_H
+
+#include <memory>
+#include <sstream>
+#include "CoordPoint.h"
+#include "StringRep.h"
+#include "libboardgame_util/ArrayList.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+using libboardgame_util::ArrayList;
+
+//-----------------------------------------------------------------------------
+
+/** %Geometry data of a board with a given size.
+ This class is a base class that uses virtual functions in its constructor
+ that allow to restrict the shape of the board to a subset of the rectangle
+ and/or to define different definitions of adjacent and diagonal neighbors
+ of a point for geometries that are not a regular rectangular grid.
+ @tparam P An instantiation of libboardgame_base::Point (or compatible
+ class)
+ @tparam S A class with functions to convert points from and to strings
+ depending on the string representation of points in the game. */
+template<class P>
+class Geometry
+{
+public:
+ typedef P Point;
+
+ typedef typename Point::IntType IntType;
+
+ /** On-board adjacent neighbors of a point. */
+ typedef ArrayList<Point, 4, unsigned short> AdjList;
+
+ /** On-board diagonal neighbors of a point
+ Currently supports up to nine diagonal points as used on boards
+ for Blokus Trigon. */
+ typedef ArrayList<Point, 9, unsigned short> DiagList;
+
+ /** Adjacent neighbors of a coordinate. */
+ typedef ArrayList<CoordPoint, 4> AdjCoordList;
+
+ /** Diagonal neighbors of a coordinate. */
+ typedef ArrayList<CoordPoint, 9> DiagCoordList;
+
+ class Iterator
+ {
+ public:
+ explicit Iterator(IntType i) { m_i = i; }
+
+ bool operator==(Iterator it) const { return m_i == it.m_i; }
+
+ bool operator!=(Iterator it) const { return m_i != it.m_i; }
+
+ void operator++() { ++m_i; }
+
+ Point operator*() const { return Point(m_i); }
+
+ private:
+ IntType m_i;
+ };
+
+ virtual ~Geometry();
+
+ virtual AdjCoordList get_adj_coord(int x, int y) const = 0;
+
+ virtual DiagCoordList get_diag_coord(int x, int y) const = 0;
+
+ /** Return the point type if the board has different types of points.
+ For example, in the geometry used in Blokus Trigon, there are two
+ point types (0=upward triangle, 1=downward triangle); in a regular
+ rectangle, there is only one point type. By convention, 0 is the
+ type of the point at (0,0).
+ @param x The x coordinate (may be negative and/or outside the board).
+ @param y The y coordinate (may be negative and/or outside the board). */
+ virtual unsigned get_point_type(int x, int y) const = 0;
+
+ /** Get repeat interval for point types along the x axis.
+ If the board has different point types, the layout of the point types
+ repeats in this x interval. If the board has only one point type,
+ the function should return 1. */
+ virtual unsigned get_period_x() const = 0;
+
+ /** Get repeat interval for point types along the y axis.
+ @see get_period_x(). */
+ virtual unsigned get_period_y() const = 0;
+
+ Iterator begin() const { return Iterator(0); }
+
+ Iterator end() const { return Iterator(get_range()); }
+
+ unsigned get_point_type(CoordPoint p) const;
+
+ unsigned get_point_type(Point p) const;
+
+ bool is_onboard(unsigned x, unsigned y) const;
+
+ bool is_onboard(CoordPoint p) const;
+
+ /** Return the point at a given coordinate.
+ @pre x < get_width()
+ @pre y < get_height()
+ @return The point or Point::null() if this coordinates are
+ off-board. */
+ Point get_point(unsigned x, unsigned y) const;
+
+ unsigned get_width() const;
+
+ unsigned get_height() const;
+
+ /** Get range used for onboard points. */
+ IntType get_range() const;
+
+ unsigned get_x(Point p) const;
+
+ unsigned get_y(Point p) const;
+
+ bool from_string(const string& s, Point& p) const;
+
+ const string& to_string(Point p) const;
+
+ const AdjList& get_adj(Point p) const;
+
+ const DiagList& get_diag(Point p) const;
+
+protected:
+ explicit Geometry(unique_ptr<StringRep> string_rep =
+ unique_ptr<StringRep>(new StdStringRep));
+
+ /** Initialize.
+ Subclasses must call this function in their constructors. */
+ void init(unsigned width, unsigned height);
+
+ /** Initialize on-board points.
+ This function is used in init() and allows the subclass to restrict the
+ on-board points to a subset of the on-board points of a rectangle to
+ support different board shapes. It will only be called with x and
+ y within the width and height of the geometry. */
+ virtual bool init_is_onboard(unsigned x, unsigned y) const = 0;
+
+private:
+ AdjList m_adj[Point::range_onboard];
+
+ DiagList m_diag[Point::range_onboard];
+
+ IntType m_range;
+
+ Point m_points[Point::max_width][Point::max_height];
+
+ unique_ptr<StringRep> m_string_rep;
+
+ unsigned m_width;
+
+ unsigned m_height;
+
+ unsigned m_x[Point::range_onboard];
+
+ unsigned m_y[Point::range_onboard];
+
+ unsigned m_point_type[Point::range_onboard];
+
+ string m_string[Point::range];
+
+#if LIBBOARDGAME_DEBUG
+ bool is_valid(Point p) const;
+#endif
+};
+
+
+template<class P>
+Geometry<P>::Geometry(unique_ptr<StringRep> string_rep)
+ : m_string_rep(move(string_rep))
+{ }
+
+template<class P>
+Geometry<P>::~Geometry() = default;
+
+template<class P>
+bool Geometry<P>::from_string(const string& s, Point& p) const
+{
+ istringstream in(s);
+ unsigned x;
+ unsigned y;
+ if (m_string_rep->read(in, m_width, m_height, x, y)
+ && is_onboard(CoordPoint(x, y)))
+ {
+ p = get_point(x, y);
+ return true;
+ }
+ return false;
+}
+
+template<class P>
+inline auto Geometry<P>::get_adj(Point p) const -> const AdjList&
+{
+ LIBBOARDGAME_ASSERT(is_valid(p));
+ return m_adj[p.to_int()];
+}
+
+template<class P>
+inline auto Geometry<P>::get_diag(Point p) const -> const DiagList&
+{
+ LIBBOARDGAME_ASSERT(is_valid(p));
+ return m_diag[p.to_int()];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_height() const
+{
+ return m_height;
+}
+
+template<class P>
+inline auto Geometry<P>::get_point(unsigned x, unsigned y) const -> Point
+{
+ LIBBOARDGAME_ASSERT(x < m_width);
+ LIBBOARDGAME_ASSERT(y < m_height);
+ return m_points[x][y];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_point_type(Point p) const
+{
+ LIBBOARDGAME_ASSERT(is_valid(p));
+ return m_point_type[p.to_int()];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_point_type(CoordPoint p) const
+{
+ return get_point_type(p.x, p.y);
+}
+
+template<class P>
+inline auto Geometry<P>::get_range() const -> IntType
+{
+ return m_range;
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_width() const
+{
+ return m_width;
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_x(Point p) const
+{
+ LIBBOARDGAME_ASSERT(is_valid(p));
+ return m_x[p.to_int()];
+}
+
+template<class P>
+inline unsigned Geometry<P>::get_y(Point p) const
+{
+ LIBBOARDGAME_ASSERT(is_valid(p));
+ return m_y[p.to_int()];
+}
+
+template<class P>
+void Geometry<P>::init(unsigned width, unsigned height)
+{
+ LIBBOARDGAME_ASSERT(width >= 1);
+ LIBBOARDGAME_ASSERT(height >= 1);
+ LIBBOARDGAME_ASSERT(width <= Point::max_width);
+ LIBBOARDGAME_ASSERT(height <= Point::max_height);
+ m_width = width;
+ m_height = height;
+ m_string[Point::null().to_int()] = "null";
+ IntType n = 0;
+ ostringstream ostr;
+ for (unsigned y = 0; y < height; ++y)
+ for (unsigned x = 0; x < width; ++x)
+ if (init_is_onboard(x, y))
+ {
+ m_points[x][y] = Point(n);
+ m_x[n] = x;
+ m_y[n] = y;
+ ostr.str("");
+ m_string_rep->write(ostr, x, y, width, height);
+ m_string[n] = ostr.str();
+ ++n;
+ }
+ else
+ m_points[x][y] = Point::null();
+ m_range = n;
+ for (IntType i = 0; i < m_range; ++i)
+ {
+ Point p(i);
+ auto x = get_x(p);
+ auto y = get_y(p);
+ for (auto& p : get_adj_coord(x, y))
+ if (is_onboard(p))
+ m_adj[i].push_back(get_point(p.x, p.y));
+ for (auto& p : get_diag_coord(x, y))
+ if (is_onboard(p))
+ m_diag[i].push_back(get_point(p.x, p.y));
+ m_point_type[i] = get_point_type(x, y);
+ }
+}
+
+template<class P>
+inline bool Geometry<P>::is_onboard(unsigned x, unsigned y) const
+{
+ return ! get_point(x, y).is_null();
+}
+
+template<class P>
+bool Geometry<P>::is_onboard(CoordPoint p) const
+{
+ return p.is_onboard(m_width, m_height) && is_onboard(p.x, p.y);
+}
+
+#if LIBBOARDGAME_DEBUG
+
+template<class P>
+inline bool Geometry<P>::is_valid(Point p) const
+{
+ return ! p.is_null() && p.to_int() < get_range();
+}
+
+#endif
+
+template<class P>
+inline const string& Geometry<P>::to_string(Point p) const
+{
+ LIBBOARDGAME_ASSERT(p.to_int() < get_range());
+ return m_string[p.to_int()];
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_GEOMETRY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/GeometryUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_GEOMETRY_UTIL_H
+#define LIBBOARDGAME_BASE_GEOMETRY_UTIL_H
+
+#include "Geometry.h"
+
+namespace libboardgame_base {
+namespace geometry_util {
+
+//-----------------------------------------------------------------------------
+
+/** Shift a list of points as close to the (0,0) point as possible.
+ This will minimize the minimum x and y coordinates. The function also
+ returns the width and height of the bounding box and the offset that was
+ subtracted from the points for the shifting.
+ @note This transformation does not preserve point types. If the original
+ list was compatible with the point types on the board, the new point type of
+ (0,0) will be Geometry::get_point_type(offset).
+ @tparam T An iterator over a container containing CoordPoint element. */
+template<typename T>
+void normalize_offset(T begin, T end, unsigned& width, unsigned& height,
+ CoordPoint& offset)
+{
+ int min_x = numeric_limits<int>::max();
+ int min_y = numeric_limits<int>::max();
+ int max_x = numeric_limits<int>::min();
+ int max_y = numeric_limits<int>::min();
+ for (auto i = begin; i != end; ++i)
+ {
+ if (i->x < min_x)
+ min_x = i->x;
+ if (i->x > max_x)
+ max_x = i->x;
+ if (i->y < min_y)
+ min_y = i->y;
+ if (i->y > max_y)
+ max_y = i->y;
+ }
+ width = max_x - min_x + 1;
+ height = max_y - min_y + 1;
+ offset = CoordPoint(min_x, min_y);
+ for (auto i = begin; i != end; ++i)
+ *i -= offset;
+}
+
+/** Get an offset to shift points that are not compatible with the point types
+ used in the geometry.
+ The offset shifts points in a minimal positive direction to match the
+ types, x-direction is preferred.
+ @param geo
+ @param point_type The point type of (0, 0) of the coordinate system used by
+ the points. */
+template<typename P>
+CoordPoint type_match_offset(const Geometry<P>& geo, unsigned point_type)
+{
+ for (unsigned y = 0; y < geo.get_period_y(); ++y)
+ for (unsigned x = 0; x < geo.get_period_x(); ++x)
+ if (geo.get_point_type(x, y) == point_type)
+ return CoordPoint(x, y);
+ LIBBOARDGAME_ASSERT(false);
+ return CoordPoint(0, 0);
+}
+
+/** Apply type_match_offset() to a list of points.
+ @tparam T An iterator over a container containing CoordPoint elements.
+ @param geo The geometry.
+ @param begin The beginning of the list of points.
+ @param end The end of the list of points.
+ @param point_type The point type of (0,0) in the list of points. */
+template<typename P, typename T>
+void type_match_shift(const Geometry<P>& geo, T begin, T end,
+ unsigned point_type)
+{
+ CoordPoint offset = type_match_offset(geo, point_type);
+ for (auto i = begin; i != end; ++i)
+ *i += offset;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace geometry_util
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_GEOMETRY_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Grid.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_GRID_H
+#define LIBBOARDGAME_BASE_GRID_H
+
+#include <algorithm>
+#include <cstring>
+#include <iomanip>
+#include <sstream>
+#include <type_traits>
+#include "Geometry.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+template<class T>
+string grid_to_string(const T& grid, const Geometry<typename T::Point>& geo)
+{
+ ostringstream buffer;
+ size_t max_len = 0;
+ for (auto p : geo)
+ {
+ buffer.str("");
+ buffer << grid[p];
+ max_len = max(max_len, buffer.str().length());
+ }
+ buffer.str("");
+ auto width = geo.get_width();
+ auto height = geo.get_height();
+ string empty(max_len, ' ');
+ for (unsigned y = 0; y < height; ++y)
+ {
+ for (unsigned x = 0; x < width; ++x)
+ {
+ auto p = geo.get_point(x, y);
+ if (! p.is_null())
+ buffer << setw(int(max_len)) << grid[p];
+ else
+ buffer << empty;
+ if (x < width - 1)
+ buffer << ' ';
+ }
+ buffer << '\n';
+ }
+ return buffer.str();
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P, typename T> class GridExt;
+
+/** Elements assigned to on-board points.
+ The elements must be default-constructible. This class is a POD if the
+ element type is a POD.
+ @tparam P An instantiation of libboardgame_base::Point (or compatible
+ class)
+ @tparam T The element type. */
+template<class P, typename T>
+class Grid
+{
+ friend class GridExt<P, T>; // for GridExt::copy_from(Grid)
+
+public:
+ typedef P Point;
+
+ typedef libboardgame_base::Geometry<P> Geometry;
+
+ T& operator[](const Point& p);
+
+ const T& operator[](const Point& p) const;
+
+ /** Fill all on-board points for a given geometry with a value. */
+ void fill(const T& val, const Geometry& geo);
+
+ /** Fill points with a value. */
+ void fill_all(const T& val);
+
+ string to_string(const Geometry& geo) const;
+
+ void copy_from(const Grid& grid, const Geometry& geo);
+
+ /** Specialized version for trivially copyable elements.
+ Can be used instead of copy_from if the compiler is not smart enough to
+ figure out that it can use memcpy.
+ @pre std::is_trivially_copyable<T>::value */
+ void memcpy_from(const Grid& grid, const Geometry& geo);
+
+private:
+ T m_a[Point::range_onboard];
+};
+
+template<class P, typename T>
+inline T& Grid<P, T>::operator[](const Point& p)
+{
+ LIBBOARDGAME_ASSERT(! p.is_null());
+ return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline const T& Grid<P, T>::operator[](const Point& p) const
+{
+ LIBBOARDGAME_ASSERT(! p.is_null());
+ return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline void Grid<P, T>::copy_from(const Grid& grid, const Geometry& geo)
+{
+ copy(grid.m_a, grid.m_a + geo.get_range(), m_a);
+}
+
+template<class P, typename T>
+inline void Grid<P, T>::fill(const T& val, const Geometry& geo)
+{
+ std::fill(m_a, m_a + geo.get_range(), val);
+}
+
+template<class P, typename T>
+inline void Grid<P, T>::fill_all(const T& val)
+{
+ std::fill(m_a, m_a + Point::range_onboard, val);
+}
+
+template<class P, typename T>
+void Grid<P, T>::memcpy_from(const Grid& grid, const Geometry& geo)
+{
+ // std::is_trivially_copyable is not available with GCC < 5
+#if ! (__GNUC__ && __GNUC__ < 5)
+ static_assert(is_trivially_copyable<T>::value, "");
+#endif
+ memcpy(&m_a, grid.m_a, geo.get_range() * sizeof(T));
+}
+
+template<class P, typename T>
+string Grid<P, T>::to_string(const Geometry& geo) const
+{
+ return grid_to_string(*this, geo);
+}
+
+//-----------------------------------------------------------------------------
+
+/** Like Grid, but allows Point::null() as index. */
+template<class P, typename T>
+class GridExt
+{
+public:
+ typedef P Point;
+
+ typedef libboardgame_base::Geometry<P> Geometry;
+
+ T& operator[](const Point& p);
+
+ const T& operator[](const Point& p) const;
+
+ /** Fill all on-board points for a given geometry with a value. */
+ void fill(const T& val, const Geometry& geo);
+
+ /** Fill points with a value. */
+ void fill_all(const T& val);
+
+ string to_string(const Geometry& geo) const;
+
+ void copy_from(const Grid<P, T>& grid, const Geometry& geo);
+
+ void copy_from(const GridExt& grid, const Geometry& geo);
+
+private:
+ T m_a[Point::range];
+};
+
+template<class P, typename T>
+inline T& GridExt<P, T>::operator[](const Point& p)
+{
+ return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline const T& GridExt<P, T>::operator[](const Point& p) const
+{
+ return m_a[p.to_int()];
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::fill(const T& val, const Geometry& geo)
+{
+ std::fill(m_a, m_a + geo.get_range(), val);
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::fill_all(const T& val)
+{
+ std::fill(m_a, m_a + Point::range, val);
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::copy_from(const Grid<P, T>& grid,
+ const Geometry& geo)
+{
+ copy(grid.m_a, grid.m_a + geo.get_range(), m_a);
+}
+
+template<class P, typename T>
+inline void GridExt<P, T>::copy_from(const GridExt& grid,
+ const Geometry& geo)
+{
+ copy(grid.m_a, grid.m_a + geo.get_range(), m_a);
+}
+
+template<class P, typename T>
+string GridExt<P, T>::to_string(const Geometry& geo) const
+{
+ return grid_to_string(*this, geo);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_GRID_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Marker.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_MARKER_H
+#define LIBBOARDGAME_BASE_MARKER_H
+
+#include <algorithm>
+#include <limits>
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** %Marker to mark points on board with fast operation to clear all marks.
+ This marker is typically used in recursive fills or other loops to
+ remember what points have already been visited.
+ @tparam P An instantiation of libboardgame_base::Point */
+template<class P>
+class Marker
+{
+public:
+ typedef P Point;
+
+ Marker();
+
+ void clear();
+
+ /** Mark a point.
+ @return true if the point was already marked. */
+ bool set(Point p);
+
+ bool operator[](Point p) const;
+
+ /** Set up for overflow test (for testing purposes only).
+ The function is equivalent to calling reset() and then clear()
+ nu_clear times. It allows a faster implementation of a unit test case
+ that tests if the overflow is handled correctly, if clear() is called
+ more than numeric_limits<unsigned>::max() times. */
+ void setup_for_overflow_test(unsigned nu_clear);
+
+private:
+ unsigned m_current;
+
+ unsigned m_a[Point::range];
+
+ void reset();
+};
+
+template<class P>
+inline Marker<P>::Marker()
+{
+ reset();
+}
+
+template<class P>
+bool Marker<P>::operator[](Point p) const
+{
+ return m_a[p.to_int()] == m_current;
+}
+
+template<class P>
+inline void Marker<P>::clear()
+{
+ if (--m_current == 0)
+ reset();
+}
+
+template<class P>
+inline void Marker<P>::setup_for_overflow_test(unsigned nu_clear)
+{
+ reset();
+ m_current -= nu_clear;
+}
+
+template<class P>
+inline void Marker<P>::reset()
+{
+ m_current = numeric_limits<unsigned>::max() - 1;
+ fill(m_a, m_a + Point::range, numeric_limits<unsigned>::max());
+}
+
+template<class P>
+inline bool Marker<P>::set(Point p)
+{
+ auto& a = m_a[p.to_int()];
+ if (a == m_current)
+ return true;
+ a = m_current;
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_MARKER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Point.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_POINT_H
+#define LIBBOARDGAME_BASE_POINT_H
+
+#include <limits>
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+using namespace libboardgame_util;
+
+//-----------------------------------------------------------------------------
+
+/** Coordinate on the board.
+ Depending on the game, a point represents a field or intersection (in Go)
+ on the board. The class is a lightweight wrapper around an integer. All
+ information about points including the coordinates are contained in
+ Geometry. The convention for the coordinates is that the top left corner of
+ the board has the coordinates (0,0). Point::null() has the meaning
+ "no point".
+ @tparam M The maximum number of on-board points of all geometries this
+ point is used in (excluding the null point).
+ @tparam W The maximum width of all geometries this point is used in.
+ @tparam H The maximum height of all geometries this point is used in.
+ @tparam I An unsigned integer type to store the point value. */
+template<unsigned M, unsigned W, unsigned H, typename I>
+class Point
+{
+public:
+ typedef I IntType;
+
+ static const unsigned max_onboard = M;
+
+ static const unsigned max_width = W;
+
+ static const unsigned max_height = W;
+
+ static_assert(numeric_limits<I>::is_integer, "");
+
+ static_assert(! numeric_limits<I>::is_signed, "");
+
+ static_assert(max_onboard <= max_width * max_height, "");
+
+ static const unsigned range_onboard = max_onboard;
+
+ static const unsigned range = max_onboard + 1;
+
+ static Point null();
+
+ LIBBOARDGAME_FORCE_INLINE Point();
+
+ explicit Point(unsigned i);
+
+ bool operator==(const Point& p) const;
+
+ bool operator!=(const Point& p) const;
+
+ bool operator<(const Point& p) const;
+
+ bool is_null() const;
+
+ /** Return point as an integer between 0 and Point::range */
+ unsigned to_int() const;
+
+private:
+ static const IntType value_uninitialized = range;
+
+ static const IntType value_null = range - 1;
+
+ IntType m_i;
+
+ LIBBOARDGAME_FORCE_INLINE bool is_initialized() const;
+};
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline Point<M, W, H, I>::Point()
+{
+#if LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline Point<M, W, H, I>::Point(unsigned i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = static_cast<I>(i);
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::operator==(const Point& p) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(p.is_initialized());
+ return m_i == p.m_i;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::operator!=(const Point& p) const
+{
+ return ! operator==(p);
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::operator<(const Point& p) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(p.is_initialized());
+ return m_i < p.m_i;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::is_initialized() const
+{
+ return m_i < value_uninitialized;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline bool Point<M, W, H, I>::is_null() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i == value_null;
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline auto Point<M, W, H, I>::null() -> Point
+{
+ return Point(value_null);
+}
+
+template<unsigned M, unsigned W, unsigned H, typename I>
+inline unsigned Point<M, W, H, I>::to_int() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_POINT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/PointTransform.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_POINT_TRANSFORM_H
+#define LIBBOARDGAME_BASE_POINT_TRANSFORM_H
+
+#include <cmath>
+#include "Geometry.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** %Transform a point.
+ @tparam P An instance of class Point. */
+template<class P>
+class PointTransform
+{
+public:
+ typedef P Point;
+
+ virtual ~PointTransform();
+
+ virtual Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const = 0;
+};
+
+template<class P>
+PointTransform<P>::~PointTransform() = default;
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfIdent
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfIdent<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ LIBBOARDGAME_UNUSED(geo);
+ return p;
+}
+
+//-----------------------------------------------------------------------------
+
+/** Rotate point by 90 degrees. */
+template<class P>
+class PointTransfRot90
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot90<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ unsigned x = geo.get_width() - geo.get_y(p) - 1;
+ unsigned y = geo.get_x(p);
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+/** Rotate point by 180 degrees. */
+template<class P>
+class PointTransfRot180
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot180<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ unsigned x = geo.get_width() - geo.get_x(p) - 1;
+ unsigned y = geo.get_height() - geo.get_y(p) - 1;
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+/** Rotate point by 270 degrees. */
+template<class P>
+class PointTransfRot270
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot270<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ unsigned x = geo.get_y(p);
+ unsigned y = geo.get_height() - geo.get_x(p) - 1;
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+/** Rotate point by 270 degrees and reflect on y axis shifted to the center.
+ This is equivalent to a reflection on the x=y line. */
+template<class P>
+class PointTransfRot270Refl
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot270Refl<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ return geo.get_point(geo.get_y(p), geo.get_x(p));
+}
+
+//-----------------------------------------------------------------------------
+
+/** Rotate point by 90 degrees and reflect on y axis shifted to the center.
+ This is equivalent to a reflection on the x=width-y line. */
+template<class P>
+class PointTransfRot90Refl
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRot90Refl<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ unsigned x = geo.get_width() - geo.get_y(p) - 1;
+ unsigned y = geo.get_height() - geo.get_x(p) - 1;
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+/** Mirror along x axis. */
+template<class P>
+class PointTransfRefl
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfRefl<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ unsigned x = geo.get_width() - geo.get_x(p) - 1;
+ unsigned y = geo.get_y(p);
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+/** Mirror along y axis. */
+template<class P>
+class PointTransfReflRot180
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfReflRot180<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ unsigned x = geo.get_x(p);
+ unsigned y = geo.get_height() - geo.get_y(p) - 1;
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonRot60
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot60<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx + 0.5f * px + 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy - 0.5f * px + 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonRot120
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot120<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx - 0.5f * px + 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy - 0.5f * px - 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonRot240
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot240<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx - 0.5f * px - 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy + 0.5f * px - 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonRot300
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonRot300<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx + 0.5f * px - 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy + 0.5f * px + 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot60
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot60<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx + 0.5f * (-px) + 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy - 0.5f * (-px) + 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot120
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot120<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx - 0.5f * (-px) + 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy - 0.5f * (-px) - 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot240
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot240<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx - 0.5f * (-px) - 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy + 0.5f * (-px) - 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfTrigonReflRot300
+ : public PointTransform<P>
+{
+public:
+ typedef P Point;
+
+ Point get_transformed(const Point& p,
+ const Geometry<P>& geo) const override;
+};
+
+template<class P>
+P PointTransfTrigonReflRot300<P>::get_transformed(const Point& p,
+ const Geometry<P>& geo) const
+{
+ float cx = 0.5f * static_cast<float>(geo.get_width() - 1);
+ float cy = 0.5f * static_cast<float>(geo.get_height() - 1);
+ float px = static_cast<float>(geo.get_x(p)) - cx;
+ float py = static_cast<float>(geo.get_y(p)) - cy;
+ unsigned x = static_cast<unsigned>(round(cx + 0.5f * (-px) - 1.5f * py));
+ unsigned y = static_cast<unsigned>(round(cy + 0.5f * (-px) + 0.5f * py));
+ return geo.get_point(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_POINT_TRANSFORM_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Rating.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Rating.h"
+
+#include <cmath>
+#include <iostream>
+#include "libboardgame_util/Assert.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, const Rating& rating)
+{
+ out << rating.m_elo;
+ return out;
+}
+
+istream& operator>>(istream& in, Rating& rating)
+{
+ in >> rating.m_elo;
+ return in;
+}
+
+float Rating::get_expected_result(Rating elo_opponent,
+ unsigned nu_opponents) const
+{
+ float diff = elo_opponent.m_elo - m_elo;
+ return
+ 1.f
+ / (1.f + static_cast<float>(nu_opponents) * pow(10.f, diff / 400.f));
+}
+
+void Rating::update(float game_result, Rating elo_opponent, float k_value,
+ unsigned nu_opponents)
+{
+ LIBBOARDGAME_ASSERT(k_value > 0);
+ float diff = game_result - get_expected_result(elo_opponent, nu_opponents);
+ m_elo += k_value * diff;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Rating.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_RATING_H
+#define LIBBOARDGAME_BASE_RATING_H
+
+#include <cmath>
+#include <iosfwd>
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Elo-rating of a player. */
+class Rating
+{
+public:
+ friend ostream& operator<<(ostream& out, const Rating& rating);
+ friend istream& operator>>(istream& in, Rating& rating);
+
+ explicit Rating(float elo = 0);
+
+ /** Get the expected outcome of a game.
+ @param elo_opponent Elo-rating of the opponent.
+ @param nu_opponents The number of opponents (all with the same rating
+ elo_opponent) */
+ float get_expected_result(Rating elo_opponent,
+ unsigned nu_opponents = 1) const;
+
+ /** Update a rating after a game.
+ @param game_result The outcome of the game (0=loss, 0.5=tie, 1=win)
+ @param elo_opponent Elo-rating of the opponent.
+ @param k_value The K-value
+ @param nu_opponents The number of opponents (all with the same rating
+ elo_opponent) */
+ void update(float game_result, Rating elo_opponent, float k_value = 32,
+ unsigned nu_opponents = 1);
+
+ float get() const;
+
+ /** Get rating rounded to an integer. */
+ int to_int() const;
+
+private:
+ float m_elo;
+};
+
+inline Rating::Rating(float elo)
+ : m_elo(elo)
+{
+}
+
+inline float Rating::get() const
+{
+ return m_elo;
+}
+
+inline int Rating::to_int() const
+{
+ return static_cast<int>(round(m_elo));
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_RATING_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/RectGeometry.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_RECT_GEOMETRY_H
+#define LIBBOARDGAME_BASE_RECT_GEOMETRY_H
+
+#include <map>
+#include <memory>
+#include "Geometry.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Geometry of a regular rectangular grid.
+ @tparam P An instantiation of libboardgame_base::Point */
+template<class P>
+class RectGeometry final
+ : public Geometry<P>
+{
+public:
+ typedef P Point;
+
+ using AdjCoordList = typename Geometry<P>::AdjCoordList;
+ using DiagCoordList = typename Geometry<P>::DiagCoordList;
+ using AdjList = typename Geometry<P>::AdjList;
+ using DiagList = typename Geometry<P>::DiagList;
+
+ /** Create or reuse an already created geometry with a given size. */
+ static const RectGeometry& get(unsigned width, unsigned height);
+
+ RectGeometry(unsigned width, unsigned height);
+
+ AdjCoordList get_adj_coord(int x, int y) const override;
+
+ DiagCoordList get_diag_coord(int x, int y) const override;
+
+ unsigned get_point_type(int x, int y) const override;
+
+ unsigned get_period_x() const override;
+
+ unsigned get_period_y() const override;
+
+protected:
+ bool init_is_onboard(unsigned x, unsigned y) const override;
+
+private:
+ /** Stores already created geometries by width and height. */
+ static map<pair<unsigned, unsigned>, shared_ptr<RectGeometry>> s_geometry;
+};
+
+template<class P>
+map<pair<unsigned, unsigned>, shared_ptr<RectGeometry<P>>>
+ RectGeometry<P>::s_geometry;
+
+template<class P>
+RectGeometry<P>::RectGeometry(unsigned width, unsigned height)
+{
+ Geometry<P>::init(width, height);
+}
+
+template<class P>
+const RectGeometry<P>& RectGeometry<P>::get(unsigned width, unsigned height)
+{
+ auto key = make_pair(width, height);
+ auto pos = s_geometry.find(key);
+ if (pos != s_geometry.end())
+ return *pos->second;
+ auto geometry = make_shared<RectGeometry>(width, height);
+ return *s_geometry.insert(make_pair(key, geometry)).first->second;
+}
+
+template<class P>
+auto RectGeometry<P>::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+ AdjCoordList l;
+ l.push_back(CoordPoint(x, y - 1));
+ l.push_back(CoordPoint(x - 1, y));
+ l.push_back(CoordPoint(x + 1, y));
+ l.push_back(CoordPoint(x, y + 1));
+ return l;
+}
+
+template<class P>
+auto RectGeometry<P>::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+ // The order does not matter logically but it is better to put far away
+ // points first because in Blokus, libpentobi::BoardConst uses the
+ // forbidden status of the first points during move generation and far away
+ // points can reject more moves.
+ DiagCoordList l;
+ l.push_back(CoordPoint(x - 1, y - 1));
+ l.push_back(CoordPoint(x + 1, y + 1));
+ l.push_back(CoordPoint(x + 1, y - 1));
+ l.push_back(CoordPoint(x - 1, y + 1));
+ return l;
+}
+
+template<class P>
+unsigned RectGeometry<P>::get_period_x() const
+{
+ return 1;
+}
+
+template<class P>
+unsigned RectGeometry<P>::get_period_y() const
+{
+ return 1;
+}
+
+template<class P>
+unsigned RectGeometry<P>::get_point_type(int x, int y) const
+{
+ LIBBOARDGAME_UNUSED(x);
+ LIBBOARDGAME_UNUSED(y);
+ return 0;
+}
+
+template<class P>
+bool RectGeometry<P>::init_is_onboard(unsigned x, unsigned y) const
+{
+ LIBBOARDGAME_UNUSED(x);
+ LIBBOARDGAME_UNUSED(y);
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_RECT_GEOMETRY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/RectTransform.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "RectTransform.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfIdentity::get_transformed(const CoordPoint& p) const
+{
+ return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot90::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(-p.y, p.x);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot180::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(-p.x, -p.y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot270::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(p.y, -p.x);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRefl::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(-p.x, p.y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot90Refl::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(-p.y, -p.x);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot180Refl::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(p.x, -p.y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot270Refl::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(p.y, p.x);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/RectTransform.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_RECTTRANSFORM_H
+#define LIBBOARDGAME_BASE_RECTTRANSFORM_H
+
+#include "Transform.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+class TransfIdentity
+ : public Transform
+{
+public:
+ TransfIdentity() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot90
+ : public Transform
+{
+public:
+ TransfRectRot90() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot180
+ : public Transform
+{
+public:
+ TransfRectRot180() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot270
+ : public Transform
+{
+public:
+ TransfRectRot270() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRefl
+ : public Transform
+{
+public:
+ TransfRectRefl() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot90Refl
+ : public Transform
+{
+public:
+ TransfRectRot90Refl() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot180Refl
+ : public Transform
+{
+public:
+ TransfRectRot180Refl() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot270Refl
+ : public Transform
+{
+public:
+ TransfRectRot270Refl() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_TRANSFORM_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/StringRep.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "StringRep.h"
+
+#include <cstdio>
+#include <iostream>
+#include "libboardgame_util/StringUtil.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libboardgame_base {
+
+using libboardgame_util::get_letter_coord;
+
+//-----------------------------------------------------------------------------
+
+StringRep::~StringRep() = default;
+
+//-----------------------------------------------------------------------------
+
+StdStringRep::~StdStringRep() = default;
+
+bool StdStringRep::read(istream& in, unsigned width, unsigned height,
+ unsigned& x, unsigned& y) const
+{
+ int c;
+ while (true)
+ {
+ c = in.peek();
+ if (c == EOF || ! isspace(c))
+ break;
+ in.get();
+ }
+ bool read_x = false;
+ x = 0;
+ while (true)
+ {
+ c = in.peek();
+ if (c == EOF || ! isalpha(c))
+ break;
+ c = tolower(in.get());
+ if (c < 'a' || c > 'z')
+ return false;
+ x = 26 * x + (c - 'a' + 1);
+ read_x = true;
+ }
+ if (! read_x)
+ return false;
+ --x;
+ if (x >= width)
+ return false;
+ c = in.peek();
+ if (c < '0' || c > '9')
+ return false;
+ in >> y;
+ if (! in || y > height + 1)
+ return false;
+ y = height - y;
+ c = in.peek();
+ if (c == EOF)
+ {
+ in.clear();
+ return true;
+ }
+ if (isspace(c))
+ return true;
+ return false;
+}
+
+void StdStringRep::write(ostream& out, unsigned x, unsigned y, unsigned width,
+ unsigned height) const
+{
+ LIBBOARDGAME_UNUSED(width);
+ out << get_letter_coord(x) << (height - y);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/StringRep.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_STRING_REP_H
+#define LIBBOARDGAME_BASE_STRING_REP_H
+
+#include <iosfwd>
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** String representation of points. */
+struct StringRep
+{
+ virtual ~StringRep();
+
+ virtual bool read(istream& in, unsigned width, unsigned height,
+ unsigned& x, unsigned& y) const = 0;
+
+ virtual void write(ostream& out, unsigned x, unsigned y, unsigned width,
+ unsigned height) const = 0;
+};
+
+//-----------------------------------------------------------------------------
+
+/** Spreadsheet-style string representation of points.
+ Can be used as a template argument for libboardgame_base::Point.
+ Columns are represented as letters including the letter 'J'. After 'Z',
+ multi-letter combinations are used: 'AA', 'AB', etc. Rows are represented
+ by numbers starting with '1'. Note that unlike in spreadsheets, row number
+ 1 is at the bottom and increases to the top to be compatible with the
+ convention used in chess. */
+struct StdStringRep
+ : public StringRep
+{
+ ~StdStringRep();
+
+ bool read(istream& in, unsigned width, unsigned height, unsigned& x,
+ unsigned& y) const override;
+
+ void write(ostream& out, unsigned x, unsigned y, unsigned width,
+ unsigned height) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_STRING_REP_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Transform.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Transform.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+Transform::~Transform()
+{
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/Transform.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_BASE_TRANSFORM_H
+#define LIBBOARDGAME_BASE_TRANSFORM_H
+
+#include "CoordPoint.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+/** Rotation and/or reflection of local coordinates on the board. */
+class Transform
+{
+public:
+ virtual ~Transform();
+
+ virtual CoordPoint get_transformed(const CoordPoint& p) const = 0;
+
+ /** Get the new point type of the (0,0) coordinates.
+ The transformation may change the point type of the (0,0) coordinates.
+ For example, in the Blokus Trigon board, a reflection at the y axis
+ changes the type from 0 (=downside triangle) to 1 (=upside triangle).
+ @see Geometry::get_point_type() */
+ unsigned get_new_point_type() const { return m_new_point_type; }
+
+ /** @tparam I An iterator of a container with elements of type CoordPoint */
+ template<class I>
+ void transform(I begin, I end) const;
+
+protected:
+ explicit Transform(unsigned new_point_type)
+ : m_new_point_type(new_point_type)
+ {}
+
+private:
+ unsigned m_new_point_type;
+};
+
+template<class I>
+void Transform::transform(I begin, I end) const
+{
+ for (I i = begin; i != end; ++i)
+ *i = get_transformed(*i);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_base
+
+#endif // LIBBOARDGAME_BASE_TRANSFORM_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Arguments.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Arguments.h"
+
+#include <cctype>
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+void Arguments::check_size(unsigned n) const
+{
+ if (get_size() == n)
+ return;
+ if (n == 0)
+ throw Failure("no arguments allowed");
+ else if (n == 1)
+ throw Failure("command needs one argument");
+ else
+ {
+ ostringstream msg;
+ msg << "command needs " << n << " arguments";
+ throw Failure(msg.str());
+ }
+}
+
+void Arguments::check_size_less_equal(unsigned n) const
+{
+ if (get_size() <= n)
+ return;
+ if (n == 1)
+ throw Failure("command needs at most one argument");
+ else
+ {
+ ostringstream msg;
+ msg << "command needs at most " << n << " arguments";
+ throw Failure(msg.str());
+ }
+}
+
+CmdLineRange Arguments::get(unsigned i) const
+{
+ if (i < get_size())
+ return m_line.get_element(m_line.get_idx_name() + i + 1);
+ ostringstream msg;
+ msg << "missing argument " << (i + 1);
+ throw Failure(msg.str());
+}
+
+string Arguments::get_tolower(unsigned i) const
+{
+ string value = get(i);
+ for (auto& c : value)
+ c = static_cast<char>(tolower(c));
+ return value;
+}
+
+string Arguments::get_tolower() const
+{
+ check_size(1);
+ return get_tolower(0);
+}
+
+CmdLineRange Arguments::get_remaining_line(unsigned i) const
+{
+ if (i < get_size())
+ return m_line.get_trimmed_line_after_elem(m_line.get_idx_name() + i
+ + 1);
+ ostringstream msg;
+ msg << "missing argument " << (i + 1);
+ throw Failure(msg.str());
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Arguments.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_GTP_ARGUMENTS_H
+#define LIBBOARDGAME_GTP_ARGUMENTS_H
+
+#ifdef __GNUC__
+#include <cxxabi.h>
+#endif
+#include <sstream>
+#include "CmdLine.h"
+#include "Failure.h"
+
+namespace libboardgame_gtp {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Access arguments of command line. */
+class Arguments
+{
+public:
+ /** Constructor.
+ @param line The command line (@ref libboardgame_doc_storesref) */
+ explicit Arguments(const CmdLine& line);
+
+ /** Get argument.
+ @param i Argument index starting with 0
+ @return Argument value
+ @throws Failure If no such argument */
+ CmdLineRange get(unsigned i) const;
+
+ /** Get single argument.
+ @return Argument value
+ @throws Failure If no such argument or command has more than one
+ arguments */
+ CmdLineRange get() const;
+
+ /** Get argument converted to lowercase.
+ @param i Argument index starting with 0
+ @return Copy of argument value converted to lowercase
+ @throws Failure If no such argument */
+ string get_tolower(unsigned i) const;
+
+ /** Get single argument converted to lowercase. */
+ string get_tolower() const;
+
+ /** Get argument converted to a type.
+ The type must implement operator<<(istream)
+ @param i Argument index starting with 0
+ @return The converted argument
+ @throws Failure If no such argument, or argument cannot be converted */
+ template<typename T>
+ T parse(unsigned i) const;
+
+ /** Get single argument converted to a type.
+ The type must implement operator<<(istream)
+ @return The converted argument
+ @throws Failure If no such argument, or argument cannot be converted,
+ or command has more than one arguments */
+ template<typename T>
+ T parse() const;
+
+ /** Get argument converted to a type and check against a minum value.
+ The type must implement operator<< and operator<
+ @param i Argument index starting with 0
+ @param min Minimum allowed value
+ @return Argument value
+ @throws Failure If no such argument, argument cannot be converted
+ or smaller than the mimimum value */
+ template<typename T>
+ T parse_min(unsigned i, T min) const;
+
+ /** Get argument converted to a type and check against a range.
+ The type must implement operator<< and operator<
+ @param i Argument index starting with 0
+ @param min Minimum allowed value
+ @param max Maximum allowed value
+ @return Argument value
+ @throws Failure If no such argument, argument cannot be converted
+ or not in range */
+ template<typename T>
+ T parse_min_max(unsigned i, T min, T max) const;
+
+ template<typename T>
+ T parse_min_max(T min, T max) const;
+
+ /** Check that command has no arguments.
+ @throws Failure If command has arguments
+ */
+ void check_empty() const;
+
+ /** Check number of arguments.
+ @param n Expected number of arguments
+ @throws Failure If command has a different number of arguments */
+ void check_size(unsigned n) const;
+
+ /** Check maximum number of arguments.
+ @param n Expected maximum number of arguments
+ @throws Failure If command has more arguments */
+ void check_size_less_equal(unsigned n) const;
+
+ /** Get argument line.
+ Get all arguments as a line.
+ No modfications to the line were made apart from trimmimg leading
+ and trailing white spaces. */
+ CmdLineRange get_line() const;
+
+ /** Get number of arguments. */
+ unsigned get_size() const;
+
+ /** Return remaining line after argument.
+ @param i Argument index starting with 0
+ @return The remaining line after the given argument, unmodified apart
+ from leading and trailing whitespaces, which are trimmed. Quotation
+ marks are not handled.
+ @throws Failure If no such argument */
+ CmdLineRange get_remaining_line(unsigned i) const;
+
+private:
+ const CmdLine& m_line;
+
+ template<typename T>
+ static string get_type_name();
+};
+
+inline Arguments::Arguments(const CmdLine& line)
+ : m_line(line)
+{
+}
+
+inline void Arguments::check_empty() const
+{
+ check_size(0);
+}
+
+inline CmdLineRange Arguments::get() const
+{
+ check_size(1);
+ return get(0);
+}
+
+inline CmdLineRange Arguments::get_line() const
+{
+ return m_line.get_trimmed_line_after_elem(m_line.get_idx_name());
+}
+
+inline unsigned Arguments::get_size() const
+{
+ return
+ static_cast<unsigned>(m_line.get_elements().size())
+ - m_line.get_idx_name() - 1;
+}
+
+template<typename T>
+string Arguments::get_type_name()
+{
+#ifdef __GNUC__
+ int status;
+ auto name_ptr =
+ abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, &status);
+ if (status == 0)
+ {
+ string result(name_ptr);
+ free(name_ptr);
+ return result;
+ }
+ else
+ return typeid(T).name();
+#else
+ return typeid(T).name();
+#endif
+}
+
+template<typename T>
+T Arguments::parse() const
+{
+ check_size(1);
+ return parse<T>(0);
+}
+
+template<typename T>
+T Arguments::parse(unsigned i) const
+{
+ string s = get(i);
+ istringstream in(s);
+ T result;
+ in >> result;
+ if (! in)
+ {
+ ostringstream msg;
+ msg << "argument " << (i + 1) << " ('" << s
+ << "') has invalid type (expected " << get_type_name<T>() << ")";
+ throw Failure(msg.str());
+ }
+ return result;
+}
+
+template<typename T>
+T Arguments::parse_min(unsigned i, T min) const
+{
+ T result = parse<T>(i);
+ if (result < min)
+ {
+ ostringstream msg;
+ msg << "argument " << (i + 1) << " must be greater or equal " << min;
+ throw Failure(msg.str());
+ }
+ return result;
+}
+
+template<typename T>
+T Arguments::parse_min_max(T min, T max) const
+{
+ check_size(1);
+ return parse_min_max<T>(0, min, max);
+}
+
+template<typename T>
+T Arguments::parse_min_max(unsigned i, T min, T max) const
+{
+ T result = parse_min(i, min);
+ if (max < result)
+ {
+ ostringstream msg;
+ msg << "argument " << (i + 1) << " must be less or equal " << max;
+ throw Failure(msg.str());
+ }
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_ARGUMENTS_H
--- /dev/null
+add_library(boardgame_gtp STATIC
+ Arguments.h
+ Arguments.cpp
+ CmdLine.h
+ CmdLine.cpp
+ CmdLineRange.h
+ Engine.h
+ Engine.cpp
+ Failure.h
+ Response.h
+ Response.cpp
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/CmdLine.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "CmdLine.h"
+
+#include <cassert>
+#include <limits>
+#include <sstream>
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+CmdLine::~CmdLine() = default;
+
+void CmdLine::add_elem(string::const_iterator begin,
+ string::const_iterator end)
+{
+ // Ignore command line elements greater UINT_MAX because we use unsigned
+ // for element indices.
+ if (m_elem.size() < numeric_limits<unsigned>::max())
+ m_elem.emplace_back(begin, end);
+}
+
+/** Find elements (ID, command name, arguments).
+ Arguments are words separated by whitespaces.
+ Arguments with whitespaces can be quoted with quotation marks ('"').
+ Characters can be escaped with a backslash ('\'). */
+void CmdLine::find_elem()
+{
+ m_elem.clear();
+ bool escape = false;
+ bool is_in_string = false;
+ string::const_iterator begin = m_line.begin();
+ string::const_iterator i;
+ for (i = begin; i < m_line.end(); ++i)
+ {
+ char c = *i;
+ if (c == '"' && ! escape)
+ {
+ if (is_in_string)
+ add_elem(begin, i);
+ begin = i + 1;
+ is_in_string = ! is_in_string;
+ }
+ else if (isspace(static_cast<unsigned char>(c)) && ! is_in_string)
+ {
+ if (i > begin)
+ m_elem.emplace_back(begin, i);
+ begin = i + 1;
+ }
+ escape = (c == '\\' && ! escape);
+ }
+ if (i > begin)
+ m_elem.emplace_back(begin, m_line.end());
+}
+
+CmdLineRange CmdLine::get_trimmed_line_after_elem(unsigned i) const
+{
+ assert(i < m_elem.size());
+ auto& e = m_elem[i];
+ auto begin = e.end();
+ if (begin < m_line.end() && *begin == '"')
+ ++begin;
+ while (begin < m_line.end() && isspace(static_cast<unsigned char>(*begin)))
+ ++begin;
+ auto end = m_line.end();
+ while (end > begin && isspace(static_cast<unsigned char>(*(end - 1))))
+ --end;
+ return CmdLineRange(begin, end);
+}
+
+void CmdLine::init(const string& line)
+{
+ m_line = line;
+ find_elem();
+ assert(! m_elem.empty());
+ parse_id();
+ assert(! m_elem.empty());
+}
+
+void CmdLine::init(const CmdLine& c)
+{
+ m_idx_name = c.m_idx_name;
+ m_line = c.m_line;
+ m_elem.clear();
+ for (auto& i : c.m_elem)
+ {
+ auto begin = m_line.begin() + (i.begin() - c.m_line.begin());
+ auto end = m_line.begin() + (i.end() - c.m_line.begin());
+ m_elem.emplace_back(begin, end);
+ }
+}
+
+void CmdLine::parse_id()
+{
+ m_idx_name = 0;
+ if (m_elem.size() < 2)
+ return;
+ istringstream in(m_elem[0]);
+ int id;
+ in >> id;
+ if (in)
+ m_idx_name = 1;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/CmdLine.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_GTP_CMDLINE_H
+#define LIBBOARDGAME_GTP_CMDLINE_H
+
+#include <algorithm>
+#include <cassert>
+#include <string>
+#include <iterator>
+#include <vector>
+#include "CmdLineRange.h"
+
+namespace libboardgame_gtp {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Parsed GTP command line.
+ Only used internally by libboardgame_gtp::Engine. GTP command handlers
+ query arguments of the command line through the instance of class Arguments
+ given as a function argument by class Engine to the command handler. */
+class CmdLine
+{
+public:
+ /** Construct empty command.
+ @warning An empty command cannot be used, before init() was called.
+ This constructor exists only to reuse instances. */
+ CmdLine() = default;
+
+ /** Construct with a command line.
+ @see init() */
+ explicit CmdLine(const string& line);
+
+ ~CmdLine();
+
+ void init(const string& line);
+
+ void init(const CmdLine& c);
+
+ const string& get_line() const;
+
+ /** Get command name. */
+ CmdLineRange get_name() const;
+
+ void write_id(ostream& out) const;
+
+ CmdLineRange get_trimmed_line_after_elem(unsigned i) const;
+
+ const vector<CmdLineRange>& get_elements() const;
+
+ const CmdLineRange& get_element(unsigned i) const;
+
+ int get_idx_name() const;
+
+private:
+ int m_idx_name;
+
+ /** Full command line. */
+ string m_line;
+
+ vector<CmdLineRange> m_elem;
+
+ void add_elem(string::const_iterator begin, string::const_iterator end);
+
+ void find_elem();
+
+ void parse_id();
+};
+
+inline CmdLine::CmdLine(const string& line)
+{
+ init(line);
+}
+
+inline const vector<CmdLineRange>& CmdLine::get_elements() const
+{
+ return m_elem;
+}
+
+inline const CmdLineRange& CmdLine::get_element(unsigned i) const
+{
+ assert(i < m_elem.size());
+ return m_elem[i];
+}
+
+inline int CmdLine::get_idx_name() const
+{
+ return m_idx_name;
+}
+
+inline const string& CmdLine::get_line() const
+{
+ return m_line;
+}
+
+inline CmdLineRange CmdLine::get_name() const
+{
+ return m_elem[m_idx_name];
+}
+
+inline void CmdLine::write_id(ostream& out) const
+{
+ if (m_idx_name == 0)
+ return;
+ auto& e = m_elem[0];
+ copy(e.begin(), e.end(), ostream_iterator<char>(out));
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_CMDLINE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/CmdLineRange.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_GTP_CMDLINERANGE_H
+#define LIBBOARDGAME_GTP_CMDLINERANGE_H
+
+#include <iosfwd>
+#include <algorithm>
+#include <string>
+
+namespace libboardgame_gtp {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Subrange of the GTP command line.
+ Avoids allocation of strings on the heap for each parsed command line.
+ Instances of this class are valid only during the lifetime of the command
+ line object. Command handlers, which access the command line through the
+ instance of Arguments given as a function argument, should not store
+ references to CmdLineRange objects. */
+struct CmdLineRange
+{
+ string::const_iterator m_begin;
+
+ string::const_iterator m_end;
+
+
+ CmdLineRange(string::const_iterator begin, string::const_iterator end)
+ : m_begin(begin),
+ m_end(end)
+ { }
+
+ bool operator==(const string& s) const
+ {
+ return size() == s.size() && equal(m_begin, m_end, s.begin());
+ }
+
+ bool operator!=(const string& s) const
+ {
+ return ! operator==(s);
+ }
+
+ operator string() const
+ {
+ return string(m_begin, m_end);
+ }
+
+ string::const_iterator begin() const
+ {
+ return m_begin;
+ }
+
+ string::const_iterator end() const
+ {
+ return m_end;
+ }
+
+ string::size_type size() const
+ {
+ return m_end - m_begin;
+ }
+
+ void write(ostream& o) const
+ {
+ o << string(*this);
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+inline ostream& operator<<(ostream& out, const CmdLineRange& r)
+{
+ r.write(out);
+ return out;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_CMDLINERANGE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Engine.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Engine.h"
+
+#include <cctype>
+#include <iostream>
+#include "CmdLine.h"
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+/** Utility functions. */
+namespace {
+
+/** Check, if line contains a command. */
+bool is_cmd_line(const string& line)
+{
+ for (char c : line)
+ if (! isspace(static_cast<unsigned char>(c)))
+ return c != '#';
+ return false;
+}
+
+/** Read next command from stream.
+ @param in The input stream.
+ @param[out] c The command (reused for efficiency)
+ @return @c false on end-of-stream or read error. */
+bool read_cmd(CmdLine& c, istream& in)
+{
+ string line;
+ while (getline(in, line))
+ if (is_cmd_line(line))
+ break;
+ if (! in.fail())
+ {
+ c.init(line);
+ return true;
+ }
+ else
+ return false;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+Engine::Engine()
+{
+ add("known_command", &Engine::cmd_known_command);
+ add("list_commands", &Engine::cmd_list_commands);
+ add("name", &Engine::cmd_name);
+ add("protocol_version", &Engine::cmd_protocol_version);
+ add("quit", &Engine::cmd_quit);
+ add("version", &Engine::cmd_version);
+}
+
+Engine::~Engine() = default;
+
+void Engine::add(const string& name, Handler f)
+{
+ m_handlers[name] = f;
+}
+
+void Engine::add(const string& name, HandlerNoArgs f)
+{
+ add(name,
+ Handler(bind(no_args_wrapper, f, placeholders::_1, placeholders::_2)));
+}
+
+void Engine::add(const string& name, HandlerNoResponse f)
+{
+ add(name, Handler(bind(no_response_wrapper, f,
+ placeholders::_1, placeholders::_2)));
+}
+
+void Engine::add(const string& name, HandlerNoArgsNoResponse f)
+{
+ add(name, Handler(bind(no_args_no_response_wrapper, f,
+ placeholders::_1, placeholders::_2)));
+}
+
+/** Return @c true if command is known, @c false otherwise. */
+void Engine::cmd_known_command(const Arguments& args, Response& response)
+{
+ response.set(contains(args.get()) ? "true" : "false");
+}
+
+/** List all known commands. */
+void Engine::cmd_list_commands(Response& response)
+{
+ for (auto& i : m_handlers)
+ response << i.first << '\n';
+}
+
+/** Return name. */
+void Engine::cmd_name(Response& response)
+{
+ response.set("Unknown");
+}
+
+/** Return protocol version. */
+void Engine::cmd_protocol_version(Response& response)
+{
+ response.set("2");
+}
+
+/** Quit command loop. */
+void Engine::cmd_quit()
+{
+ m_quit = true;
+}
+
+/** Return empty version string.
+ The GTP standard says to return empty string, if no meaningful response
+ is available. */
+void Engine::cmd_version(Response&)
+{
+}
+
+bool Engine::contains(const string& name) const
+{
+ return m_handlers.count(name) > 0;
+}
+
+bool Engine::exec(istream& in, bool throw_on_fail, ostream* log)
+{
+ string line;
+ Response response;
+ string buffer;
+ CmdLine cmd;
+ while (getline(in, line))
+ {
+ if (! is_cmd_line(line))
+ continue;
+ cmd.init(line);
+ if (log)
+ *log << cmd.get_line() << '\n';
+ bool status = handle_cmd(cmd, log, response, buffer);
+ if (! status && throw_on_fail)
+ {
+ ostringstream msg;
+ msg << "executing '" << cmd.get_line() << "' failed";
+ throw Failure(msg.str());
+ }
+ }
+ return ! in.fail();
+}
+
+void Engine::exec_main_loop(istream& in, ostream& out)
+{
+ m_quit = false;
+ CmdLine cmd;
+ Response response;
+ string buffer;
+ while (! m_quit)
+ {
+ if (read_cmd(cmd, in))
+ handle_cmd(cmd, &out, response, buffer);
+ else
+ break;
+ }
+}
+
+/** Call the handler of a command and write its response.
+ @param line The command
+ @param out The output stream for the response
+ @param response A reusable response instance to avoid memory allocation in
+ each function call
+ @param buffer A reusable string instance to avoid memory allocation in each
+ function call */
+bool Engine::handle_cmd(CmdLine& line, ostream* out, Response& response,
+ string& buffer)
+{
+ on_handle_cmd_begin();
+ bool status = true;
+ try
+ {
+ response.clear();
+ auto pos = m_handlers.find(line.get_name());
+ if (pos != m_handlers.end())
+ {
+ Arguments args(line);
+ (pos->second)(args, response);
+ }
+ else
+ {
+ status = false;
+ response << "unknown command (" << line.get_name() << ')';
+ }
+ }
+ catch (const Failure& failure)
+ {
+ status = false;
+ response.set(failure.what());
+ }
+ if (out)
+ {
+ *out << (status ? '=' : '?');
+ line.write_id(*out);
+ *out << ' ';
+ response.write(*out, buffer);
+ out->flush();
+ }
+ return status;
+}
+
+void Engine::no_args_wrapper(HandlerNoArgs h, const Arguments& args,
+ Response& response)
+{
+ args.check_empty();
+ h(response);
+}
+
+void Engine::no_response_wrapper(HandlerNoResponse h, const Arguments& args,
+ Response&)
+{
+ h(args);
+}
+
+void Engine::no_args_no_response_wrapper(HandlerNoArgsNoResponse h,
+ const Arguments& args, Response&)
+{
+ args.check_empty();
+ h();
+}
+
+void Engine::on_handle_cmd_begin()
+{
+ // Default implementation does nothing
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Engine.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_GTP_ENGINE_H
+#define LIBBOARDGAME_GTP_ENGINE_H
+
+#include <functional>
+#include <iosfwd>
+#include <map>
+#include "Arguments.h"
+#include "Response.h"
+
+namespace libboardgame_gtp {
+
+class CmdLine;
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Base class for GTP engines.
+ Commands can be added with Engine::add(). Existing commands can be
+ overridden by registering a new handler for the command.
+ @see @ref libboardgame_gtp_commands */
+class Engine
+{
+public:
+ typedef function<void(const Arguments&, Response&)> Handler;
+
+ typedef function<void(Response&)> HandlerNoArgs;
+
+ typedef function<void(const Arguments&)> HandlerNoResponse;
+
+ typedef function<void()> HandlerNoArgsNoResponse;
+
+ /** @page libboardgame_gtp_commands libboardgame_gtp::Engine GTP commands
+ <dl>
+ <dt>@link cmd_known_command() @c known_command @endlink</dt>
+ <dd>@copydoc cmd_known_command() </dd>
+ <dt>@link cmd_list_commands() @c list_commands @endlink</dt>
+ <dd>@copydoc cmd_list_commands() </dd>
+ <dt>@link cmd_name() @c name @endlink</dt>
+ <dd>@copydoc cmd_name() </dd>
+ <dt>@link cmd_protocol_version() @c protocol_version @endlink</dt>
+ <dd>@copydoc cmd_protocol_version() </dd>
+ <dt>@link cmd_quit() @c quit @endlink</dt>
+ <dd>@copydoc cmd_quit() </dd>
+ <dt>@link cmd_version() @c version @endlink</dt>
+ <dd>@copydoc cmd_version() </dd>
+ </dl> */
+ /** @name Command handlers */
+ /** @{ */
+ void cmd_known_command(const Arguments&, Response&);
+ void cmd_list_commands(Response&);
+ void cmd_name(Response&);
+ void cmd_protocol_version(Response&);
+ void cmd_quit();
+ void cmd_version(Response&);
+ /** @} */ // @name
+
+ Engine();
+
+ Engine(const Engine&) = delete;
+
+ Engine& operator=(const Engine&) const = delete;
+
+ virtual ~Engine();
+
+ /** Execute commands from an input stream.
+ @param in The input stream
+ @param throw_on_fail Whether to throw an exception if a command fails,
+ or to continue executing the remainign commands
+ @param log Stream for logging the commands and responses to.
+ @return The stream state as a bool
+ @throws Failure If a command fails, and @c throw_on_fail is @c true */
+ bool exec(istream& in, bool throw_on_fail, ostream* log);
+
+ /** Run the main command loop.
+ Reads lines from input stream, calls the corresponding command handler
+ and writes the response to the output stream. Empty lines in the
+ command responses will be replaced by a line containing a single space,
+ because empty lines are not allowed in GTP responses. */
+ void exec_main_loop(istream& in, ostream& out);
+
+ /** Register command handler.
+ If a command was already registered with the same name, it will be
+ replaced by the new command. */
+ void add(const string& name, Handler f);
+
+ void add(const string& name, HandlerNoArgs f);
+
+ void add(const string& name, HandlerNoResponse f);
+
+ void add(const string& name, HandlerNoArgsNoResponse f);
+
+ /** Register a member function as a command handler.
+ If a command was already registered with the same name, it will be
+ replaced by the new command. */
+ template<class T>
+ void add(const string& name,
+ void (T::*f)(const Arguments&, Response&), T* t);
+
+ template<class T>
+ void add(const string& name, void (T::*f)(const Arguments&), T* t);
+
+ template<class T>
+ void add(const string& name, void (T::*f)(Response&), T* t);
+
+ template<class T>
+ void add(const string& name, void (T::*f)(), T* t);
+
+ /** Returns if command registered. */
+ bool contains(const string& name) const;
+
+protected:
+ /** Hook function to be executed before each command.
+ The default implementation does nothing. */
+ virtual void on_handle_cmd_begin();
+
+ /** Register a member function of the current instance as a command
+ handler.
+ If a command was already registered with the same name, it will be
+ replaced by the new command. */
+ template<class T>
+ void add(const string& name, void (T::*f)(const Arguments&, Response&));
+
+ template<class T>
+ void add(const string& name, void (T::*f)(const Arguments&));
+
+ template<class T>
+ void add(const string& name, void (T::*f)(Response&));
+
+ template<class T>
+ void add(const string& name, void (T::*f)());
+
+private:
+ /** Mapping of command name to command handler.
+ They key is a string subrange, not a string, to allow looking up the
+ command name using Command::name_as_subrange() without creating a
+ temporary string for the command name. The value of type CmdInfo with
+ the name string and callback function are stored in an object allocated
+ on the heap to ensure that the range stays valid, if the value object
+ is copied. */
+ typedef map<string, Handler> Handlers;
+
+
+ /** Flag to quit main loop. */
+ bool m_quit;
+
+ Handlers m_handlers;
+
+
+ bool handle_cmd(CmdLine& line, ostream* out, Response& response,
+ string& buffer);
+
+ static void no_args_wrapper(HandlerNoArgs h,
+ const Arguments& args, Response& response);
+
+ static void no_response_wrapper(HandlerNoResponse h,
+ const Arguments& args, Response&);
+
+ static void no_args_no_response_wrapper(HandlerNoArgsNoResponse h,
+ const Arguments& args, Response&);
+};
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(const Arguments&, Response&))
+{
+ add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(Response&))
+{
+ add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(const Arguments&))
+{
+ add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)())
+{
+ add(name, f, dynamic_cast<T*>(this));
+}
+
+template<class T>
+void Engine::add(const string& name,
+ void (T::*f)(const Arguments&, Response&), T* t)
+{
+ assert(f);
+ add(name,
+ static_cast<Handler>(bind(f, t, placeholders::_1, placeholders::_2)));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(Response&), T* t)
+{
+ assert(f);
+ add(name, static_cast<HandlerNoArgs>(bind(f, t, placeholders::_1)));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(const Arguments&), T* t)
+{
+ assert(f);
+ add(name, static_cast<HandlerNoResponse>(bind(f, t, placeholders::_1)));
+}
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(), T* t)
+{
+ assert(f);
+ add(name, static_cast<HandlerNoArgsNoResponse>(bind(f, t)));
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_ENGINE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Failure.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_GTP_FAILURE_H
+#define LIBBOARDGAME_GTP_FAILURE_H
+
+#include <stdexcept>
+
+namespace libboardgame_gtp {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** GTP failure.
+ Command handlers generate a GTP error response by throwing an instance
+ of Failure. */
+class Failure
+ : public runtime_error
+{
+ using runtime_error::runtime_error;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_ENGINE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Response.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Response.h"
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+ostringstream Response::s_dummy;
+
+Response::~Response() = default;
+
+void Response::write(ostream& out, string& buffer) const
+{
+ buffer = m_stream.str();
+ bool was_newline = false;
+ for (auto c : buffer)
+ {
+ bool is_newline = (c == '\n');
+ if (is_newline && was_newline)
+ out << ' ';
+ out << c;
+ was_newline = is_newline;
+ }
+ if (! was_newline)
+ out << '\n';
+ out << '\n';
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Response.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_GTP_RESPONSE_H
+#define LIBBOARDGAME_GTP_RESPONSE_H
+
+#include <sstream>
+#include <string>
+
+namespace libboardgame_gtp {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class Response
+{
+public:
+ ~Response();
+
+ /** Conversion to output stream.
+ Returns reference to response stream. */
+ operator ostream&();
+
+ /** Get response.
+ @return A copy of the internal response string stream */
+ string to_string() const;
+
+ /** Set response. */
+ void set(const string& response);
+
+ void clear();
+
+ /** Write response to output stream.
+ Also sanitizes responses containing empty lines ("\n\n" cannot occur
+ in a response, because it means end of response; it will be replaced by
+ "\n \n") and adds "\n\n" add the end of the response. */
+ void write(ostream& out, string& buffer) const;
+
+private:
+ /** Dummy stream for copying default formatting settings. */
+ static ostringstream s_dummy;
+
+ /** Response stream */
+ ostringstream m_stream;
+};
+
+inline Response::operator ostream&()
+{
+ return m_stream;
+}
+
+inline void Response::clear()
+{
+ m_stream.str("");
+ m_stream.copyfmt(s_dummy);
+}
+
+inline string Response::to_string() const
+{
+ return m_stream.str();
+}
+
+inline void Response::set(const string& response)
+{
+ m_stream.str(response);
+}
+
+//-----------------------------------------------------------------------------
+
+/** @relates libboardgame_gtp::Response */
+template<typename TYPE>
+inline Response& operator<<(Response& r, const TYPE& t)
+{
+ static_cast<ostream&>(r) << t;
+ return r;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_gtp
+
+#endif // LIBBOARDGAME_GTP_RESPONSE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/Atomic.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_MCTS_ATOMIC_H
+#define LIBBOARDGAME_MCTS_ATOMIC_H
+
+#include <atomic>
+#include "libboardgame_util/Unused.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Data that may be atomic.
+ This struct is used for sharing the same code for a single-threaded and
+ a multi-threaded implementation depending on a template argument.
+ In the multi-threaded implementation, the variable is atomic, which
+ usually causes a small performance penalty, in the single-threaded
+ implementation, it is simply a regular variable.
+ @param T The type of the variable.
+ @param MT true, if the variable should be atomic. */
+template<typename T, bool MT> struct Atomic;
+
+template<typename T>
+struct Atomic<T, false>
+{
+ T val;
+
+ T operator=(T t)
+ {
+ val = t;
+ return val;
+ }
+
+ T load(memory_order order = memory_order_seq_cst) const
+ {
+ LIBBOARDGAME_UNUSED(order);
+ return val;
+ }
+
+ void store(T t, memory_order order = memory_order_seq_cst)
+ {
+ LIBBOARDGAME_UNUSED(order);
+ val = t;
+ }
+
+ operator T() const
+ {
+ return val;
+ }
+
+ T fetch_add(T t)
+ {
+ T tmp = val;
+ val += t;
+ return tmp;
+ }
+};
+
+template<typename T>
+struct Atomic<T, true>
+{
+ atomic<T> val;
+
+ T operator=(T t)
+ {
+ val.store(t);
+ return val;
+ }
+
+ T load(memory_order order = memory_order_seq_cst) const
+ {
+ return val.load(order);
+ }
+
+ void store(T t, memory_order order = memory_order_seq_cst)
+ {
+ val.store(t, order);
+ }
+
+ operator T() const
+ {
+ return load();
+ }
+
+ T fetch_add(T t)
+ {
+ return val.fetch_add(t);
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_ATOMIC_H
--- /dev/null
+# This library contains only header files with templates. The empty target
+# exists only to add the headers to IDE project files.
+add_custom_target(boardgame_mcts SOURCES
+ Atomic.h
+ LastGoodReply.h
+ Node.h
+ PlayerMove.h
+ SearchBase.h
+ Tree.h
+ TreeUtil.h
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/LastGoodReply.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H
+#define LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H
+
+#include <algorithm>
+#include <cstring>
+#include <memory>
+#include <random>
+#include "Atomic.h"
+#include "PlayerMove.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Storage for Last-Good-Reply heuristic.
+ Uses LGRF-2 (Baier, Drake: The Power of Forgetting: Improving the
+ Last-Good-Reply Policy in Monte-Carlo Go. 2010.
+ http://webdisk.lclark.edu/drake/publications/baier-drake-ieee-2010.pdf)
+ To save space, only the player of the reply move is considered when storing
+ or receiving a reply, the players of the last and second last moves are
+ ignored. In games without a fixed order of players (i.e. when move
+ sequences with the same moves but not played by the same players occur),
+ this can cause undetected collisions. If these collisions are not
+ sufficiently rare, the last-good-reply heuristic should be disabled in the
+ search. Undetected collisions can also occur because the replies are stored
+ in a hash table without collision check. But since the replies have to be
+ checked for legality in the current position anyway and the collisions are
+ probably rare, no major negative effect is expected from these collisions.
+ @tparam M The move type.
+ @tparam P The (maximum) number of players.
+ @tparam S The number of entries in the LGR2 has table (per player).
+ @tparam MT Whether the LGR table is used in a multi-threaded search. */
+template<class M, unsigned P, size_t S, bool MT>
+class LastGoodReply
+{
+public:
+ typedef M Move;
+
+ static const unsigned max_players = P;
+
+ static const size_t hash_table_size = S;
+
+ LastGoodReply();
+
+ void init(PlayerInt nu_players);
+
+ void store(PlayerInt player, Move last, Move second_last, Move reply);
+
+ void forget(PlayerInt player, Move last, Move second_last, Move reply);
+
+ Move get_lgr1(PlayerInt player, Move last) const;
+
+ Move get_lgr2(PlayerInt player, Move last, Move second_last) const;
+
+private:
+ size_t m_hash[Move::range];
+
+ Atomic<typename Move::IntType, MT> m_lgr1[max_players][Move::range];
+
+ Atomic<typename Move::IntType, MT> m_lgr2[max_players][hash_table_size];
+
+ size_t get_index(Move last, Move second_last) const;
+};
+
+template<class M, unsigned P, size_t S, bool MT>
+LastGoodReply<M, P, S, MT>::LastGoodReply()
+{
+ mt19937 generator;
+ for (auto& hash : m_hash)
+ hash = generator();
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+inline size_t LastGoodReply<M, P, S, MT>::get_index(Move last,
+ Move second_last) const
+{
+ size_t hash = (m_hash[last.to_int()] ^ m_hash[second_last.to_int()]);
+ return hash % hash_table_size;
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+inline auto LastGoodReply<M, P, S, MT>::get_lgr1(PlayerInt player,
+ Move last) const -> Move
+{
+ return Move(m_lgr1[player][last.to_int()].load(memory_order_relaxed));
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+inline auto LastGoodReply<M, P, S, MT>::get_lgr2(
+ PlayerInt player, Move last, Move second_last) const -> Move
+{
+ auto index = get_index(last, second_last);
+ return Move(m_lgr2[player][index].load(memory_order_relaxed));
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+void LastGoodReply<M, P, S, MT>::init(PlayerInt nu_players)
+{
+ for (PlayerInt i = 0; i < nu_players; ++i)
+ if (Move::null().to_int() == 0)
+ {
+ // Using memset is ok even if the elements are atomic because
+ // init() is used before the multi-threaded search starts.
+ memset(m_lgr1[i], 0, Move::range * sizeof(m_lgr1[i][0]));
+ memset(m_lgr2[i], 0, hash_table_size * sizeof(m_lgr2[i][0]));
+ }
+ else
+ {
+ fill(m_lgr1[i], m_lgr1[i] + Move::range, Move::null().to_int());
+ fill(m_lgr2[i], m_lgr2[i] + hash_table_size,
+ Move::null().to_int());
+ }
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+inline void LastGoodReply<M, P, S, MT>::forget(PlayerInt player, Move last,
+ Move second_last, Move reply)
+{
+ auto reply_int = reply.to_int();
+ auto null_int = Move::null().to_int();
+ {
+ auto index = get_index(last, second_last);
+ auto& stored_reply = m_lgr2[player][index];
+ if (stored_reply.load(memory_order_relaxed) == reply_int)
+ stored_reply.store(null_int, memory_order_relaxed);
+ }
+ auto& stored_reply = m_lgr1[player][last.to_int()];
+ if (stored_reply.load(memory_order_relaxed) == reply_int)
+ stored_reply.store(null_int, memory_order_relaxed);
+}
+
+template<class M, unsigned P, size_t S, bool MT>
+inline void LastGoodReply<M, P, S, MT>::store(PlayerInt player, Move last,
+ Move second_last, Move reply)
+{
+ auto reply_int = reply.to_int();
+ auto index = get_index(last, second_last);
+ m_lgr2[player][index].store(reply_int, memory_order_relaxed);
+ m_lgr1[player][last.to_int()].store(reply_int, memory_order_relaxed);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_LAST_GOOD_REPLY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/Node.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_MCTS_NODE_H
+#define LIBBOARDGAME_MCTS_NODE_H
+
+#include <cstdint>
+#include "Atomic.h"
+#include "libboardgame_util/Assert.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+typedef uint_least32_t NodeIdx;
+
+//-----------------------------------------------------------------------------
+
+/** %Node in a MCTS tree.
+ For details about how the nodes are used in lock-free multi-threaded mode,
+ see @ref libboardgame_doc_enz_2009. */
+template<typename M, typename F, bool MT>
+class Node
+{
+public:
+ typedef M Move;
+
+ typedef F Float;
+
+ Node() = default;
+
+ Node(const Node&) = delete;
+
+ Node& operator=(const Node&) = delete;
+
+ /** Initialize the node.
+ This function may not be called on a node that is already part of
+ the tree in multi-threaded mode.
+ The node may be initialized with values and counts greater zero
+ (prior knowledge) but even if it is initialized with count zero, it
+ must be initialized with a usable value (e.g. first play urgency for
+ inner nodes or tie value for the root node). */
+ void init(const Move& mv, Float value, Float count);
+
+ /** Initializes the root node.
+ Does not initialize value and value count as they are not used for the
+ root. */
+ void init_root();
+
+ const Move& get_move() const;
+
+ /** Number of simulations that went through this node. */
+ Float get_visit_count() const;
+
+ /** Number of values that were added.
+ This count is usually larger than the visit count because in addition
+ to the terminal values of the simulations, prior knowledge values and
+ weighted RAVE values could have been added added. */
+ Float get_value_count() const;
+
+ /** Value of the node.
+ For the root node, this is the value of the position from the point of
+ view of the player at the root node; for all other nodes, this is the
+ value of the move leading to the position at the node from the point
+ of view of the player at the parent node. */
+ Float get_value() const;
+
+ bool has_children() const;
+
+ unsigned short get_nu_children() const;
+
+ /** Copy the value count from another node without changing the child
+ information.
+ This function is not thread-safe and may not be called during the
+ search. */
+ void copy_data_from(const Node& node);
+
+ void link_children(NodeIdx first_child, unsigned short nu_children);
+
+ /** Faster version of link_children() for single-threaded parts of the
+ code. */
+ void link_children_st(NodeIdx first_child, unsigned short nu_children);
+
+ void unlink_children();
+
+ /** Faster version of unlink_children() for single-threaded parts of the
+ code. */
+ void unlink_children_st();
+
+ void add_value(Float v, Float weight = 1);
+
+ /** Add a value with weight 1 and remove a previously added loss.
+ Needed for the implementation of virtual losses in multi-threaded
+ MCTS and more efficient that a separate add and remove call. */
+ void add_value_remove_loss(Float v);
+
+ void inc_visit_count();
+
+ /** Get node index of first child.
+ @pre has_children() */
+ NodeIdx get_first_child() const;
+
+private:
+ Atomic<Float, MT> m_value;
+
+ Atomic<Float, MT> m_value_count;
+
+ Atomic<Float, MT> m_visit_count;
+
+ Atomic<unsigned short, MT> m_nu_children;
+
+ Move m_move;
+
+ NodeIdx m_first_child;
+};
+
+template<typename M, typename F, bool MT>
+void Node<M, F, MT>::add_value(Float v, Float weight)
+{
+ // Intentionally uses no synchronization and does not care about
+ // lost updates in multi-threaded mode
+ Float count = m_value_count.load(memory_order_relaxed);
+ Float value = m_value.load(memory_order_relaxed);
+ count += weight;
+ value += weight * (v - value) / count;
+ m_value.store(value, memory_order_relaxed);
+ m_value_count.store(count, memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+void Node<M, F, MT>::add_value_remove_loss(Float v)
+{
+ // Intentionally uses no synchronization and does not care about
+ // lost updates in multi-threaded mode
+ Float count = m_value_count.load(memory_order_relaxed);
+ if (count == 0)
+ return; // Adding the virtual loss was a lost update
+ Float value = m_value.load(memory_order_relaxed);
+ value += v / count;
+ m_value.store(value, memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+void Node<M, F, MT>::copy_data_from(const Node& node)
+{
+ // Reminder to update this function when the class gets additional members
+ struct Dummy
+ {
+ Atomic<Float, MT> m_value;
+ Atomic<Float, MT> m_value_count;
+ Atomic<Float, MT> m_visit_count;
+ Atomic<unsigned short, MT> m_nu_children;
+ Move m_move;
+ NodeIdx m_first_child;
+ };
+ static_assert(sizeof(Node) == sizeof(Dummy), "");
+
+ m_move = node.m_move;
+ // Load/store relaxed (it wouldn't even need to be atomic) because this
+ // function is only used before the multi-threaded search.
+ m_value_count.store(node.m_value_count.load(memory_order_relaxed),
+ memory_order_relaxed);
+ m_value.store(node.m_value.load(memory_order_relaxed),
+ memory_order_relaxed);
+ m_visit_count.store(node.m_visit_count.load(memory_order_relaxed),
+ memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_value_count() const -> Float
+{
+ return m_value_count.load(memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline NodeIdx Node<M, F, MT>::get_first_child() const
+{
+ LIBBOARDGAME_ASSERT(has_children());
+ return m_first_child;
+}
+
+template<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_move() const -> const Move&
+{
+ return m_move;
+}
+
+template<typename M, typename F, bool MT>
+inline unsigned short Node<M, F, MT>::get_nu_children() const
+{
+ return m_nu_children.load(memory_order_acquire);
+}
+
+template<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_value() const -> Float
+{
+ return m_value.load(memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline auto Node<M, F, MT>::get_visit_count() const -> Float
+{
+ return m_visit_count.load(memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline bool Node<M, F, MT>::has_children() const
+{
+ return get_nu_children() > 0;
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::inc_visit_count()
+{
+ // We don't care about the unlikely case that updates are lost because
+ // incrementing is not atomic
+ Float count = m_visit_count.load(memory_order_relaxed);
+ ++count;
+ m_visit_count.store(count, memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+void Node<M, F, MT>::init(const Move& mv, Float value, Float count)
+{
+ // The node is not yet visible to other threads because init() is called
+ // before the children are linked to its parent with link_children()
+ // (which does a memory_order_release on m_nu_children of the parent).
+ // Therefore, the most efficient way here is to initialize all values with
+ // memory_order_relaxed.
+ m_move = mv;
+ m_value_count.store(count, memory_order_relaxed);
+ m_value.store(value, memory_order_relaxed);
+ m_visit_count.store(0, memory_order_relaxed);
+ m_nu_children.store(0, memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+void Node<M, F, MT>::init_root()
+{
+#if LIBBOARDGAME_DEBUG
+ m_move = Move::null();
+#endif
+ m_visit_count.store(0, memory_order_relaxed);
+ m_nu_children.store(0, memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::link_children(NodeIdx first_child,
+ unsigned short nu_children)
+{
+ LIBBOARDGAME_ASSERT(nu_children < Move::range);
+ // first_child cannot be 0 because 0 is always used for the root node
+ LIBBOARDGAME_ASSERT(first_child != 0);
+ m_first_child = first_child;
+ m_nu_children.store(nu_children, memory_order_release);
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::link_children_st(NodeIdx first_child,
+ unsigned short nu_children)
+{
+ LIBBOARDGAME_ASSERT(nu_children < Move::range);
+ // first_child cannot be 0 because 0 is always used for the root node
+ LIBBOARDGAME_ASSERT(first_child != 0);
+ m_first_child = first_child;
+ // Store relaxed (wouldn't even need to be atomic)
+ m_nu_children.store(nu_children, memory_order_relaxed);
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::unlink_children()
+{
+ m_nu_children.store(0, memory_order_release);
+}
+
+template<typename M, typename F, bool MT>
+inline void Node<M, F, MT>::unlink_children_st()
+{
+ // Store relaxed (wouldn't even need to be atomic)
+ m_nu_children.store(0, memory_order_relaxed);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_NODE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/PlayerMove.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_MCTS_PLAYER_MOVE_H
+#define LIBBOARDGAME_MCTS_PLAYER_MOVE_H
+
+#include <cstdint>
+
+namespace libboardgame_mcts {
+
+//-----------------------------------------------------------------------------
+
+typedef uint_fast8_t PlayerInt;
+
+//-----------------------------------------------------------------------------
+
+template<typename MOVE>
+struct PlayerMove
+{
+ PlayerInt player;
+
+ MOVE move;
+
+ PlayerMove() = default;
+
+ PlayerMove(PlayerInt player, MOVE move)
+ {
+ this->player = player;
+ this->move = move;
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_PLAYER_MOVE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/SearchBase.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_MCTS_SEARCH_BASE_H
+#define LIBBOARDGAME_MCTS_SEARCH_BASE_H
+
+#include <array>
+#include <condition_variable>
+#include <functional>
+#include <mutex>
+#include <thread>
+#include "Atomic.h"
+#include "LastGoodReply.h"
+#include "PlayerMove.h"
+#include "Tree.h"
+#include "TreeUtil.h"
+#include "libboardgame_util/Abort.h"
+#include "libboardgame_util/ArrayList.h"
+#include "libboardgame_util/Barrier.h"
+#include "libboardgame_util/IntervalChecker.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/MathUtil.h"
+#include "libboardgame_util/Statistics.h"
+#include "libboardgame_util/StringUtil.h"
+#include "libboardgame_util/TimeIntervalChecker.h"
+#include "libboardgame_util/Timer.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+using libboardgame_mcts::tree_util::find_node;
+using libboardgame_util::get_abort;
+using libboardgame_util::time_to_string;
+using libboardgame_util::to_string;
+using libboardgame_util::ArrayList;
+using libboardgame_util::Barrier;
+using libboardgame_util::IntervalChecker;
+using libboardgame_util::RandomGenerator;
+using libboardgame_util::StatisticsBase;
+using libboardgame_util::StatisticsDirtyLockFree;
+using libboardgame_util::StatisticsExt;
+using libboardgame_util::Timer;
+using libboardgame_util::TimeIntervalChecker;
+using libboardgame_util::TimeSource;
+
+//-----------------------------------------------------------------------------
+
+#define LIBBOARDGAME_LOG_THREAD(thread_state, ...) \
+ LIBBOARDGAME_LOG('[', thread_state.thread_id, "] ", __VA_ARGS__)
+
+//-----------------------------------------------------------------------------
+
+/** Default optional compile-time parameters for Search.
+ See description of class Search for more information. */
+struct SearchParamConstDefault
+{
+ /** The floating type used for mean values and counts.
+ The default type is @c float for a reduced node size and performance
+ gains (especially on 32-bit systems). However, using @c float sets a
+ practical limit on the number of simulations before the count and mean
+ values go into saturation. This maximum is given by 2^d-1 with d being
+ the digits in the mantissa (=23 for IEEE 754 float's). The search will
+ terminate when this number is reached. For longer searches, the code
+ should be compiled with floating type @c double. */
+ typedef float Float;
+
+ /** The maximum number of players. */
+ static const PlayerInt max_players = 2;
+
+ /** The maximum length of a game. */
+ static const unsigned max_moves = 1000;
+
+ /** Compile with support for multi-threaded search.
+ Disabling this slightly increases the performance if support for a
+ multi-threaded search is not needed. */
+ static const bool multithread = true;
+
+ /** Use RAVE. */
+ static const bool rave = false;
+
+ /** Enable distance weighting of RAVE updates.
+ The weight decreases linearly from the start to the end of a
+ simulation. The distance weight is applied in addition to the normal
+ RAVE weight. */
+ static const bool rave_dist_weighting = false;
+
+ /** Enable Last-Good-Reply heuristic.
+ @see LastGoodReply */
+ static const bool use_lgr = false;
+
+ /** See LastGoodReply::hash_table_size.
+ Must be greater 0 if use_lgr is true. */
+ static const size_t lgr_hash_table_size = 0;
+
+ /** Use virtual loss in multi-threaded mode.
+ See Chaslot et al.: Parallel Monte-Carlo Tree Search. 2008. */
+ static const bool virtual_loss = false;
+
+ /** Terminate search early if move is unlikely to change.
+ See implementation of check_cannot_change(). */
+ static const bool use_unlikely_change = true;
+
+ /** The minimum count used in prior knowledge initialization of
+ the children of an expanded node.
+ The value must be greater 0 (it may be a positive epsilon) because
+ otherwise the search would need to handle a special case in the bias
+ term computation. */
+ static constexpr Float child_min_count = 0;
+
+ /** An evaluation value representing a 50% winning probability. */
+ static constexpr Float tie_value = 0.5f;
+
+ /** Value to start the tree pruning with.
+ This value should be above typical count initializations if prior
+ knowledge initialization is used. */
+ static constexpr Float prune_count_start = 16;
+
+ /** Expected simulations per second.
+ If the simulations per second vary a lot, it should be a value closer
+ to the lower values. This value is used, for example, to determine an
+ interval for checking expensive abort conditions in deterministic mode
+ (in regular mode, the simulations per second will be measured and the
+ interval will be adjusted automatically). That means that in
+ deterministic mode, a pessimistic low value will cause more calls to
+ the expensive function but an optimistic high value will delay aborting
+ the search. */
+ static constexpr double expected_sim_per_sec = 100;
+};
+
+//-----------------------------------------------------------------------------
+
+/** Game-independent Monte-Carlo tree search.
+ Game-dependent functionality is added by implementing some pure virtual
+ functions and by template parameters.
+
+ RAVE (see @ref libboardgame_doc_rave) is implemented differently from
+ the algorithm described in the original paper: RAVE values are not stored
+ separately in the nodes but added to the normal values with a certain
+ (constant) weight and up to a maximum visit count of the parent node. This
+ saves memory in the tree and speeds up move selection in the in-tree phase.
+ It is weaker than the original RAVE at a low number of simulations but
+ seems to be equally good or even better at a high number of simulations.
+
+ @tparam S The game-dependent state of a simulation. The state provides
+ functions for move generation, evaluation of terminal positions, etc. The
+ state should be thread-safe to support multiple states if multi-threading
+ is used.
+ @tparam M The move type. The type must be convertible to an integer by
+ providing M::to_int() and M::range.
+ @tparam R Optional compile-time parameters, see SearchParamConstDefault */
+template<class S, class M, class R = SearchParamConstDefault>
+class SearchBase
+{
+public:
+ typedef S State;
+
+ typedef M Move;
+
+ typedef R SearchParamConst;
+
+ static const bool multithread = SearchParamConst::multithread;
+
+ typedef typename SearchParamConst::Float Float;
+
+ typedef libboardgame_mcts::Node<M, Float, multithread> Node;
+
+ typedef libboardgame_mcts::Tree<Node> Tree;
+
+ typedef libboardgame_mcts::PlayerMove<M> PlayerMove;
+
+ static const PlayerInt max_players = SearchParamConst::max_players;
+
+ static const unsigned max_moves = SearchParamConst::max_moves;
+
+ static const size_t lgr_hash_table_size =
+ SearchParamConst::lgr_hash_table_size;
+
+ static_assert(! SearchParamConst::use_lgr || lgr_hash_table_size > 0, "");
+
+
+ /** Constructor.
+ @param nu_threads
+ @param memory The memory to be used for (all) the search trees. */
+ SearchBase(unsigned nu_threads, size_t memory);
+
+ virtual ~SearchBase();
+
+
+ /** @name Pure virtual functions */
+ /** @{ */
+
+ /** Create a new game-specific state to be used in a thread of the
+ search. */
+ virtual unique_ptr<State> create_state() = 0;
+
+ /** Get the current number of players. */
+ virtual PlayerInt get_nu_players() const = 0;
+
+ /** Get player to play at root node of the search. */
+ virtual PlayerInt get_player() const = 0;
+
+ /** @} */ // @name
+
+
+ /** @name Virtual functions */
+ /** @{ */
+
+ /** Check if the position at the root is a follow-up position of the last
+ search.
+ In this function, the subclass can store the game state at the root of
+ the search, compare it to the the one of the last search, check if
+ the current state is a follow-up position and return the move sequence
+ leading from the last position to the current one, so that the search
+ can check if a subtree of the last search can be reused.
+ This function will be called exactly once at the beginning of each
+ search. The default implementation returns false.
+ The information is also used for deciding whether to clear other
+ caches from the last search (e.g. Last-Good-Reply heuristic). */
+ virtual bool check_followup(ArrayList<Move, max_moves>& sequence);
+
+ virtual string get_info() const;
+
+ virtual string get_info_ext() const;
+
+ /** @} */ // @name
+
+
+ /** @name Parameters */
+ /** @{ */
+
+ /** Minimum count of a node to be expanded. */
+ void set_expand_threshold(Float n);
+
+ Float get_expand_threshold() const;
+
+ /** Increase of the expand threshold per in-tree move played. */
+ void set_expand_threshold_inc(Float n);
+
+ Float get_expand_threshold_inc() const;
+
+ /** Constant used in the exploration term.
+ The exploration term has the form c * sqrt(parent_count) / child_count
+ with a configurable constant c. It assumes that children counts are
+ initialized greater than 0. */
+ void set_exploration_constant(Float c) { m_exploration_constant = c; }
+
+ Float get_exploration_constant() const { return m_exploration_constant; }
+
+ /** Reuse the subtree from the previous search if the current position is
+ a follow-up position of the previous one. */
+ void set_reuse_subtree(bool enable);
+
+ bool get_reuse_subtree() const;
+
+ /** Reuse the tree from the previous search if the current position is
+ the same position as the previous one. */
+ void set_reuse_tree(bool enable);
+
+ bool get_reuse_tree() const;
+
+ /** Maximum parent visit count for applying RAVE. */
+ void set_rave_parent_max(Float value);
+
+ Float get_rave_parent_max() const;
+
+ /** Maximum child value count for applying RAVE. */
+ void set_rave_child_max(Float value);
+
+ Float get_rave_child_max() const;
+
+ /** Weight used for adding RAVE values to the node value. */
+ void set_rave_weight(Float value);
+
+ Float get_rave_weight() const;
+
+ /** @} */ // @name
+
+
+ /** Run a search.
+ @param[out] mv
+ @param max_count Number of simulations to run. The search might return
+ earlier if the best move cannot change anymore or if the count of the
+ root node was initialized from an init tree
+ @param min_simulations
+ @param max_time Maximum search time. Only used if max_count is zero
+ @param time_source Time source for time measurement
+ @return @c false if no move could be generated because the position is
+ a terminal position. */
+ bool search(Move& mv, Float max_count, size_t min_simulations,
+ double max_time, TimeSource& time_source);
+
+ const Tree& get_tree() const;
+
+#if LIBBOARDGAME_DEBUG
+ string dump() const;
+#endif
+
+ /** Number of simulations in the current search in all threads. */
+ size_t get_nu_simulations() const;
+
+ /** Select the move to play.
+ Uses select_final(). */
+ bool select_move(Move& mv) const;
+
+ /** Select the best child of the root node after the search.
+ Selects child with highest number of wins; the value is used as a
+ tie-breaker for equal counts (important at very low number of
+ simulations, e.g. all children have count 1 or 0). */
+ const Node* select_final() const;
+
+ State& get_state(unsigned thread_id);
+
+ const State& get_state(unsigned thread_id) const;
+
+ /** Set a callback function that informs the caller about the
+ estimated time left.
+ The callback function will be called about every 0.1s. The arguments
+ of the callback function are: elapsed time, estimated remaining time. */
+ void set_callback(function<void(double, double)> callback);
+
+ /** Get evaluation for a player at root node. */
+ const StatisticsDirtyLockFree<Float>& get_root_val(PlayerInt player) const;
+
+ /** Get evaluation for get_player() at root node. */
+ const StatisticsDirtyLockFree<Float>& get_root_val() const;
+
+ /** The number of times the root node was visited.
+ This is equal to the number of simulations plus the visit count
+ of a subtree reused from the previous search. */
+ Float get_root_visit_count() const;
+
+ /** Create the threads used in the search.
+ This cannot be done in the constructor because it uses the virtual
+ function create_state(). This function will automatically be called
+ before a search if the threads have not been constructed yet, but it
+ is advisable to explicitely call it in the constructor of the subclass
+ to save some time at the first move generation where the game clock
+ might already be running. */
+ void create_threads();
+
+protected:
+ struct Simulation
+ {
+ ArrayList<const Node*, max_moves> nodes;
+
+ ArrayList<PlayerMove, max_moves> moves;
+
+ array<Float, max_players> eval;
+ };
+
+ virtual void on_start_search(bool is_followup);
+
+ /** Time source for current search.
+ Only valid during a search. */
+ TimeSource& get_time_source();
+
+private:
+#if LIBBOARDGAME_DEBUG
+ class AssertionHandler
+ : public libboardgame_util::AssertionHandler
+ {
+ public:
+ AssertionHandler(const SearchBase& search);
+
+ ~AssertionHandler();
+
+ void run() override;
+
+ private:
+ const SearchBase& m_search;
+ };
+#endif
+
+ /** Thread-specific search state. */
+ struct ThreadState
+ {
+ unique_ptr<State> state;
+
+ unsigned thread_id;
+
+ /** Was the search in this thread terminated because the search tree
+ was full? */
+ bool is_out_of_mem;
+
+ Simulation simulation;
+
+ StatisticsExt<> stat_len;
+
+ StatisticsExt<> stat_in_tree_len;
+
+ /** Local variable for update_rave().
+ Reused for efficiency. */
+ array<PlayerInt, Move::range> was_played;
+
+ /** Local variable for update_rave().
+ Reused for efficiency. */
+ array<unsigned, Move::range> first_play;
+
+ ~ThreadState();
+ };
+
+ /** Thread in the parallel search.
+ The thread waits for a call to start_search(), then runs
+ SearchBase::search_loop()) with the thread-specific search state.
+ After start_search(), wait_search_finished() needs to called before
+ calling start_search() again or destructing this object. */
+ class Thread
+ {
+ public:
+ typedef function<void(ThreadState&)> SearchFunc;
+
+ ThreadState thread_state;
+
+ explicit Thread(SearchFunc& search_func);
+
+ ~Thread();
+
+ void run();
+
+ void start_search();
+
+ void wait_search_finished();
+
+ private:
+ SearchFunc m_search_func;
+
+ bool m_quit = false;
+
+ bool m_start_search_flag = false;
+
+ bool m_search_finished_flag = false;
+
+ Barrier m_thread_ready{2};
+
+ mutex m_start_search_mutex;
+
+ mutex m_search_finished_mutex;
+
+ condition_variable m_start_search_cond;
+
+ condition_variable m_search_finished_cond;
+
+ unique_lock<mutex> m_search_finished_lock{m_search_finished_mutex,
+ defer_lock};
+
+ thread m_thread;
+
+ void thread_main();
+ };
+
+
+ /** @name Members that are used concurrently by all threads during the
+ lock-free multi-threaded search */
+ /** @{ */
+
+ Tree m_tree;
+
+ /** See get_root_val(). */
+ array<StatisticsDirtyLockFree<Float>, max_players> m_root_val;
+
+ LastGoodReply<Move, max_players, lgr_hash_table_size, multithread> m_lgr;
+
+ /** See get_nu_simulations(). */
+ Atomic<size_t, multithread> m_nu_simulations;
+
+ /** @} */ // @name
+
+
+ unsigned m_nu_threads;
+
+ Float m_expand_threshold = 0;
+
+ Float m_expand_threshold_inc = 0;
+
+ bool m_deterministic;
+
+ bool m_reuse_subtree = true;
+
+ bool m_reuse_tree = false;
+
+ /** Player to play at the root node of the search. */
+ PlayerInt m_player;
+
+ /** Cached return value of get_nu_players() that stays constant during
+ a search. */
+ PlayerInt m_nu_players;
+
+ /** Time of last search. */
+ double m_last_time;
+
+ Float m_rave_parent_max = 50000;
+
+ Float m_rave_child_max = 2000;
+
+ Float m_rave_weight = 0.3f;
+
+ /** Minimum simulations to perform in the current search.
+ This does not include the count of simulations reused from a subtree of
+ a previous search. */
+ size_t m_min_simulations;
+
+ /** Maximum simulations of current search.
+ This include the count of simulations reused from a subtree of a
+ previous search. */
+ Float m_max_count;
+
+ /** Maximum time of current search. */
+ double m_max_time;
+
+ TimeSource* m_time_source;
+
+ Float m_exploration_constant;
+
+ Timer m_timer;
+
+ vector<unique_ptr<Thread>> m_threads;
+
+ Tree m_tmp_tree;
+
+#if LIBBOARDGAME_DEBUG
+ AssertionHandler m_assertion_handler;
+#endif
+
+
+ function<void(double, double)> m_callback;
+
+ ArrayList<Move, max_moves> m_followup_sequence;
+
+ bool check_abort(const ThreadState& thread_state) const;
+
+ LIBBOARDGAME_NOINLINE
+ bool check_abort_expensive(ThreadState& thread_state) const;
+
+ bool check_cannot_change(ThreadState& thread_state, Float remaining) const;
+
+ bool estimate_reused_root_val(Tree& tree, const Node& root, Float& value,
+ Float& count);
+
+ bool expand_node(ThreadState& thread_state, const Node& node,
+ const Node*& best_child);
+
+ void playout(ThreadState& thread_state);
+
+ void play_in_tree(ThreadState& thread_state);
+
+ bool prune(TimeSource& time_source, double time, Float prune_min_count,
+ Float& new_prune_min_count);
+
+ void search_loop(ThreadState& thread_state);
+
+ const Node* select_child(const Node& node);
+
+ void update_lgr(ThreadState& thread_state);
+
+ void update_rave(ThreadState& thread_state);
+
+ void update_values(ThreadState& thread_state);
+};
+
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::ThreadState::~ThreadState() = default;
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::Thread::Thread(SearchFunc& search_func)
+ : m_search_func(search_func)
+{ }
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::Thread::~Thread()
+{
+ if (! m_thread.joinable())
+ return;
+ m_quit = true;
+ {
+ lock_guard<mutex> lock(m_start_search_mutex);
+ m_start_search_flag = true;
+ }
+ m_start_search_cond.notify_one();
+ m_thread.join();
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::Thread::run()
+{
+ m_thread = thread(bind(&Thread::thread_main, this));
+ m_thread_ready.wait();
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::Thread::start_search()
+{
+ LIBBOARDGAME_ASSERT(m_thread.joinable());
+ m_search_finished_lock.lock();
+ {
+ lock_guard<mutex> lock(m_start_search_mutex);
+ m_start_search_flag = true;
+ }
+ m_start_search_cond.notify_one();
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::Thread::thread_main()
+{
+ unique_lock<mutex> lock(m_start_search_mutex);
+ m_thread_ready.wait();
+ while (true)
+ {
+ while (! m_start_search_flag)
+ m_start_search_cond.wait(lock);
+ m_start_search_flag = false;
+ if (m_quit)
+ break;
+ m_search_func(thread_state);
+ {
+ lock_guard<mutex> lock(m_search_finished_mutex);
+ m_search_finished_flag = true;
+ }
+ m_search_finished_cond.notify_one();
+ }
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::Thread::wait_search_finished()
+{
+ LIBBOARDGAME_ASSERT(m_thread.joinable());
+ while (! m_search_finished_flag)
+ m_search_finished_cond.wait(m_search_finished_lock);
+ m_search_finished_flag = false;
+ m_search_finished_lock.unlock();
+}
+
+
+#if LIBBOARDGAME_DEBUG
+template<class S, class M, class R>
+SearchBase<S, M, R>::AssertionHandler::AssertionHandler(
+ const SearchBase& search)
+ : m_search(search)
+{
+}
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::AssertionHandler::~AssertionHandler() = default;
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::AssertionHandler::run()
+{
+ LIBBOARDGAME_LOG(m_search.dump());
+}
+#endif // LIBBOARDGAME_DEBUG
+
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::SearchBase(unsigned nu_threads, size_t memory)
+ : m_tree(memory / 2, nu_threads),
+ m_nu_threads(nu_threads),
+ m_exploration_constant(0),
+ m_tmp_tree(memory / 2, m_nu_threads)
+#if LIBBOARDGAME_DEBUG
+ , m_assertion_handler(*this)
+#endif
+{ }
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::~SearchBase() = default;
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_abort(const ThreadState& thread_state) const
+{
+#if LIBBOARDGAME_DISABLE_LOG
+ LIBBOARDGAME_UNUSED(thread_state);
+#endif
+ if (m_max_count > 0 && m_tree.get_root().get_visit_count() >= m_max_count)
+ {
+ LIBBOARDGAME_LOG_THREAD(thread_state, "Maximum count reached");
+ return true;
+ }
+ return false;
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_abort_expensive(
+ ThreadState& thread_state) const
+{
+ if (get_abort())
+ {
+ LIBBOARDGAME_LOG_THREAD(thread_state, "Search aborted");
+ return true;
+ }
+ static_assert(numeric_limits<Float>::radix == 2, "");
+ auto count = m_tree.get_root().get_visit_count();
+ if (count >= (size_t(1) << numeric_limits<Float>::digits) - 1)
+ {
+ LIBBOARDGAME_LOG_THREAD(thread_state,
+ "Max count supported by float exceeded");
+ return true;
+ }
+ auto time = m_timer();
+ if (! m_deterministic && time < 0.1)
+ // Simulations per second might be inaccurate for very small times
+ return false;
+ double simulations_per_sec;
+ if (time == 0)
+ simulations_per_sec = SearchParamConst::expected_sim_per_sec;
+ else
+ {
+ size_t nu_simulations = m_nu_simulations.load(memory_order_relaxed);
+ simulations_per_sec = double(nu_simulations) / time;
+ }
+ double remaining_time;
+ Float remaining_simulations;
+ if (m_max_count == 0)
+ {
+ // Search uses time limit
+ if (time > m_max_time)
+ {
+ LIBBOARDGAME_LOG_THREAD(thread_state, "Maximum time reached");
+ return true;
+ }
+ remaining_time = m_max_time - time;
+ remaining_simulations = Float(remaining_time * simulations_per_sec);
+ }
+ else
+ {
+ // Search uses count limit
+ remaining_simulations = m_max_count - count;
+ remaining_time = remaining_simulations / simulations_per_sec;
+ }
+ if (thread_state.thread_id == 0 && m_callback)
+ m_callback(time, remaining_time);
+ if (check_cannot_change(thread_state, remaining_simulations))
+ return true;
+ return false;
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_cannot_change(ThreadState& thread_state,
+ Float remaining) const
+{
+#if LIBBOARDGAME_DISABLE_LOG
+ LIBBOARDGAME_UNUSED(thread_state);
+#endif
+ // select_final() selects move with highest number of wins.
+ Float max_wins = 0;
+ Float second_max = 0;
+ for (auto& i : m_tree.get_root_children())
+ {
+ Float wins = i.get_value() * i.get_value_count();
+ if (wins > max_wins)
+ {
+ second_max = max_wins;
+ max_wins = wins;
+ }
+ }
+ Float diff = max_wins - second_max;
+ if (SearchParamConst::use_unlikely_change)
+ {
+ // Weight remaining number of simulations with current global win rate,
+ // but not less than 10%
+ auto& root_val = m_root_val[m_player];
+ Float win_rate;
+ if (root_val.get_count() > 100)
+ {
+ win_rate = root_val.get_mean();
+ if (win_rate < 0.1f)
+ win_rate = 0.1f;
+ }
+ else
+ win_rate = 1; // Not enough statistics
+ if (diff < win_rate * remaining)
+ return false;
+ }
+ else if (diff < remaining)
+ return false;
+ LIBBOARDGAME_LOG_THREAD(thread_state, "Move will not change");
+ return true;
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_followup(ArrayList<Move, max_moves>& sequence)
+{
+ LIBBOARDGAME_UNUSED(sequence);
+ return false;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::create_threads()
+{
+ if (! multithread && m_nu_threads > 1)
+ throw runtime_error("libboardgame_mcts::Search was compiled"
+ " without support for multithreading");
+ LIBBOARDGAME_LOG("Creating ", m_nu_threads, " threads");
+ m_threads.clear();
+ m_threads.reserve(m_nu_threads);
+ auto search_func =
+ static_cast<typename Thread::SearchFunc>(
+ bind(&SearchBase::search_loop, this, placeholders::_1));
+ for (unsigned i = 0; i < m_nu_threads; ++i)
+ {
+ unique_ptr<Thread> t(new Thread(search_func));
+ auto& thread_state = t->thread_state;
+ thread_state.thread_id = i;
+ thread_state.state = create_state();
+ for (auto& was_played : thread_state.was_played)
+ was_played = max_players;
+ if (i > 0)
+ t->run();
+ m_threads.push_back(move(t));
+ }
+}
+
+#if LIBBOARDGAME_DEBUG
+template<class S, class M, class R>
+string SearchBase<S, M, R>::dump() const
+{
+ ostringstream s;
+ for (unsigned i = 0; i < m_nu_threads; ++i)
+ {
+ s << "Thread state " << i << ":\n"
+ << get_state(i).dump();
+ }
+ return s.str();
+}
+#endif
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::expand_node(ThreadState& thread_state,
+ const Node& node,
+ const Node*& best_child)
+{
+ auto& state = *thread_state.state;
+ auto thread_id = thread_state.thread_id;
+ typename Tree::NodeExpander expander(thread_id, m_tree,
+ SearchParamConst::child_min_count);
+ auto root_val = m_root_val[state.get_player()].get_mean();
+ if (state.gen_children(expander, root_val))
+ {
+ expander.link_children(m_tree, node);
+ best_child = expander.get_best_child();
+ return true;
+ }
+ return false;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_expand_threshold() const -> Float
+{
+ return m_expand_threshold;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_expand_threshold_inc() const -> Float
+{
+ return m_expand_threshold_inc;
+}
+
+template<class S, class M, class R>
+inline size_t SearchBase<S, M, R>::get_nu_simulations() const
+{
+ return m_nu_simulations;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_root_val(PlayerInt player) const
+-> const StatisticsDirtyLockFree<Float>&
+{
+ LIBBOARDGAME_ASSERT(player < m_nu_players);
+ return m_root_val[player];
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_root_val() const
+-> const StatisticsDirtyLockFree<Float>&
+{
+ return get_root_val(get_player());
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_root_visit_count() const -> Float
+{
+ return m_tree.get_root().get_visit_count();
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_rave_parent_max() const -> Float
+{
+ return m_rave_parent_max;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_rave_child_max() const -> Float
+{
+ return m_rave_child_max;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_rave_weight() const -> Float
+{
+ return m_rave_weight;
+}
+
+template<class S, class M, class R>
+inline bool SearchBase<S, M, R>::get_reuse_subtree() const
+{
+ return m_reuse_subtree;
+}
+
+template<class S, class M, class R>
+inline bool SearchBase<S, M, R>::get_reuse_tree() const
+{
+ return m_reuse_tree;
+}
+
+template<class S, class M, class R>
+inline S& SearchBase<S, M, R>::get_state(unsigned thread_id)
+{
+ LIBBOARDGAME_ASSERT(thread_id < m_threads.size());
+ return *m_threads[thread_id]->thread_state.state;
+}
+
+template<class S, class M, class R>
+inline const S& SearchBase<S, M, R>::get_state(unsigned thread_id) const
+{
+ LIBBOARDGAME_ASSERT(thread_id < m_threads.size());
+ return *m_threads[thread_id]->thread_state.state;
+}
+
+template<class S, class M, class R>
+inline TimeSource& SearchBase<S, M, R>::get_time_source()
+{
+ LIBBOARDGAME_ASSERT(m_time_source != 0);
+ return *m_time_source;
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::get_tree() const -> const Tree&
+{
+ return m_tree;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::on_start_search(bool is_followup)
+{
+ // Default implementation does nothing
+ LIBBOARDGAME_UNUSED(is_followup);
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::playout(ThreadState& thread_state)
+{
+ auto& state = *thread_state.state;
+ state.start_playout();
+ auto& simulation = thread_state.simulation;
+ auto& moves = simulation.moves;
+ auto nu_moves = moves.size();
+ Move last = nu_moves > 0 ? moves[nu_moves - 1].move : Move::null();
+ Move second_last = nu_moves > 1 ? moves[nu_moves - 2].move : Move::null();
+ PlayerMove mv;
+ while (state.gen_playout_move(m_lgr, last, second_last, mv))
+ {
+ state.play_playout(mv.move);
+ moves.push_back(mv);
+ second_last = last;
+ last = mv.move;
+ }
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::play_in_tree(ThreadState& thread_state)
+{
+ auto& state = *thread_state.state;
+ auto& simulation = thread_state.simulation;
+ simulation.nodes.resize(1);
+ simulation.moves.clear();
+ auto& root = m_tree.get_root();
+ auto node = &root;
+ Float expand_threshold = m_expand_threshold;
+ while (node->has_children())
+ {
+ node = select_child(*node);
+ if (multithread && SearchParamConst::virtual_loss)
+ m_tree.add_value(*node, 0);
+ simulation.nodes.push_back(node);
+ Move mv = node->get_move();
+ simulation.moves.push_back(PlayerMove(state.get_player(), mv));
+ state.play_in_tree(mv);
+ expand_threshold += m_expand_threshold_inc;
+ }
+ state.finish_in_tree();
+ if (node->get_visit_count() > expand_threshold)
+ {
+ if (! expand_node(thread_state, *node, node))
+ thread_state.is_out_of_mem = true;
+ else if (node)
+ {
+ simulation.nodes.push_back(node);
+ Move mv = node->get_move();
+ simulation.moves.push_back(PlayerMove(state.get_player(), mv));
+ state.play_expanded_child(mv);
+ }
+ }
+ thread_state.stat_in_tree_len.add(double(simulation.moves.size()));
+}
+
+template<class S, class M, class R>
+string SearchBase<S, M, R>::get_info() const
+{
+ auto& root = m_tree.get_root();
+ if (m_threads.empty())
+ return string();
+ auto& thread_state = m_threads[0]->thread_state;
+ ostringstream s;
+ s << fixed << setprecision(2) << "Val: " << get_root_val().get_mean()
+ << setprecision(0) << ", ValCnt: " << get_root_val().get_count()
+ << ", VstCnt: " << get_root_visit_count()
+ << ", Sim: " << m_nu_simulations;
+ auto child = select_final();
+ if (child && root.get_visit_count() > 0)
+ s << setprecision(1) << ", Chld: "
+ << (100 * child->get_visit_count() / root.get_visit_count())
+ << '%';
+ s << "\nNds: " << m_tree.get_nu_nodes()
+ << ", Tm: " << time_to_string(m_last_time)
+ << setprecision(0) << ", Sim/s: "
+ << (double(m_nu_simulations) / m_last_time)
+ << ", Len: " << thread_state.stat_len.to_string(true, 1, true)
+ << "\nDp: " << thread_state.stat_in_tree_len.to_string(true, 1, true)
+ << "\n";
+ return s.str();
+}
+
+template<class S, class M, class R>
+string SearchBase<S, M, R>::get_info_ext() const
+{
+ return string();
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::prune(TimeSource& time_source, double time,
+ Float prune_min_count,
+ Float& new_prune_min_count)
+{
+#if LIBBOARDGAME_DISABLE_LOG
+ LIBBOARDGAME_UNUSED(time);
+#endif
+ Timer timer(time_source);
+ m_tmp_tree.clear();
+ m_tree.copy_subtree(m_tmp_tree, m_tmp_tree.get_root(), m_tree.get_root(),
+ prune_min_count);
+ int percent = int(m_tmp_tree.get_nu_nodes() * 100 / m_tree.get_nu_nodes());
+ LIBBOARDGAME_LOG("Pruning MinCnt: ", prune_min_count, ", AtTm: ", time,
+ ", Nds: ", m_tmp_tree.get_nu_nodes(), " (", percent,
+ "%), Tm: ", timer());
+ m_tree.swap(m_tmp_tree);
+ if (percent > 50)
+ {
+ if (prune_min_count >= 0.5 * numeric_limits<Float>::max())
+ return false;
+ new_prune_min_count = prune_min_count * 2;
+ return true;
+ }
+ else
+ {
+ new_prune_min_count = prune_min_count;
+ return true;
+ }
+}
+
+/** Estimate the value and count of a root node from its children.
+ After reusing a subtree, we don't know the value of the root because nodes
+ only store the value of moves. To estimate the root value, we use the child
+ with the highest visit count. */
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::estimate_reused_root_val(Tree& tree,
+ const Node& root,
+ Float& value, Float& count)
+{
+ const Node* best = nullptr;
+ Float max_count = 0;
+ for (auto& i : tree.get_children(root))
+ if (i.get_visit_count() > max_count)
+ {
+ best = &i;
+ max_count = i.get_visit_count();
+ }
+ if (! best)
+ return false;
+ value = best->get_value();
+ count = best->get_value_count();
+ return count > 0;
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::search(Move& mv, Float max_count,
+ size_t min_simulations, double max_time,
+ TimeSource& time_source)
+{
+ if (m_nu_threads != m_threads.size())
+ create_threads();
+ m_deterministic = RandomGenerator::has_global_seed();
+ bool is_followup = check_followup(m_followup_sequence);
+ on_start_search(is_followup);
+ if (max_count > 0)
+ // A fixed number of simulations means that no time limit is used, but
+ // max_time is still used at some places in the code, so we set it to
+ // infinity
+ max_time = numeric_limits<double>::max();
+ m_player = get_player();
+ m_nu_players = get_nu_players();
+ bool clear_tree = true;
+ bool is_same = false;
+ if (is_followup && m_followup_sequence.empty())
+ {
+ is_same = true;
+ is_followup = false;
+ }
+ if (is_same || (is_followup && m_followup_sequence.size() <= m_nu_players))
+ {
+ // Use root_val from last search but with a count of max. 100
+ for (PlayerInt i = 0; i < m_nu_players; ++i)
+ if (m_root_val[i].get_count() > 100)
+ m_root_val[i].init(m_root_val[i].get_mean(), 100);
+ }
+ else
+ for (PlayerInt i = 0; i < m_nu_players; ++i)
+ m_root_val[i].init(SearchParamConst::tie_value, 1);
+ if ((m_reuse_subtree && is_followup) || (m_reuse_tree && is_same))
+ {
+ size_t tree_nodes = m_tree.get_nu_nodes();
+ if (m_followup_sequence.empty())
+ {
+ if (tree_nodes > 1)
+ LIBBOARDGAME_LOG("Reusing all ", tree_nodes, "nodes (count=",
+ m_tree.get_root().get_visit_count(), ")");
+ }
+ else
+ {
+ Timer timer(time_source);
+ m_tmp_tree.clear();
+ auto node = find_node(m_tree, m_followup_sequence);
+ if (node)
+ {
+ m_tree.extract_subtree(m_tmp_tree, *node);
+ auto& tmp_tree_root = m_tmp_tree.get_root();
+ if (! is_same)
+ {
+ Float value, count;
+ if (estimate_reused_root_val(m_tmp_tree, tmp_tree_root,
+ value, count))
+ m_root_val[m_player].add(value, count);
+ }
+ size_t tmp_tree_nodes = m_tmp_tree.get_nu_nodes();
+ if (tree_nodes > 1 && tmp_tree_nodes > 1)
+ {
+ double time = timer();
+ LIBBOARDGAME_LOG("Reusing ", tmp_tree_nodes, " nodes (",
+ std::fixed, setprecision(1),
+ 100 * double(tmp_tree_nodes)
+ / double(tree_nodes),
+ "% tm=", setprecision(4), time, ")");
+ m_tree.swap(m_tmp_tree);
+ clear_tree = false;
+ max_time -= time;
+ if (max_time < 0)
+ max_time = 0;
+ }
+ }
+ }
+ }
+ if (clear_tree)
+ m_tree.clear();
+
+ m_timer.reset(time_source);
+ m_time_source = &time_source;
+ if (SearchParamConst::use_lgr && ! is_followup)
+ m_lgr.init(m_nu_players);
+ for (auto& i : m_threads)
+ {
+ auto& thread_state = i->thread_state;
+ thread_state.stat_len.clear();
+ thread_state.stat_in_tree_len.clear();
+ thread_state.state->start_search();
+ }
+ m_max_count = max_count;
+ m_min_simulations = min_simulations;
+ m_max_time = max_time;
+ m_nu_simulations.store(0);
+ Float prune_min_count = SearchParamConst::prune_count_start;
+
+ // Don't use multi-threading for very short searches (less than 0.5s).
+ auto reused_count = m_tree.get_root().get_visit_count();
+ unsigned nu_threads = m_nu_threads;
+ double expected_time;
+ if (max_count > 0)
+ expected_time =
+ (max_count - reused_count)
+ / SearchParamConst::expected_sim_per_sec;
+ else
+ expected_time = max_time;
+ if (nu_threads > 1 && expected_time < 0.5)
+ {
+ LIBBOARDGAME_LOG("Using single-threading for short search");
+ nu_threads = 1;
+ }
+
+ auto& thread_state_0 = m_threads[0]->thread_state;
+ auto& root = m_tree.get_root();
+ if (! root.has_children())
+ {
+ const Node* best_child;
+ thread_state_0.state->start_simulation(0);
+ thread_state_0.state->finish_in_tree();
+ expand_node(thread_state_0, root, best_child);
+ }
+
+ if (root.get_nu_children() == 0)
+ LIBBOARDGAME_LOG("No legal moves at root");
+ else if (root.get_nu_children() == 1 && min_simulations == 0)
+ LIBBOARDGAME_LOG("Root has only one child");
+ else
+ while (true)
+ {
+ for (unsigned i = 1; i < nu_threads; ++i)
+ m_threads[i]->start_search();
+ search_loop(thread_state_0);
+ for (unsigned i = 1; i < nu_threads; ++i)
+ m_threads[i]->wait_search_finished();
+ bool is_out_of_mem = false;
+ for (unsigned i = 0; i < nu_threads; ++i)
+ if (m_threads[i]->thread_state.is_out_of_mem)
+ {
+ is_out_of_mem = true;
+ break;
+ }
+ if (! is_out_of_mem)
+ break;
+ double time = m_timer();
+ prune(time_source, time, prune_min_count, prune_min_count);
+ }
+
+ m_last_time = m_timer();
+ LIBBOARDGAME_LOG(get_info());
+ bool result = select_move(mv);
+ m_time_source = nullptr;
+ return result;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::search_loop(ThreadState& thread_state)
+{
+ auto& state = *thread_state.state;
+ auto& simulation = thread_state.simulation;
+ simulation.nodes.assign(&m_tree.get_root());
+ simulation.moves.clear();
+ double time_interval = 0.1;
+ if (m_max_count == 0 && m_max_time < 1)
+ time_interval = 0.1 * m_max_time;
+ IntervalChecker expensive_abort_checker(
+ *m_time_source, time_interval,
+ bind(&SearchBase::check_abort_expensive, this,
+ ref(thread_state)));
+ if (m_deterministic)
+ {
+ unsigned interval =
+ static_cast<unsigned>(
+ max(1.0, SearchParamConst::expected_sim_per_sec / 5.0));
+ expensive_abort_checker.set_deterministic(interval);
+ }
+ while (true)
+ {
+ thread_state.is_out_of_mem = false;
+ if ((check_abort(thread_state) || expensive_abort_checker())
+ && m_nu_simulations >= m_min_simulations)
+ break;
+ state.start_simulation(m_nu_simulations.fetch_add(1));
+ play_in_tree(thread_state);
+ if (thread_state.is_out_of_mem)
+ break;
+ playout(thread_state);
+ state.evaluate_playout(simulation.eval);
+ thread_state.stat_len.add(double(simulation.moves.size()));
+ update_values(thread_state);
+ if (SearchParamConst::rave)
+ update_rave(thread_state);
+ if (SearchParamConst::use_lgr)
+ update_lgr(thread_state);
+ }
+}
+
+template<class S, class M, class R>
+inline auto SearchBase<S, M, R>::select_child(const Node& node) -> const Node*
+{
+ auto children = m_tree.get_children(node);
+ LIBBOARDGAME_ASSERT(! children.empty());
+ auto parent_count = node.get_visit_count();
+ Float bias_factor = m_exploration_constant * sqrt(parent_count);
+ static_assert(SearchParamConst::child_min_count > 0, "");
+ auto bias_limit = bias_factor / SearchParamConst::child_min_count;
+ auto i = children.begin();
+ auto value = i->get_value() + bias_factor / i->get_value_count();
+ auto best_value = value;
+ auto best_child = i;
+ auto limit = best_value - bias_limit;
+ while (++i != children.end())
+ {
+ value = i->get_value();
+ if (value <= limit)
+ continue;
+ value += bias_factor / i->get_value_count();
+ if (value > best_value)
+ {
+ best_value = value;
+ best_child = i;
+ limit = best_value - bias_limit;
+ }
+ }
+ return best_child;
+}
+
+template<class S, class M, class R>
+auto SearchBase<S, M, R>::select_final() const-> const Node*
+{
+ // Select the child with the highest number of wins
+ auto children = m_tree.get_children(m_tree.get_root());
+ if (children.empty())
+ return nullptr;
+ auto i = children.begin();
+ auto best_child = i;
+ auto max_wins = i->get_value_count() * i->get_value();
+ while (++i != children.end())
+ {
+ auto wins = i->get_value_count() * i->get_value();
+ if (wins > max_wins)
+ {
+ max_wins = wins;
+ best_child = i;
+ }
+ }
+ return best_child;
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::select_move(Move& mv) const
+{
+ auto child = select_final();
+ if (child)
+ {
+ mv = child->get_move();
+ return true;
+ }
+ else
+ return false;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_callback(function<void(double, double)> callback)
+{
+ m_callback = callback;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_expand_threshold(Float n)
+{
+ m_expand_threshold = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_expand_threshold_inc(Float n)
+{
+ m_expand_threshold_inc = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_rave_parent_max(Float n)
+{
+ m_rave_parent_max = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_rave_child_max(Float n)
+{
+ m_rave_child_max = n;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_rave_weight(Float v)
+{
+ m_rave_weight = v;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_reuse_subtree(bool enable)
+{
+ m_reuse_subtree = enable;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_reuse_tree(bool enable)
+{
+ m_reuse_tree = enable;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::update_lgr(ThreadState& thread_state)
+{
+ const auto& simulation = thread_state.simulation;
+ auto& eval = simulation.eval;
+ auto max_eval = eval[0];
+ for (PlayerInt i = 1; i < m_nu_players; ++i)
+ max_eval = max(eval[i], max_eval);
+ array<bool,max_players> is_winner;
+ for (PlayerInt i = 0; i < m_nu_players; ++i)
+ // Note: this handles a draw as a win. Without additional information
+ // we cannot make a good decision how to handle draws and some
+ // experiments in Blokus Duo showed (with low confidence) that treating
+ // them as a win for both players is slightly better than treating them
+ // as a loss for both.
+ is_winner[i] = (eval[i] == max_eval);
+ auto& moves = simulation.moves;
+ auto nu_moves = moves.size();
+ Move last = moves.get_unchecked(0).move;
+ Move second_last = Move::null();
+ for (unsigned i = 1; i < nu_moves; ++i)
+ {
+ PlayerMove reply = moves[i];
+ PlayerInt player = reply.player;
+ Move mv = reply.move;
+ if (is_winner[player])
+ m_lgr.store(player, last, second_last, mv);
+ else
+ m_lgr.forget(player, last, second_last, mv);
+ second_last = last;
+ last = mv;
+ }
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::update_rave(ThreadState& thread_state)
+{
+ const auto& state = *thread_state.state;
+ auto& moves = thread_state.simulation.moves;
+ auto nu_moves = static_cast<unsigned>(moves.size());
+ if (nu_moves == 0)
+ return;
+ auto& was_played = thread_state.was_played;
+ auto& first_play = thread_state.first_play;
+ auto& nodes = thread_state.simulation.nodes;
+ unsigned nu_nodes = static_cast<unsigned>(nodes.size());
+ unsigned i = nu_moves - 1;
+ // nu_nodes is at least 2 (including root) because the case of no legal
+ // moves at the root is already handled before running any simulations.
+ LIBBOARDGAME_ASSERT(nu_nodes > 1);
+
+ // Fill was_played and first_play with information from playout moves
+ for ( ; i >= nu_nodes - 1; --i)
+ {
+ auto mv = moves[i];
+ if (state.skip_rave(mv.move))
+ continue;
+ was_played[mv.move.to_int()] = mv.player;
+ first_play[mv.move.to_int()] = i;
+ }
+
+ // Add RAVE values to children of nodes of current simulation
+ while (true)
+ {
+ const auto node = nodes[i];
+ if (node->get_visit_count() > m_rave_parent_max)
+ break;
+ auto mv = moves[i];
+ auto player = mv.player;
+ Float dist_factor;
+ if (SearchParamConst::rave_dist_weighting)
+ dist_factor = 1 / static_cast<Float>(nu_moves - i);
+ auto children = m_tree.get_children(*node);
+ LIBBOARDGAME_ASSERT(! children.empty());
+ auto it = children.begin();
+ do
+ {
+ auto mv = it->get_move();
+ if (was_played[mv.to_int()] != player
+ || it->get_value_count() > m_rave_child_max)
+ continue;
+ auto first = first_play[mv.to_int()];
+ LIBBOARDGAME_ASSERT(first > i);
+ Float weight = m_rave_weight;
+ if (SearchParamConst::rave_dist_weighting)
+ weight *= 1 - static_cast<Float>(first - i) * dist_factor;
+ m_tree.add_value(*it, thread_state.simulation.eval[player], weight);
+ }
+ while (++it != children.end());
+ if (i == 0)
+ break;
+ if (! state.skip_rave(mv.move))
+ {
+ was_played[mv.move.to_int()] = player;
+ first_play[mv.move.to_int()] = i;
+ }
+ --i;
+ }
+
+ // Reset was_played
+ while (++i < nu_moves)
+ was_played[moves[i].move.to_int()] = max_players;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::update_values(ThreadState& thread_state)
+{
+ const auto& simulation = thread_state.simulation;
+ auto& nodes = simulation.nodes;
+ auto& eval = simulation.eval;
+ unsigned nu_nodes = static_cast<unsigned>(nodes.size());
+ m_tree.inc_visit_count(*nodes[0]);
+ for (unsigned i = 1; i < nu_nodes; ++i)
+ {
+ auto& node = *nodes[i];
+ auto mv = simulation.moves[i - 1];
+ if (multithread && SearchParamConst::virtual_loss)
+ // Note that this could become problematic if the number of threads
+ // is large. The lock-free algorithm intentionally ignores lost or
+ // partial updates to run faster. But the probability that adding
+ // a virtual loss is lost is not the same as that its removal is
+ // lost because the removal is done in this function with many
+ // calls to add_value() but the adding is done in play_in_tree().
+ // This could introduce a systematic error.
+ m_tree.add_value_remove_loss(node, eval[mv.player]);
+ else
+ m_tree.add_value(node, eval[mv.player]);
+ m_tree.inc_visit_count(node);
+ }
+ for (PlayerInt i = 0; i < m_nu_players; ++i)
+ m_root_val[i].add(eval[i]);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_SEARCH_BASE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/Tree.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_MCTS_TREE_H
+#define LIBBOARDGAME_MCTS_TREE_H
+
+#include <algorithm>
+#include <memory>
+#include "Node.h"
+#include "libboardgame_util/Abort.h"
+#include "libboardgame_util/IntervalChecker.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+using libboardgame_util::get_abort;
+using libboardgame_util::IntervalChecker;
+
+//-----------------------------------------------------------------------------
+
+/** %Tree for Monte-Carlo tree search.
+ The nodes can be modified only through member functions of this class,
+ so that it can guarantee an intact tree structure. The user has access to
+ all nodes, but only as const references.<p>
+ The tree uses separate parts of the node storage for different threads,
+ so it can be used without locking in multi-threaded search. Not all
+ functions are thread-safe, only the ones that are used during a search
+ (e.g. expanding a node is thread-safe, but clear() is not) */
+template<typename N>
+class Tree
+{
+ struct ThreadStorage;
+
+ friend class NodeExpander;
+
+public:
+ typedef N Node;
+
+ typedef typename Node::Move Move;
+
+ typedef typename Node::Float Float;
+
+ /** Range for iterating over the children of a node. */
+ class Children
+ {
+ public:
+ Children(const Tree& tree, const Node& node)
+ {
+ auto nu_children = node.get_nu_children();
+ m_begin = (nu_children != 0 ?
+ &tree.get_node(node.get_first_child()) : nullptr);
+ m_end = m_begin + nu_children;
+ }
+
+ const Node* begin() const
+ {
+ return m_begin;
+ }
+
+ const Node* end() const
+ {
+ return m_end;
+ }
+
+ bool empty() const
+ {
+ return m_begin == nullptr;
+ }
+
+ private:
+ const Node* m_begin;
+
+ const Node* m_end;
+ };
+
+
+ /** Helper class that is passed to the search state during node expansion.
+ This class allows the search state to directly create children of a
+ node at the node expansion, so that copying to a temporary move list
+ is not necessary, but avoids that the search needs to expose a
+ non-const reference to the tree to the state. */
+ class NodeExpander
+ {
+ public:
+ /** Constructor.
+ @param thread_id
+ @param tree
+ @param child_min_count The minimum count used for initializing
+ children. Used only in debug mode to assert that the children
+ are really initialized with a minimum count as declared with
+ SearchParamConst::child_min_count. */
+ NodeExpander(unsigned thread_id, Tree& tree, Float child_min_count);
+
+ /** Check if the tree still has the capacity for a given number
+ of children. */
+ bool check_capacity(unsigned short nu_children) const;
+
+ /** Add new child.
+ It needs to be checked first with check_capacity() that the tree
+ has enough capacity. */
+ void add_child(const Move& mv, Float value, Float count);
+
+ /** Link the children to the parent node. */
+ void link_children(Tree& tree, const Node& node);
+
+ /** Return the node to play after the node expansion.
+ This returns the child with the highest value if prior knowledge
+ was used, or the first child, or null if no children. This can be
+ used for avoiding and extra iteration over the children when
+ selecting a child after a node expansion. */
+ const Node* get_best_child() const;
+
+ private:
+ ThreadStorage& m_thread_storage;
+
+ Float m_best_value = -numeric_limits<Float>::max();
+
+ const Node* m_first_child;
+
+ const Node* m_best_child;
+
+#if LIBBOARDGAME_DEBUG
+ Float m_child_min_count;
+#endif
+ };
+
+ Tree(size_t memory, unsigned nu_threads);
+
+ ~Tree();
+
+ /** Remove all nodes but the root node. */
+ void clear();
+
+ const Node& get_root() const;
+
+ Children get_children(const Node& node) const
+ {
+ return Children(*this, node);
+ }
+
+ Children get_root_children() const
+ {
+ return get_children(get_root());
+ }
+
+ size_t get_nu_nodes() const;
+
+ const Node& get_node(NodeIdx i) const;
+
+ void link_children(const Node& node, const Node* first_child,
+ unsigned short nu_children);
+
+ void add_value(const Node& node, Float v);
+
+ void add_value(const Node& node, Float v, Float weight);
+
+ void add_value_remove_loss(const Node& node, Float v);
+
+ void inc_visit_count(const Node& node);
+
+ void swap(Tree& tree);
+
+ /** Extract a subtree.
+ Note that you still have to re-initialize the value of the subtree
+ after the extraction because the value of the root node and the values
+ of inner nodes have a different meaning.
+ @pre Target tree is empty (! target.get_root().has_children())
+ @param target The target tree
+ @param node The root node of the subtree. */
+ void extract_subtree(Tree& target, const Node& node) const;
+
+ /** Copy a subtree.
+ The caller is responsible that the trees have the same number of
+ maximum nodes and that the target tree has room for the subtree.
+ @param target The target tree
+ @param target_node The target node
+ @param node The root node of the subtree.
+ @param min_count Don't copy subtrees of nodes below this count */
+ void copy_subtree(Tree& target, const Node& target_node, const Node& node,
+ Float min_count) const;
+
+private:
+ struct ThreadStorage
+ {
+ Node* begin;
+
+ Node* end;
+
+ Node* next;
+ };
+
+
+ unique_ptr<Node[]> m_nodes;
+
+ unique_ptr<ThreadStorage[]> m_thread_storage;
+
+ unsigned m_nu_threads;
+
+ size_t m_max_nodes;
+
+ size_t m_nodes_per_thread;
+
+
+ bool contains(const Node& node) const;
+
+ void copy_recurse(Tree& target, const Node& target_node, const Node& node,
+ Float min_count) const;
+
+ unsigned get_thread_storage(const Node& node) const;
+
+ Node& non_const(const Node& node) const;
+};
+
+template<typename N>
+inline Tree<N>::NodeExpander::NodeExpander(unsigned thread_id, Tree& tree,
+ Float child_min_count)
+ : m_thread_storage(tree.m_thread_storage[thread_id]),
+ m_first_child(m_thread_storage.next),
+ m_best_child(nullptr)
+{
+ LIBBOARDGAME_ASSERT(thread_id < tree.m_nu_threads);
+#if LIBBOARDGAME_DEBUG
+ m_child_min_count = child_min_count;
+#else
+ LIBBOARDGAME_UNUSED(child_min_count);
+#endif
+}
+
+template<typename N>
+inline void Tree<N>::NodeExpander::add_child(const Move& mv, Float value,
+ Float count)
+{
+ // -numeric_limits<Float>::max() ist init value for m_best_value
+ LIBBOARDGAME_ASSERT(value > -numeric_limits<Float>::max());
+ LIBBOARDGAME_ASSERT(count >= m_child_min_count);
+ auto& next = m_thread_storage.next;
+ LIBBOARDGAME_ASSERT(next < m_thread_storage.end);
+ next->init(mv, value, count);
+ if (value > m_best_value)
+ {
+ m_best_child = next;
+ m_best_value = value;
+ }
+ ++next;
+}
+
+template<typename N>
+inline bool Tree<N>::NodeExpander::check_capacity(
+ unsigned short nu_children) const
+{
+ return m_thread_storage.end - m_thread_storage.next >= nu_children;
+}
+
+template<typename N>
+inline auto Tree<N>::NodeExpander::get_best_child() const -> const Node*
+{
+ return m_best_child;
+}
+
+template<typename N>
+inline auto Tree<N>::get_node(NodeIdx i) const -> const Node&
+{
+ return m_nodes[i];
+}
+
+template<typename N>
+inline void Tree<N>::NodeExpander::link_children(Tree& tree, const Node& node)
+{
+ auto nu_children =
+ static_cast<unsigned short>(m_thread_storage.next - m_first_child);
+ tree.link_children(node, m_first_child, nu_children);
+}
+
+
+template<typename N>
+Tree<N>::Tree(size_t memory, unsigned nu_threads)
+ : m_nu_threads(nu_threads)
+{
+ size_t max_nodes = memory / sizeof(Node);
+ // It doesn't make sense to set max_nodes higher than what can be accessed
+ // with NodeIdx
+ max_nodes =
+ min(max_nodes, static_cast<size_t>(numeric_limits<NodeIdx>::max()));
+ if (max_nodes == 0)
+ // We need at least the root node (for useful searches we need of
+ // course also children, but a root node is the minimum requirement to
+ // avoid crashing).
+ max_nodes = 1;
+ m_max_nodes = max_nodes;
+ m_nodes.reset(new Node[max_nodes]);
+ m_thread_storage.reset(new ThreadStorage[m_nu_threads]);
+ m_nodes_per_thread = max_nodes / m_nu_threads;
+ for (unsigned i = 0; i < m_nu_threads; ++i)
+ {
+ auto& thread_storage = m_thread_storage[i];
+ thread_storage.begin = m_nodes.get() + i * m_nodes_per_thread;
+ thread_storage.end = thread_storage.begin + m_nodes_per_thread;
+ }
+ clear();
+}
+
+template<typename N>
+Tree<N>::~Tree() = default;
+
+template<typename N>
+inline void Tree<N>::add_value(const Node& node, Float v)
+{
+ non_const(node).add_value(v);
+}
+
+template<typename N>
+inline void Tree<N>::add_value(const Node& node, Float v, Float weight)
+{
+ non_const(node).add_value(v, weight);
+}
+
+template<typename N>
+void Tree<N>::clear()
+{
+ m_thread_storage[0].next = m_thread_storage[0].begin + 1;
+ for (unsigned i = 1; i < m_nu_threads; ++i)
+ m_thread_storage[i].next = m_thread_storage[i].begin;
+ m_nodes[0].init_root();
+}
+
+template<typename N>
+bool Tree<N>::contains(const Node& node) const
+{
+ return &node >= m_nodes.get() && &node < m_nodes.get() + m_max_nodes;
+}
+
+template<typename N>
+void Tree<N>::copy_subtree(Tree& target, const Node& target_node,
+ const Node& node, Float min_count) const
+{
+ target.non_const(target_node).copy_data_from(node);
+ if (node.has_children())
+ copy_recurse(target, target_node, node, min_count);
+ else
+ target.non_const(target_node).unlink_children_st();
+}
+
+template<typename N>
+void Tree<N>::copy_recurse(Tree& target, const Node& target_node,
+ const Node& node, Float min_count) const
+{
+ LIBBOARDGAME_ASSERT(target.m_max_nodes == m_max_nodes);
+ LIBBOARDGAME_ASSERT(target.m_nu_threads == m_nu_threads);
+ LIBBOARDGAME_ASSERT(contains(node));
+ auto nu_children = node.get_nu_children();
+ auto& first_child = get_node(node.get_first_child());
+ // Create target children in the equivalent thread storage as in source.
+ // This ensures that the thread storage will not overflow (because the
+ // trees have identical nu_threads/max_nodes)
+ ThreadStorage& thread_storage =
+ target.m_thread_storage[get_thread_storage(first_child)];
+ auto target_child = thread_storage.next;
+ auto target_first_child =
+ static_cast<NodeIdx>(target_child - target.m_nodes.get());
+ target.non_const(target_node).link_children_st(target_first_child,
+ nu_children);
+ thread_storage.next += nu_children;
+ // Without the extra () around thread_storage.next in the following
+ // assert, GCC 4.7.2 gives the error: parse error in template argument list
+ LIBBOARDGAME_ASSERT((thread_storage.next) < thread_storage.end);
+ auto end = &first_child + node.get_nu_children();
+ for (auto i = &first_child; i != end; ++i, ++target_child)
+ {
+ target_child->copy_data_from(*i);
+ if (! i->has_children() || i->get_visit_count() < min_count)
+ {
+ target_child->unlink_children_st();
+ continue;
+ }
+ copy_recurse(target, *target_child, *i, min_count);
+ }
+}
+
+template<typename N>
+void Tree<N>::extract_subtree(Tree& target, const Node& node) const
+{
+ LIBBOARDGAME_ASSERT(contains(node));
+ LIBBOARDGAME_ASSERT(&target != this);
+ LIBBOARDGAME_ASSERT(target.m_max_nodes == m_max_nodes);
+ LIBBOARDGAME_ASSERT(! target.get_root().has_children());
+ copy_subtree(target, target.m_nodes[0], node, 0);
+}
+
+template<typename N>
+size_t Tree<N>::get_nu_nodes() const
+{
+ size_t result = 0;
+ for (unsigned i = 0; i < m_nu_threads; ++i)
+ {
+ auto& thread_storage = m_thread_storage[i];
+ result += thread_storage.next - thread_storage.begin;
+ }
+ return result;
+}
+
+template<typename N>
+inline auto Tree<N>::get_root() const -> const Node&
+{
+ return m_nodes[0];
+}
+
+/** Get the thread storage a node belongs to. */
+template<typename N>
+inline unsigned Tree<N>::get_thread_storage(const Node& node) const
+{
+ size_t diff = &node - m_nodes.get();
+ return static_cast<unsigned>(diff / m_nodes_per_thread);
+}
+
+template<typename N>
+inline void Tree<N>::inc_visit_count(const Node& node)
+{
+ non_const(node).inc_visit_count();
+}
+
+template<typename N>
+inline void Tree<N>::link_children(const Node& node, const Node* first_child,
+ unsigned short nu_children)
+{
+ NodeIdx first_child_idx = static_cast<NodeIdx>(first_child - m_nodes.get());
+ LIBBOARDGAME_ASSERT(first_child_idx > 0);
+ LIBBOARDGAME_ASSERT(first_child_idx < m_max_nodes);
+ non_const(node).link_children(first_child_idx, nu_children);
+}
+
+/** Convert a const reference to node from user to a non-const reference.
+ The user has only read access to the nodes, because the tree guarantees
+ the validity of the tree structure. */
+template<typename N>
+inline auto Tree<N>::non_const(const Node& node) const -> Node&
+{
+ LIBBOARDGAME_ASSERT(contains(node));
+ return const_cast<Node&>(node);
+}
+
+template<typename N>
+inline void Tree<N>::add_value_remove_loss(const Node& node, Float v)
+{
+ non_const(node).add_value_remove_loss(v);
+}
+
+template<typename N>
+void Tree<N>::swap(Tree& tree)
+{
+ // Reminder to update this function when the class gets additional members
+ struct Dummy
+ {
+ unsigned m_nu_threads;
+ size_t m_max_nodes;
+ size_t m_nodes_per_thread;
+ unique_ptr<ThreadStorage> m_thread_storage;
+ unique_ptr<Node[]> m_nodes;
+ };
+ static_assert(sizeof(Tree) == sizeof(Dummy), "");
+ std::swap(m_nu_threads, tree.m_nu_threads);
+ std::swap(m_max_nodes, tree.m_max_nodes);
+ std::swap(m_nodes_per_thread, tree.m_nodes_per_thread);
+ m_thread_storage.swap(tree.m_thread_storage);
+ m_nodes.swap(tree.m_nodes);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_TREE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_mcts/TreeUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_MCTS_TREE_UTIL_H
+#define LIBBOARDGAME_MCTS_TREE_UTIL_H
+
+#include "Tree.h"
+
+namespace libboardgame_mcts {
+namespace tree_util {
+
+//-----------------------------------------------------------------------------
+
+template<typename N>
+const N* find_child(const Tree<N>& tree, const N& node, typename N::Move mv)
+{
+ for (auto& i : tree.get_children(node))
+ if (i.get_move() == mv)
+ return &i;
+ return nullptr;
+}
+
+template<typename N, class S>
+const N* find_node(const Tree<N>& tree, const S& sequence)
+{
+ auto node = &tree.get_root();
+ for (auto mv : sequence)
+ if (! ((node = find_child(tree, *node, mv))))
+ break;
+ return node;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace tree_util
+} // namespace libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_TREE_UTIL_H
--- /dev/null
+add_library(boardgame_sgf STATIC
+ InvalidPropertyValue.h
+ InvalidTree.h
+ MissingProperty.h
+ MissingProperty.cpp
+ Reader.h
+ Reader.cpp
+ SgfNode.h
+ SgfNode.cpp
+ SgfTree.h
+ SgfTree.cpp
+ SgfUtil.h
+ SgfUtil.cpp
+ TreeReader.h
+ TreeReader.cpp
+ TreeWriter.h
+ TreeWriter.cpp
+ Writer.h
+ Writer.cpp
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/InvalidPropertyValue.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H
+#define LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H
+
+#include "InvalidTree.h"
+
+#include "sstream"
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class InvalidPropertyValue
+ : public InvalidTree
+{
+public:
+ template<typename T>
+ InvalidPropertyValue(const string& id, const T& value);
+
+private:
+ template<typename T>
+ static string get_message(const string& id, const T& value);
+};
+
+template<typename T>
+InvalidPropertyValue::InvalidPropertyValue(const string& id, const T& value)
+ : InvalidTree(get_message(id, value))
+{
+}
+
+template<typename T>
+string InvalidPropertyValue::get_message(const string& id, const T& value)
+{
+ ostringstream msg;
+ msg << "Invalid value '" << value << " for SGF property '" << id << "'";
+ return msg.str();
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_INVALID_PROPERTY_VALUE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/InvalidTree.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_INVALID_TREE_H
+#define LIBBOARDGAME_SGF_INVALID_TREE_H
+
+#include <stdexcept>
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Exception indication a semantic error in the tree.
+ This exception is used for semantic errors in SGF trees. If a SGF tree
+ is loaded from an external file, it is usually only checked for
+ (game-independent) syntax errors, but not for semantic errors (e.g. illegal
+ moves) because that would be too expensive when loading large trees and
+ not allow the user to partially use a tree if there is an error only in
+ some variations. As a consequence, functions that use the tree may cause
+ errors later (e.g. when trying to update the game state to a node in the
+ tree). In this case, they should throw InvalidTree. */
+class InvalidTree
+ : public runtime_error
+{
+ using runtime_error::runtime_error;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_INVALID_TREE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/MissingProperty.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "MissingProperty.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+MissingProperty::MissingProperty(const string& message)
+ : InvalidTree("Missing SGF property: " + message)
+{
+}
+
+MissingProperty::MissingProperty(const string& id, const string& message)
+ : InvalidTree("Missing SGF property '" + id + ": " + message)
+{
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/MissingProperty.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_MISSING_PROPERTY_H
+#define LIBBOARDGAME_SGF_MISSING_PROPERTY_H
+
+#include "InvalidTree.h"
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class MissingProperty
+ : public InvalidTree
+{
+public:
+ explicit MissingProperty(const string& message);
+
+ MissingProperty(const string& id, const string& message);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_MISSING_PROPERTY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/Reader.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Reader.h"
+
+#include <cctype>
+#include <cstdio>
+#include <fstream>
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** Replacement for std::isspace() that returns true only for whitespaces
+ in the ASCII range. */
+bool is_ascii_space(int c)
+{
+ return c >= 0 && c < 128 && isspace(c);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+Reader::Reader() = default;
+
+Reader::~Reader() = default;
+
+void Reader::consume_char(char expected)
+{
+ LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(expected);
+ char c = read_char();
+ LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(c);
+ LIBBOARDGAME_ASSERT(c == expected);
+}
+
+void Reader::consume_whitespace()
+{
+ while (is_ascii_space(peek()))
+ m_in->get();
+}
+
+void Reader::on_begin_node(bool is_root)
+{
+ // Default implementation does nothing
+ LIBBOARDGAME_UNUSED(is_root);
+}
+
+void Reader::on_begin_tree(bool is_root)
+{
+ // Default implementation does nothing
+ LIBBOARDGAME_UNUSED(is_root);
+}
+
+void Reader::on_end_node()
+{
+ // Default implementation does nothing
+}
+
+void Reader::on_end_tree(bool is_root)
+{
+ // Default implementation does nothing
+ LIBBOARDGAME_UNUSED(is_root);
+}
+
+void Reader::on_property(const string& id, const vector<string>& values)
+{
+ // Default implementation does nothing
+ LIBBOARDGAME_UNUSED(id);
+ LIBBOARDGAME_UNUSED(values);
+}
+
+char Reader::peek()
+{
+ int c = m_in->peek();
+ if (c == EOF)
+ throw ReadError("Unexpected end of input");
+ return char(c);
+}
+
+void Reader::read(istream& in, bool check_single_tree,
+ bool* more_game_trees_left)
+{
+ m_in = ∈
+ m_is_in_main_variation = true;
+ consume_whitespace();
+ read_tree(true);
+ while (true)
+ {
+ int c = m_in->peek();
+ if (c == EOF)
+ {
+ if (more_game_trees_left)
+ *more_game_trees_left = false;
+ return;
+ }
+ else if (c == '(')
+ {
+ if (check_single_tree)
+ throw ReadError("Input has multiple game trees");
+ else
+ {
+ if (more_game_trees_left)
+ *more_game_trees_left = true;
+ return;
+ }
+ }
+ else if (is_ascii_space(c))
+ m_in->get();
+ else
+ throw ReadError("Extra characters after end of tree.");
+ }
+}
+
+void Reader::read(const string& file)
+{
+ ifstream in(file);
+ if (! in)
+ throw ReadError("Could not open '" + file + "'");
+ try
+ {
+ read(in, true);
+ }
+ catch (const ReadError& e)
+ {
+ throw ReadError("Could not read '" + file + "': " + e.what());
+ }
+}
+
+char Reader::read_char()
+{
+ int c = m_in->get();
+ if (c == EOF)
+ throw ReadError("Unexpected end of SGF stream");
+ if (c == '\r')
+ {
+ // Convert CR+LF or single CR into LF
+ if (peek() == '\n')
+ m_in->get();
+ return '\n';
+ }
+ return char(c);
+}
+
+void Reader::read_expected(char expected)
+{
+ if (read_char() != expected)
+ throw ReadError(string("Expected '") + expected + "'");
+}
+
+void Reader::read_node(bool is_root)
+{
+ read_expected(';');
+ if (! m_read_only_main_variation || m_is_in_main_variation)
+ on_begin_node(is_root);
+ while (true)
+ {
+ consume_whitespace();
+ char c = peek();
+ if (c == '(' || c == ')' || c == ';')
+ break;
+ read_property();
+ }
+ if (! m_read_only_main_variation || m_is_in_main_variation)
+ on_end_node();
+}
+
+void Reader::read_property()
+{
+ if (m_read_only_main_variation && ! m_is_in_main_variation)
+ {
+ while (peek() != '[')
+ read_char();
+ while (peek() == '[')
+ {
+ consume_char('[');
+ bool escape = false;
+ while (peek() != ']' || escape)
+ {
+ char c = read_char();
+ if (c == '\\' && ! escape)
+ {
+ escape = true;
+ continue;
+ }
+ escape = false;
+ }
+ consume_char(']');
+ consume_whitespace();
+ }
+ }
+ else
+ {
+ m_id.clear();
+ while (peek() != '[')
+ m_id += read_char();
+ m_values.clear();
+ while (peek() == '[')
+ {
+ consume_char('[');
+ m_value.clear();
+ bool escape = false;
+ while (peek() != ']' || escape)
+ {
+ char c = read_char();
+ if (c == '\\' && ! escape)
+ {
+ escape = true;
+ continue;
+ }
+ escape = false;
+ m_value += c;
+ }
+ consume_char(']');
+ consume_whitespace();
+ m_values.push_back(m_value);
+ }
+ on_property(m_id, m_values);
+ }
+}
+
+void Reader::read_tree(bool is_root)
+{
+ read_expected('(');
+ on_begin_tree(is_root);
+ bool was_root = is_root;
+ while (true)
+ {
+ consume_whitespace();
+ char c = peek();
+ if (c == ')')
+ break;
+ else if (c == ';')
+ {
+ read_node(is_root);
+ is_root = false;
+ }
+ else if (c == '(')
+ read_tree(false);
+ else
+ throw ReadError("Extra text before node");
+ }
+ read_expected(')');
+ m_is_in_main_variation = false;
+ on_end_tree(was_root);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/Reader.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_READER_H
+#define LIBBOARDGAME_SGF_READER_H
+
+#include <iosfwd>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class Reader
+{
+public:
+ class ReadError
+ : public runtime_error
+ {
+ using runtime_error::runtime_error;
+ };
+
+ Reader();
+
+ virtual ~Reader();
+
+ virtual void on_begin_tree(bool is_root);
+
+ virtual void on_end_tree(bool is_root);
+
+ virtual void on_begin_node(bool is_root);
+
+ virtual void on_end_node();
+
+ virtual void on_property(const string& id, const vector<string>& values);
+
+ /** Read only the main variation.
+ Reduces CPU time and memory if only the main variation is needed. */
+ void set_read_only_main_variation(bool enable);
+
+ /** Read a game tree from the file.
+ @param in
+ @param check_single_tree Throw an error if non-whitespace characters
+ follow after the tree before the end of the stream. This is mainly
+ useful to ensure that the input is not a SGF file with multiple game
+ trees if the caller does not want to handle this case. If
+ check_single_tree is false, you can call read() multiple times to read
+ all game trees.
+ @param[out] more_game_trees_left set to true if check_single_tree is
+ false and there are more game trees to read.
+ @throws ReadError */
+ void read(istream& in, bool check_single_tree = true,
+ bool* more_game_trees_left = nullptr);
+
+ /** See read(istream&,bool) */
+ void read(const string& file);
+
+private:
+ bool m_read_only_main_variation = false;
+
+ bool m_is_in_main_variation;
+
+ istream* m_in;
+
+ /** Local variable in read_property().
+ Reused for efficiency. */
+ string m_id;
+
+ /** Local variable in read_property().
+ Reused for efficiency. */
+ string m_value;
+
+ /** Local variable in read_property().
+ Reused for efficiency. */
+ vector<string> m_values;
+
+ void consume_char(char expected);
+
+ void consume_whitespace();
+
+ char peek();
+
+ char read_char();
+
+ void read_expected(char expected);
+
+ void read_node(bool is_root);
+
+ void read_property();
+
+ void read_tree(bool is_root);
+};
+
+inline void Reader::set_read_only_main_variation(bool enable)
+{
+ m_read_only_main_variation = enable;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_READER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfNode.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "SgfNode.h"
+
+#include <algorithm>
+#include "MissingProperty.h"
+#include "libboardgame_util/Assert.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+Property::~Property() = default;
+
+//-----------------------------------------------------------------------------
+
+SgfNode::SgfNode() = default;
+
+SgfNode::~SgfNode() = default;
+
+void SgfNode::append(unique_ptr<SgfNode> node)
+{
+ node->m_parent = this;
+ if (! m_first_child)
+ m_first_child = move(node);
+ else
+ get_last_child()->m_sibling = move(node);
+}
+
+SgfNode& SgfNode::create_new_child()
+{
+ unique_ptr<SgfNode> node(new SgfNode);
+ node->m_parent = this;
+ SgfNode& result = *(node.get());
+ auto last_child = get_last_child();
+ if (! last_child)
+ m_first_child = move(node);
+ else
+ last_child->m_sibling = move(node);
+ return result;
+}
+
+void SgfNode::delete_variations()
+{
+ if (m_first_child)
+ m_first_child->m_sibling.reset(nullptr);
+}
+
+forward_list<Property>::const_iterator SgfNode::find_property(
+ const string& id) const
+{
+ return find_if(m_properties.begin(), m_properties.end(),
+ [&](const Property& p) { return p.id == id; });
+}
+
+const vector<string> SgfNode::get_multi_property(const string& id) const
+{
+ auto property = find_property(id);
+ if (property == m_properties.end())
+ return vector<string>();
+ else
+ return property->values;
+}
+
+bool SgfNode::has_property(const string& id) const
+{
+ return find_property(id) != m_properties.end();
+}
+
+const SgfNode& SgfNode::get_child(unsigned i) const
+{
+ LIBBOARDGAME_ASSERT(i < get_nu_children());
+ auto child = m_first_child.get();
+ while (i > 0)
+ {
+ child = child->m_sibling.get();
+ --i;
+ }
+ return *child;
+}
+
+unsigned SgfNode::get_child_index(const SgfNode& child) const
+{
+ auto current = m_first_child.get();
+ unsigned i = 0;
+ while (true)
+ {
+ if (current == &child)
+ return i;
+ current = current->m_sibling.get();
+ LIBBOARDGAME_ASSERT(current);
+ ++i;
+ }
+}
+
+SgfNode* SgfNode::get_last_child() const
+{
+ auto node = m_first_child.get();
+ if (! node)
+ return nullptr;
+ while (node->m_sibling)
+ node = node->m_sibling.get();
+ return node;
+}
+
+unsigned SgfNode::get_nu_children() const
+{
+ unsigned n = 0;
+ auto child = m_first_child.get();
+ while (child)
+ {
+ ++n;
+ child = child->m_sibling.get();
+ }
+ return n;
+}
+
+const SgfNode* SgfNode::get_previous_sibling() const
+{
+ if (! m_parent)
+ return nullptr;
+ auto child = &m_parent->get_first_child();
+ if (child == this)
+ return nullptr;
+ do
+ {
+ if (child->get_sibling() == this)
+ return child;
+ child = child->get_sibling();
+ }
+ while (child);
+ LIBBOARDGAME_ASSERT(false);
+ return nullptr;
+}
+
+const string& SgfNode::get_property(const string& id) const
+{
+ auto property = find_property(id);
+ if (property == m_properties.end())
+ throw MissingProperty(id);
+ return property->values[0];
+}
+
+const string& SgfNode::get_property(const string& id,
+ const string& default_value) const
+{
+ auto property = find_property(id);
+ if (property == m_properties.end())
+ return default_value;
+ else
+ return property->values[0];
+}
+
+void SgfNode::make_first_child()
+{
+ LIBBOARDGAME_ASSERT(has_parent());
+ auto current_child = m_parent->m_first_child.get();
+ if (current_child == this)
+ return;
+ while (true)
+ {
+ auto sibling = current_child->m_sibling.get();
+ if (sibling == this)
+ {
+ unique_ptr<SgfNode> tmp = move(m_parent->m_first_child);
+ m_parent->m_first_child = move(current_child->m_sibling);
+ current_child->m_sibling = move(m_sibling);
+ m_sibling = move(tmp);
+ return;
+ }
+ current_child = sibling;
+ }
+}
+
+bool SgfNode::move_property_to_front(const string& id)
+{
+ auto i = m_properties.begin();
+ forward_list<Property>::const_iterator previous = m_properties.end();
+ for ( ; i != m_properties.end(); ++i)
+ if (i->id == id)
+ break;
+ else
+ previous = i;
+ if (i == m_properties.begin() || i == m_properties.end())
+ return false;
+ auto property = *i;
+ m_properties.erase_after(previous);
+ m_properties.push_front(property);
+ return true;
+}
+
+void SgfNode::move_down()
+{
+ LIBBOARDGAME_ASSERT(has_parent());
+ auto current = m_parent->m_first_child.get();
+ if (current == this)
+ {
+ unique_ptr<SgfNode> tmp = move(m_parent->m_first_child);
+ m_parent->m_first_child = move(m_sibling);
+ m_sibling = move(m_parent->m_first_child->m_sibling);
+ m_parent->m_first_child->m_sibling = move(tmp);
+ return;
+ }
+ while (true)
+ {
+ auto sibling = current->m_sibling.get();
+ if (sibling == this)
+ {
+ if (! m_sibling)
+ return;
+ unique_ptr<SgfNode> tmp = move(current->m_sibling);
+ current->m_sibling = move(m_sibling);
+ m_sibling = move(current->m_sibling->m_sibling);
+ current->m_sibling->m_sibling = move(tmp);
+ return;
+ }
+ current = sibling;
+ }
+}
+
+void SgfNode::move_up()
+{
+ LIBBOARDGAME_ASSERT(has_parent());
+ auto current = m_parent->m_first_child.get();
+ if (current == this)
+ return;
+ SgfNode* prev = nullptr;
+ while (true)
+ {
+ auto sibling = current->m_sibling.get();
+ if (sibling == this)
+ {
+ if (! prev)
+ {
+ make_first_child();
+ return;
+ }
+ unique_ptr<SgfNode> tmp = move(prev->m_sibling);
+ prev->m_sibling = move(current->m_sibling);
+ current->m_sibling = move(m_sibling);
+ m_sibling = move(tmp);
+ return;
+ }
+ prev = current;
+ current = sibling;
+ }
+}
+
+bool SgfNode::remove_property(const string& id)
+{
+ forward_list<Property>::const_iterator previous = m_properties.end();
+ for (auto i = m_properties.begin() ; i != m_properties.end(); ++i)
+ if (i->id == id)
+ {
+ if (previous == m_properties.end())
+ m_properties.pop_front();
+ else
+ m_properties.erase_after(previous);
+ return true;
+ }
+ else
+ previous = i;
+ return false;
+}
+
+unique_ptr<SgfNode> SgfNode::remove_child(SgfNode& child)
+{
+ auto node = &m_first_child;
+ unique_ptr<SgfNode>* previous = nullptr;
+ while (true)
+ {
+ if (node->get() == &child)
+ {
+ unique_ptr<SgfNode> result = move(*node);
+ if (! previous)
+ m_first_child = move(child.m_sibling);
+ else
+ (*previous)->m_sibling = move(child.m_sibling);
+ result->m_parent = nullptr;
+ return result;
+ }
+ previous = node;
+ node = &(*node)->m_sibling;
+ LIBBOARDGAME_ASSERT(node);
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfNode.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_SGF_NODE_H
+#define LIBBOARDGAME_SGF_SGF_NODE_H
+
+#include <forward_list>
+#include <memory>
+#include <string>
+#include <vector>
+#include "InvalidPropertyValue.h"
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+
+using namespace std;
+using libboardgame_util::from_string;
+using libboardgame_util::to_string;
+
+//-----------------------------------------------------------------------------
+
+struct Property
+{
+ string id;
+
+ vector<string> values;
+
+ unique_ptr<Property> next;
+
+ Property(const Property& p)
+ : id(p.id),
+ values(p.values)
+ { }
+
+ Property(const string& id, const vector<string>& values)
+ : id(id),
+ values(values)
+ {
+ LIBBOARDGAME_ASSERT(! id.empty());
+ LIBBOARDGAME_ASSERT(! values.empty());
+ }
+
+ ~Property();
+};
+
+//-----------------------------------------------------------------------------
+
+class SgfNode
+{
+public:
+ /** Iterates over siblings. */
+ class Iterator
+ {
+ public:
+ explicit Iterator(const SgfNode* node)
+ {
+ m_node = node;
+ }
+
+ bool operator==(Iterator it) const
+ {
+ return m_node == it.m_node;
+ }
+
+ bool operator!=(Iterator it) const
+ {
+ return m_node != it.m_node;
+ }
+
+ Iterator& operator++()
+ {
+ m_node = m_node->get_sibling();
+ return *this;
+ }
+
+ const SgfNode& operator*() const
+ {
+ return *m_node;
+ }
+
+ const SgfNode* operator->() const
+ {
+ return m_node;
+ }
+
+ bool is_null() const
+ {
+ return m_node == nullptr;
+ }
+
+ private:
+ const SgfNode* m_node;
+ };
+
+ /** Range for iterating over the children of a node. */
+ class Children
+ {
+ public:
+ explicit Children(const SgfNode& node)
+ : m_begin(node.get_first_child_or_null())
+ { }
+
+ Iterator begin() const { return m_begin; }
+
+ Iterator end() const { return Iterator(nullptr); }
+
+ bool empty() const { return m_begin.is_null(); }
+
+ private:
+ Iterator m_begin;
+ };
+
+ SgfNode();
+
+ ~SgfNode();
+
+ /** Append a new child. */
+ void append(unique_ptr<SgfNode> node);
+
+ bool has_property(const string& id) const;
+
+ /** Get a property.
+ @pre has_property(id) */
+ const string& get_property(const string& id) const;
+
+ const string& get_property(const string& id,
+ const string& default_value) const;
+
+ const vector<string> get_multi_property(const string& id) const;
+
+ /** Get property parsed as a type.
+ @pre has_property(id)
+ @throws InvalidPropertyValue, MissingProperty */
+ template<typename T>
+ T parse_property(const string& id) const;
+
+ /** Get property parsed as a type with default value.
+ @throws InvalidPropertyValue */
+ template<typename T>
+ T parse_property(const string& id, const T& default_value) const;
+
+ /** @return true, if property was added or changed. */
+ template<typename T>
+ bool set_property(const string& id, const T& value);
+
+ /** @return true, if property was added or changed. */
+ bool set_property(const string& id, const char* value);
+
+ /** @return true, if property was added or changed. */
+ template<typename T>
+ bool set_property(const string& id, const vector<T>& values);
+
+ /** @return true, if node contained the property. */
+ bool remove_property(const string& id);
+
+ /** @return true, if the property was found and not already at the
+ front. */
+ bool move_property_to_front(const string& id);
+
+ const forward_list<Property>& get_properties() const
+ {
+ return m_properties;
+ }
+
+ Children get_children() const
+ {
+ return Children(*this);
+ }
+
+ SgfNode* get_sibling();
+
+ SgfNode& get_first_child();
+
+ const SgfNode& get_first_child() const;
+
+ SgfNode* get_first_child_or_null();
+
+ const SgfNode* get_first_child_or_null() const;
+
+ const SgfNode* get_sibling() const;
+
+ const SgfNode* get_previous_sibling() const;
+
+ bool has_children() const;
+
+ bool has_single_child() const;
+
+ unsigned get_nu_children() const;
+
+ /** @pre i < get_nu_children() */
+ const SgfNode& get_child(unsigned i) const;
+
+ unsigned get_child_index(const SgfNode& child) const;
+
+ /** Get single child.
+ @pre has_single_child() */
+ const SgfNode& get_child() const;
+
+ bool has_parent() const;
+
+ /** Get parent node.
+ @pre has_parent() */
+ const SgfNode& get_parent() const;
+
+ /** Get parent node or null if node has no parent. */
+ const SgfNode* get_parent_or_null() const;
+
+ SgfNode& get_parent();
+
+ SgfNode& create_new_child();
+
+ /** Remove a child.
+ @return The removed child node. */
+ unique_ptr<SgfNode> remove_child(SgfNode& child);
+
+ /** Remove all children.
+ @return A pointer to the first child (which also owns its siblings),
+ which can be used to append the children to a different node. */
+ unique_ptr<SgfNode> remove_children();
+
+ /** @pre has_parent() */
+ void make_first_child();
+
+ /** Switch place with previous sibling.
+ If the node is already the first child, nothing happens.
+ @pre has_parent() */
+ void move_up();
+
+ /** Switch place with sibling.
+ If the node is the last sibling, nothing happens.
+ @pre has_parent() */
+ void move_down();
+
+ /** Delete all siblings of the first child. */
+ void delete_variations();
+
+private:
+ SgfNode* m_parent = nullptr;
+
+ unique_ptr<SgfNode> m_first_child;
+
+ unique_ptr<SgfNode> m_sibling;
+
+ /** The properties.
+ Often a node has only one property (the move), so it saves memory
+ to use a forward_list instead of a vector. */
+ forward_list<Property> m_properties;
+
+ forward_list<Property>::const_iterator find_property(
+ const string& id) const;
+
+ SgfNode* get_last_child() const;
+};
+
+inline const SgfNode& SgfNode::get_child() const
+{
+ LIBBOARDGAME_ASSERT(has_single_child());
+ return *m_first_child;
+}
+
+inline const SgfNode& SgfNode::get_parent() const
+{
+ LIBBOARDGAME_ASSERT(has_parent());
+ return *m_parent;
+}
+
+inline SgfNode& SgfNode::get_parent()
+{
+ LIBBOARDGAME_ASSERT(has_parent());
+ return *m_parent;
+}
+
+inline const SgfNode* SgfNode::get_parent_or_null() const
+{
+ return m_parent;
+}
+
+inline SgfNode& SgfNode::get_first_child()
+{
+ LIBBOARDGAME_ASSERT(has_children());
+ return *m_first_child.get();
+}
+
+inline const SgfNode& SgfNode::get_first_child() const
+{
+ LIBBOARDGAME_ASSERT(has_children());
+ return *(m_first_child.get());
+}
+
+inline SgfNode* SgfNode::get_first_child_or_null()
+{
+ return m_first_child.get();
+}
+
+inline const SgfNode* SgfNode::get_first_child_or_null() const
+{
+ return m_first_child.get();
+}
+
+inline SgfNode* SgfNode::get_sibling()
+{
+ return m_sibling.get();
+}
+
+inline const SgfNode* SgfNode::get_sibling() const
+{
+ return m_sibling.get();
+}
+
+inline bool SgfNode::has_children() const
+{
+ return static_cast<bool>(m_first_child);
+}
+
+inline bool SgfNode::has_parent() const
+{
+ return m_parent != nullptr;
+}
+
+inline bool SgfNode::has_single_child() const
+{
+ return m_first_child && ! m_first_child->m_sibling;
+}
+
+template<typename T>
+T SgfNode::parse_property(const string& id) const
+{
+ string value = get_property(id);
+ T result;
+ if (! from_string(value, result))
+ throw InvalidPropertyValue(id, value);
+ return result;
+}
+
+template<typename T>
+T SgfNode::parse_property(const string& id, const T& default_value) const
+{
+ if (! has_property(id))
+ return default_value;
+ return parse_property<T>(id);
+}
+
+inline unique_ptr<SgfNode> SgfNode::remove_children()
+{
+ if (m_first_child)
+ m_first_child->m_parent = nullptr;
+ return move(m_first_child);
+}
+
+template<typename T>
+bool SgfNode::set_property(const string& id, const T& value)
+{
+ vector<T> values(1, value);
+ return set_property(id, values);
+}
+
+inline bool SgfNode::set_property(const string& id, const char* value)
+{
+ return set_property<string>(id, value);
+}
+
+template<typename T>
+bool SgfNode::set_property(const string& id, const vector<T>& values)
+{
+ vector<string> values_to_string;
+ for (const T& v : values)
+ values_to_string.push_back(to_string(v));
+ forward_list<Property>::const_iterator last = m_properties.end();
+ for (auto i = m_properties.begin(); i != m_properties.end(); ++i)
+ if (i->id == id)
+ {
+ bool was_changed = (i->values != values_to_string);
+ i->values = values_to_string;
+ return was_changed;
+ }
+ else
+ last = i;
+ if (last == m_properties.end())
+ m_properties.emplace_front(id, values_to_string);
+ else
+ m_properties.emplace_after(last, id, values_to_string);
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_SGF_NODE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfTree.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "SgfTree.h"
+
+#include <ctime>
+#include <cstdio>
+#include <cstdlib>
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libboardgame_sgf {
+
+using libboardgame_sgf::util::find_root;
+using libboardgame_util::trim;
+
+//-----------------------------------------------------------------------------
+
+SgfTree::SgfTree()
+{
+ init();
+}
+
+SgfTree::~SgfTree()
+{
+}
+
+bool SgfTree::contains(const SgfNode& node) const
+{
+ return &find_root(node) == &get_root();
+}
+
+const SgfNode& SgfTree::create_new_child(const SgfNode& node)
+{
+ m_modified = true;
+ return non_const(node).create_new_child();
+}
+
+void SgfTree::delete_all_variations()
+{
+ if (has_variations())
+ m_modified = true;
+ auto node = &get_root();
+ while (node)
+ {
+ non_const(*node).delete_variations();
+ node = node->get_first_child_or_null();
+ }
+}
+
+double SgfTree::get_bad_move(const SgfNode& node)
+{
+ return node.parse_property<double>("BM", 0);
+}
+
+string SgfTree::get_comment(const SgfNode& node) const
+{
+ return node.get_property("C", "");
+}
+
+string SgfTree::get_date_today()
+{
+ time_t t = time(nullptr);
+ auto tmp = localtime(&t);
+ if (! tmp)
+ return "?";
+ char date[128];
+ strftime(date, sizeof(date), "%Y-%m-%d", tmp);
+ return date;
+}
+
+double SgfTree::get_good_move(const SgfNode& node)
+{
+ return node.parse_property<double>("TE", 0);
+}
+
+unique_ptr<SgfNode> SgfTree::get_tree_transfer_ownership()
+{
+ return move(m_root);
+}
+
+bool SgfTree::has_variations() const
+{
+ auto node = m_root.get();
+ while (node)
+ {
+ if (node->get_sibling())
+ return true;
+ node = node->get_first_child_or_null();
+ }
+ return false;
+}
+
+void SgfTree::init()
+{
+ unique_ptr<SgfNode> root(new SgfNode);
+ m_root = move(root);
+ m_modified = false;
+}
+
+void SgfTree::init(unique_ptr<SgfNode>& root)
+{
+ m_root = move(root);
+ m_modified = false;
+}
+
+bool SgfTree::is_doubtful_move(const SgfNode& node)
+{
+ return node.has_property("DO");
+}
+
+bool SgfTree::is_interesting_move(const SgfNode& node)
+{
+ return node.has_property("IT");
+}
+
+void SgfTree::make_first_child(const SgfNode& node)
+{
+ auto parent = node.get_parent_or_null();
+ if (parent && &parent->get_first_child() != &node)
+ {
+ non_const(node).make_first_child();
+ m_modified = true;
+ }
+}
+
+void SgfTree::make_main_variation(const SgfNode& node)
+{
+ auto current = &non_const(node);
+ while (current->has_parent())
+ {
+ make_first_child(*current);
+ current = ¤t->get_parent();
+ }
+}
+
+void SgfTree::make_root(const SgfNode& node)
+{
+ if (&node == &get_root())
+ return;
+ LIBBOARDGAME_ASSERT(contains(node));
+ auto& parent = node.get_parent();
+ unique_ptr<SgfNode> new_root = non_const(parent).remove_child(non_const(node));
+ m_root = move(new_root);
+ m_modified = true;
+}
+
+void SgfTree::move_property_to_front(const SgfNode& node, const string& id)
+{
+ if (non_const(node).move_property_to_front(id))
+ m_modified = true;
+}
+
+void SgfTree::move_down(const SgfNode& node)
+{
+ if (node.get_sibling())
+ {
+ non_const(node).move_down();
+ m_modified = true;
+ }
+}
+
+void SgfTree::move_up(const SgfNode& node)
+{
+ auto parent = node.get_parent_or_null();
+ if (parent && &parent->get_first_child() != &node)
+ {
+ non_const(node).move_up();
+ m_modified = true;
+ }
+}
+
+void SgfTree::remove_move_annotation(const SgfNode& node)
+{
+ remove_property(node, "BM");
+ remove_property(node, "DO");
+ remove_property(node, "IT");
+ remove_property(node, "TE");
+}
+
+bool SgfTree::remove_property(const SgfNode& node, const string& id)
+{
+ bool prop_existed = non_const(node).remove_property(id);
+ if (prop_existed)
+ m_modified = true;
+ return prop_existed;
+}
+
+void SgfTree::set_application(const string& name, const string& version)
+{
+ if (version.empty())
+ set_property(get_root(), "AP", name);
+ else
+ set_property(get_root(), "AP", name + ":" + version);
+}
+
+void SgfTree::set_property(const SgfNode& node, const string& id, const char* value)
+{
+ bool was_changed = non_const(node).set_property(id, value);
+ if (was_changed)
+ m_modified = true;
+}
+
+void SgfTree::set_property_remove_empty(const SgfNode& node, const string& id,
+ const string& value)
+{
+ string trimmed = trim(value);
+ if (trimmed.empty())
+ remove_property(node, id);
+ else
+ set_property(node, id, value);
+}
+
+void SgfTree::set_bad_move(const SgfNode& node, double value)
+{
+ remove_move_annotation(node);
+ set_property(node, "BM", value);
+}
+
+void SgfTree::set_comment(const SgfNode& node, const string& s)
+{
+ set_property_remove_empty(node, "C", s);
+}
+
+void SgfTree::set_date_today()
+{
+ set_date(get_date_today());
+}
+
+void SgfTree::set_doubtful_move(const SgfNode& node)
+{
+ remove_move_annotation(node);
+ set_property(node, "DO", "");
+}
+
+void SgfTree::set_good_move(const SgfNode& node, double value)
+{
+ remove_move_annotation(node);
+ set_property(node, "TE", value);
+}
+
+void SgfTree::set_interesting_move(const SgfNode& node)
+{
+ remove_move_annotation(node);
+ set_property(node, "IT", "");
+}
+
+const SgfNode& SgfTree::truncate(const SgfNode& node)
+{
+ LIBBOARDGAME_ASSERT(node.has_parent());
+ auto& parent = node.get_parent();
+ non_const(parent).remove_child(non_const(node));
+ m_modified = true;
+ return parent;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfTree.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_SGF_TREE_H
+#define LIBBOARDGAME_SGF_SGF_TREE_H
+
+#include "libboardgame_sgf/SgfNode.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** SGF tree.
+ Tree structure of the tree can only be manipulated through member functions
+ to guarantee a consistent tree structure. Therefore the user is given
+ only const references to nodes and non-const functions of nodes can only
+ be called through wrapper functions of the tree (in which case the user
+ passes in a const reference to the node as an identifier for the node). */
+class SgfTree
+{
+public:
+ SgfTree();
+
+ virtual ~SgfTree();
+
+ virtual void init();
+
+ /** Initialize from an existing SGF tree.
+ @param root The root node of the SGF tree; the ownership is transferred
+ to this class. */
+ virtual void init(unique_ptr<SgfNode>& root);
+
+ /** Get the root node and transfer the ownership to the caller. */
+ unique_ptr<SgfNode> get_tree_transfer_ownership();
+
+ /** Check if the tree was modified since the construction or the last call
+ to init() or clear_modified() */
+ bool is_modified() const;
+
+ void set_modified();
+
+ void clear_modified();
+
+ const SgfNode& get_root() const;
+
+ const SgfNode& create_new_child(const SgfNode& node);
+
+ /** Truncate a node and its subtree from the tree.
+ Calling this function deletes the node that is to be truncated and its
+ complete subtree.
+ @pre node.has_parent()
+ @param node The node to be truncated.
+ @return The parent of the truncated node. */
+ const SgfNode& truncate(const SgfNode& node);
+
+ /** Delete all variations but the main variation. */
+ void delete_all_variations();
+
+ /** Make a node the first child of its parent. */
+ void make_first_child(const SgfNode& node);
+
+ /** Make a node switch place with its previous sibling (if it is not
+ already the first child). */
+ void move_up(const SgfNode& node);
+
+ /** Make a node switch place with its next sibling (if it is not
+ already the last child). */
+ void move_down(const SgfNode& node);
+
+ /** Make a node the root node of the tree.
+ All nodes that are not the given node or in the subtree below it are
+ deleted. Note that this operation in general creates a semantically
+ invalid tree (e.g. missing GM or CA property in the new root). You need
+ to add those after this function. In general, you will also have to
+ examine the nodes in the path to the node in the original tree and then
+ make the tree valid again after calling make_root(). Typically, you
+ will have to look at the moves played before this node and convert them
+ into setup properties to add to the new root such that the board
+ position at this node is the same as originally. */
+ void make_root(const SgfNode& node);
+
+ void make_main_variation(const SgfNode& node);
+
+ bool contains(const SgfNode& node) const;
+
+ template<typename T>
+ void set_property(const SgfNode& node, const string& id, const T& value);
+
+ void set_property(const SgfNode& node, const string& id, const char* value);
+
+ template<typename T>
+ void set_property(const SgfNode& node, const string& id,
+ const vector<T>& values);
+
+ void set_property_remove_empty(const SgfNode& node,
+ const string& id, const string& value);
+
+ bool remove_property(const SgfNode& node, const string& id);
+
+ void move_property_to_front(const SgfNode& node, const string& id);
+
+ /** See Node::remove_children() */
+ unique_ptr<SgfNode> remove_children(const SgfNode& node);
+
+ void append(const SgfNode& node, unique_ptr<SgfNode> child);
+
+ /** Get comment.
+ @return The comment, or an empty string if the node contains no
+ comment. */
+ string get_comment(const SgfNode& node) const;
+
+ void set_comment(const SgfNode& node, const string& s);
+
+ void remove_move_annotation(const SgfNode& node);
+
+ static double get_good_move(const SgfNode& node);
+
+ void set_good_move(const SgfNode& node, double value = 1);
+
+ static double get_bad_move(const SgfNode& node);
+
+ void set_bad_move(const SgfNode& node, double value = 1);
+
+ static bool is_doubtful_move(const SgfNode& node);
+
+ void set_doubtful_move(const SgfNode& node);
+
+ static bool is_interesting_move(const SgfNode& node);
+
+ void set_interesting_move(const SgfNode& node);
+
+ void set_charset(const string& charset);
+
+ void set_application(const string& name, const string& version = "");
+
+ string get_date() const;
+
+ void set_date(const string& date);
+
+ /** Get today's date in format YYYY-MM-DD as required by DT property. */
+ static string get_date_today();
+
+ void set_date_today();
+
+ string get_event() const;
+
+ void set_event(const string& event);
+
+ string get_round() const;
+
+ void set_round(const string& date);
+
+ string get_time() const;
+
+ void set_time(const string& time);
+
+ bool has_variations() const;
+
+private:
+ bool m_modified;
+
+ unique_ptr<SgfNode> m_root;
+
+ SgfNode& non_const(const SgfNode& node);
+};
+
+inline void SgfTree::append(const SgfNode& node, unique_ptr<SgfNode> child)
+{
+ if (child)
+ m_modified = true;
+ non_const(node).append(move(child));
+}
+
+inline void SgfTree::clear_modified()
+{
+ m_modified = false;
+}
+
+inline string SgfTree::get_date() const
+{
+ return m_root->get_property("DT", "");
+}
+
+inline string SgfTree::get_event() const
+{
+ return m_root->get_property("EV", "");
+}
+
+inline bool SgfTree::is_modified() const
+{
+ return m_modified;
+}
+
+inline string SgfTree::get_round() const
+{
+ return m_root->get_property("RO", "");
+}
+
+inline const SgfNode& SgfTree::get_root() const
+{
+ return *m_root;
+}
+
+inline string SgfTree::get_time() const
+{
+ return m_root->get_property("TM", "");
+}
+
+inline SgfNode& SgfTree::non_const(const SgfNode& node)
+{
+ LIBBOARDGAME_ASSERT(contains(node));
+ return const_cast<SgfNode&>(node);
+}
+
+inline unique_ptr<SgfNode> SgfTree::remove_children(const SgfNode& node)
+{
+ if (node.has_children())
+ m_modified = true;
+ return non_const(node).remove_children();
+}
+
+inline void SgfTree::set_charset(const string& charset)
+{
+ set_property_remove_empty(get_root(), "CA", charset);
+}
+
+inline void SgfTree::set_date(const string& date)
+{
+ set_property_remove_empty(get_root(), "DT", date);
+}
+
+inline void SgfTree::set_event(const string& event)
+{
+ set_property_remove_empty(get_root(), "EV", event);
+}
+
+inline void SgfTree::set_modified()
+{
+ m_modified = true;
+}
+
+template<typename T>
+void SgfTree::set_property(const SgfNode& node, const string& id, const T& value)
+{
+ bool was_changed = non_const(node).set_property(id, value);
+ if (was_changed)
+ m_modified = true;
+}
+
+template<typename T>
+void SgfTree::set_property(const SgfNode& node, const string& id,
+ const vector<T>& values)
+{
+ bool was_changed = non_const(node).set_property(id, values);
+ if (was_changed)
+ m_modified = true;
+}
+
+inline void SgfTree::set_round(const string& round)
+{
+ set_property_remove_empty(get_root(), "RO", round);
+}
+
+inline void SgfTree::set_time(const string& time)
+{
+ set_property_remove_empty(get_root(), "TM", time);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_SGF_TREE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "SgfUtil.h"
+
+#include <algorithm>
+#include <sstream>
+#include "InvalidPropertyValue.h"
+#include "TreeWriter.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+namespace util {
+
+using libboardgame_util::get_letter_coord;
+
+//-----------------------------------------------------------------------------
+
+const SgfNode& back_to_main_variation(const SgfNode& node)
+{
+ if (is_main_variation(node))
+ return node;
+ auto current = &node;
+ while (! is_main_variation(*current))
+ current = ¤t->get_parent();
+ return current->get_first_child();
+}
+
+const SgfNode& beginning_of_branch(const SgfNode& node)
+{
+ auto current = node.get_parent_or_null();
+ if (! current)
+ return node;
+ while (true)
+ {
+ auto parent = current->get_parent_or_null();
+ if (! parent || ! parent->has_single_child())
+ break;
+ current = parent;
+ }
+ return *current;
+}
+
+const SgfNode* find_next_comment(const SgfNode& node)
+{
+ auto current = get_next_node(node);
+ while (current)
+ {
+ if (has_comment(*current))
+ return current;
+ current = get_next_node(*current);
+ }
+ return nullptr;
+}
+
+const SgfNode& find_root(const SgfNode& node)
+{
+ auto current = &node;
+ while (current->has_parent())
+ current = ¤t->get_parent();
+ return *current;
+}
+
+const SgfNode& get_last_node(const SgfNode& node)
+{
+ auto n = &node;
+ while (n->has_children())
+ n = &n->get_first_child();
+ return *n;
+}
+
+unsigned get_depth(const SgfNode& node)
+{
+ unsigned depth = 0;
+ auto current = &node;
+ while (current->has_parent())
+ {
+ current = ¤t->get_parent();
+ ++depth;
+ }
+ return depth;
+}
+
+const char* get_move_annotation(const SgfTree& tree, const SgfNode& node)
+{
+ double goodMove = tree.get_good_move(node);
+ if (goodMove > 1)
+ return "!!";
+ if (goodMove > 0)
+ return "!";
+ double badMove = tree.get_bad_move(node);
+ if (badMove > 1)
+ return "??";
+ if (badMove > 0)
+ return "?";
+ if (tree.is_interesting_move(node))
+ return "!?";
+ if (tree.is_doubtful_move(node))
+ return "?!";
+ return "";
+}
+
+const SgfNode* get_next_earlier_variation(const SgfNode& node)
+{
+ auto child = &node;
+ auto current = node.get_parent_or_null();
+ while (current && ! child->get_sibling())
+ {
+ child = current;
+ current = current->get_parent_or_null();
+ }
+ if (! current)
+ return nullptr;
+ return child->get_sibling();
+}
+
+const SgfNode* get_next_node(const SgfNode& node)
+{
+ auto child = node.get_first_child_or_null();
+ if (child)
+ return child;
+ return get_next_earlier_variation(node);
+}
+
+void get_path_from_root(const SgfNode& node, vector<const SgfNode*>& path)
+{
+ auto current = &node;
+ path.assign(1, current);
+ while(current->has_parent())
+ {
+ current = ¤t->get_parent();
+ path.push_back(current);
+ }
+ reverse(path.begin(), path.end());
+}
+
+string get_variation_string(const SgfNode& node)
+{
+ string result;
+ auto current = &node;
+ unsigned depth = get_depth(*current);
+ while (current->has_parent())
+ {
+ auto& parent = current->get_parent();
+ if (parent.get_nu_children() > 1)
+ {
+ unsigned index = parent.get_child_index(*current);
+ if (index > 0)
+ {
+ ostringstream s;
+ s << depth << get_letter_coord(index);
+ if (! result.empty())
+ s << '-' << result;
+ result = s.str();
+ }
+ }
+ current = &parent;
+ --depth;
+ }
+ return result;
+}
+
+bool has_comment(const SgfNode& node)
+{
+ return node.has_property("C");
+}
+
+bool has_earlier_variation(const SgfNode& node)
+{
+ auto current = node.get_parent_or_null();
+ if (! current)
+ return false;
+ while (true)
+ {
+ auto parent = current->get_parent_or_null();
+ if (! parent)
+ return false;
+ if (! parent->has_single_child())
+ return true;
+ current = parent;
+ }
+}
+
+bool is_empty(const SgfTree& tree)
+{
+ auto& root = tree.get_root();
+ if (root.has_children())
+ return false;
+ for (auto& p : root.get_properties())
+ {
+ auto& id = p.id;
+ if (id != "GM" && id != "CA" && id != "AP" && id != "DT")
+ return false;
+ }
+ return true;
+}
+
+bool is_main_variation(const SgfNode& node)
+{
+ auto current = &node;
+ while (current->has_parent())
+ {
+ auto& parent = current->get_parent();
+ if (current != &parent.get_first_child())
+ return false;
+ current = &parent;
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace util
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_SGF_UTIL_H
+#define LIBBOARDGAME_SGF_SGF_UTIL_H
+
+#include <string>
+#include "SgfTree.h"
+
+namespace libboardgame_sgf {
+namespace util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Return the last node in the current variation that had a sibling. */
+const SgfNode& beginning_of_branch(const SgfNode& node);
+
+/** Find next node with a comment in the iteration through complete tree.
+ @param node The current node in the iteration.
+ @return The next node in the iteration through the complete tree
+ after the current node that has a comment. */
+const SgfNode* find_next_comment(const SgfNode& node);
+
+const SgfNode& find_root(const SgfNode& node);
+
+/** Get the depth of a node.
+ The root node has depth 0. */
+unsigned get_depth(const SgfNode& node);
+
+/** Get list of nodes from root to a target node.
+ @param node The target node.
+ @param[out] path The list of nodes. */
+void get_path_from_root(const SgfNode& node, vector<const SgfNode*>& path);
+
+const SgfNode& get_last_node(const SgfNode& node);
+
+/** Get a string representation of move annotation properties. */
+const char* get_move_annotation(const SgfTree& tree, const SgfNode& node);
+
+/** Get next node for iteration through complete tree. */
+const SgfNode* get_next_node(const SgfNode& node);
+
+/** Return next variation before this node. */
+const SgfNode* get_next_earlier_variation(const SgfNode& node);
+
+/** Get a text representation of the variation of a certain node.
+ The variation string is a sequence of X.Y for each branching into a
+ variation that is not the first child since the root node separated by
+ commas, with X being the depth of the child node (starting at 0, and
+ therefore equivalent to the move number if there are no non-root nodes
+ without moves) and Y being the number of the child (starting at 1). */
+string get_variation_string(const SgfNode& node);
+
+/** Check if any previous node had a sibling. */
+bool has_earlier_variation(const SgfNode& node);
+
+bool is_main_variation(const SgfNode& node);
+
+const SgfNode& back_to_main_variation(const SgfNode& node);
+
+bool has_comment(const SgfNode& node);
+
+/** Check if a tree doesn't contain nodes apart from the root node
+ or properties apart from some trivial properties (GM, CA, AP or DT) */
+bool is_empty(const SgfTree& tree);
+
+//-----------------------------------------------------------------------------
+
+} // namespace util
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_SGF_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/TreeReader.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TreeReader.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+TreeReader::TreeReader() = default;
+
+TreeReader::~TreeReader() = default;
+
+unique_ptr<SgfNode> TreeReader::get_tree_transfer_ownership()
+{
+ return move(m_root);
+}
+
+void TreeReader::on_begin_tree(bool is_root)
+{
+ if (! is_root)
+ m_stack.push(m_current);
+}
+
+void TreeReader::on_end_tree(bool is_root)
+{
+ if (! is_root)
+ {
+ LIBBOARDGAME_ASSERT(! m_stack.empty());
+ m_current = m_stack.top();
+ m_stack.pop();
+ }
+}
+
+void TreeReader::on_begin_node(bool is_root)
+{
+ if (is_root)
+ {
+ m_root.reset(new SgfNode);
+ m_current = m_root.get();
+ }
+ else
+ m_current = &m_current->create_new_child();
+}
+
+void TreeReader::on_end_node()
+{
+}
+
+void TreeReader::on_property(const string& identifier,
+ const vector<string>& values)
+{
+ m_current->set_property(identifier, values);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/TreeReader.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_TREE_READER_H
+#define LIBBOARDGAME_SGF_TREE_READER_H
+
+#include <memory>
+#include <stack>
+#include "Reader.h"
+#include "SgfNode.h"
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class TreeReader
+ : public Reader
+{
+public:
+ TreeReader();
+
+ ~TreeReader();
+
+ void on_begin_tree(bool is_root) override;
+
+ void on_end_tree(bool is_root) override;
+
+ void on_begin_node(bool is_root) override;
+
+ void on_end_node() override;
+
+ void on_property(const string& identifier,
+ const vector<string>& values) override;
+
+ const SgfNode& get_tree() const;
+
+ /** Get the tree and transfer the ownership to the caller. */
+ unique_ptr<SgfNode> get_tree_transfer_ownership();
+
+private:
+ SgfNode* m_current = nullptr;
+
+ unique_ptr<SgfNode> m_root;
+
+ stack<SgfNode*> m_stack;
+};
+
+inline const SgfNode& TreeReader::get_tree() const
+{
+ return *m_root.get();
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_TREE_READER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/TreeWriter.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TreeWriter.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+TreeWriter::TreeWriter(ostream& out, const SgfNode& root)
+ : m_root(root),
+ m_writer(out)
+{
+}
+
+TreeWriter::~TreeWriter()
+{
+}
+
+void TreeWriter::write()
+{
+ m_writer.begin_tree();
+ write_node(m_root);
+ m_writer.end_tree();
+}
+
+void TreeWriter::write_node(const SgfNode& node)
+{
+ m_writer.begin_node();
+ for (auto& i : node.get_properties())
+ write_property(i.id, i.values);
+ m_writer.end_node();
+ if (! node.has_children())
+ return;
+ else if (node.has_single_child())
+ write_node(node.get_child());
+ else
+ for (auto& i : node.get_children())
+ {
+ m_writer.begin_tree();
+ write_node(i);
+ m_writer.end_tree();
+ }
+}
+
+void TreeWriter::write_property(const string& id, const vector<string>& values)
+{
+ m_writer.write_property(id, values);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/TreeWriter.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_TREE_WRITER_H
+#define LIBBOARDGAME_SGF_TREE_WRITER_H
+
+#include "SgfNode.h"
+#include "Writer.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+class TreeWriter
+{
+public:
+ TreeWriter(ostream& out, const SgfNode& root);
+
+ virtual ~TreeWriter();
+
+ /** Overridable function to write a property.
+ Can be used in subclasses, for example, to replace or remove obsolete
+ properties or do other sanitizing. */
+ virtual void write_property(const string& id,
+ const vector<string>& values);
+
+
+ /** @name Formatting options.
+ Should be set before starting to write. */
+ /** @{ */
+
+ void set_one_prop_per_line(bool enable);
+
+ void set_one_prop_value_per_line(bool enable);
+
+ void set_indent(int indent);
+
+ /** @} */ // @name
+
+
+ void write();
+
+private:
+ const SgfNode& m_root;
+
+ Writer m_writer;
+
+ void write_node(const SgfNode& node);
+};
+
+inline void TreeWriter::set_one_prop_per_line(bool enable)
+{
+ m_writer.set_one_prop_per_line(enable);
+}
+
+inline void TreeWriter::set_one_prop_value_per_line(bool enable)
+{
+ m_writer.set_one_prop_value_per_line(enable);
+}
+
+inline void TreeWriter::set_indent(int indent)
+{
+ m_writer.set_indent(indent);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_TREE_WRITER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/Writer.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Writer.h"
+
+#include <sstream>
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+Writer::Writer(ostream& out)
+ : m_out(out)
+{ }
+
+void Writer::begin_node()
+{
+ m_is_first_prop = true;
+ write_indent();
+ m_out << ';';
+}
+
+void Writer::begin_tree()
+{
+ write_indent();
+ m_out << '(';
+ // Don't indent the first level
+ if (m_level > 0)
+ m_current_indent += m_indent;
+ ++m_level;
+ if (m_indent >= 0)
+ m_out << '\n';
+}
+
+void Writer::end_node()
+{
+ if (! m_one_prop_per_line && m_indent >= 0)
+ m_out << '\n';
+}
+
+void Writer::end_tree()
+{
+ --m_level;
+ if (m_level > 0)
+ m_current_indent -= m_indent;
+ write_indent();
+ m_out << ')';
+ if (m_indent >= 0)
+ m_out << '\n';
+}
+
+string Writer::get_escaped(const string& s)
+{
+ ostringstream buffer;
+ for (char c : s)
+ {
+ if (c == ']' || c == '\\')
+ buffer << '\\' << c;
+ else if (c == '\t' || c == '\f' || c == '\v')
+ // Replace whitespace as required by the SGF standard.
+ buffer << ' ';
+ else
+ buffer << c;
+ }
+ return buffer.str();
+}
+
+void Writer::write_indent()
+{
+ if (m_indent >= 0)
+ for (int i = 0; i < m_current_indent; ++i)
+ m_out << ' ';
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/Writer.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_WRITER_H
+#define LIBBOARDGAME_SGF_WRITER_H
+
+#include <iosfwd>
+#include <string>
+#include <vector>
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+
+using namespace std;
+using libboardgame_util::to_string;
+
+//-----------------------------------------------------------------------------
+
+class Writer
+{
+public:
+ explicit Writer(ostream& out);
+
+ /** @name Formatting options.
+ Should be set before starting to write. */
+ /** @{ */
+
+ void set_one_prop_per_line(bool enable);
+
+ void set_one_prop_value_per_line(bool enable);
+
+ /** @param indent The number of spaces to indent subtrees, -1 means
+ to not even use newlines. */
+ void set_indent(int indent);
+
+ /** @} */ // @name
+
+
+ void begin_tree();
+
+ void end_tree();
+
+ void begin_node();
+
+ void end_node();
+
+ void write_property(const string& id, const char* value);
+
+ template<typename T>
+ void write_property(const string& id, const T& value);
+
+ template<typename T>
+ void write_property(const string& id, const vector<T>& values);
+
+private:
+ ostream& m_out;
+
+ bool m_one_prop_per_line = false;
+
+ bool m_one_prop_value_per_line = false;
+
+ bool m_is_first_prop;
+
+ int m_indent = 0;
+
+ int m_current_indent = 0;
+
+ unsigned m_level = 0;
+
+
+ static string get_escaped(const string& s);
+
+ void write_indent();
+};
+
+inline void Writer::set_one_prop_per_line(bool enable)
+{
+ m_one_prop_per_line = enable;
+}
+
+inline void Writer::set_one_prop_value_per_line(bool enable)
+{
+ m_one_prop_value_per_line = enable;
+}
+
+inline void Writer::set_indent(int indent)
+{
+ m_indent = indent;
+}
+
+inline void Writer::write_property(const string& id, const char* value)
+{
+ vector<const char*> values(1, value);
+ write_property(id, values);
+}
+
+template<typename T>
+void Writer::write_property(const string& id, const T& value)
+{
+ vector<T> values(1, value);
+ write_property(id, values);
+}
+
+template<typename T>
+void Writer::write_property(const string& id, const vector<T>& values)
+{
+ if (m_one_prop_per_line && ! m_is_first_prop)
+ {
+ write_indent();
+ m_out << ' ';
+ }
+ m_out << id;
+ bool is_first_value = true;
+ for (auto& i : values)
+ {
+ if (m_one_prop_per_line && m_one_prop_value_per_line
+ && ! is_first_value && m_indent >= 0)
+ {
+ m_out << '\n';
+ int indent = static_cast<int>(m_current_indent + 1 + id.size());
+ for (int i = 0; i < indent; ++i)
+ m_out << ' ';
+ }
+ m_out << '[' << get_escaped(to_string(i)) << ']';
+ is_first_value = false;
+ }
+ if (m_one_prop_per_line && m_indent >= 0)
+ m_out << '\n';
+ m_is_first_prop = false;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
+
+#endif // LIBBOARDGAME_SGF_WRITER_H
--- /dev/null
+add_library(boardgame_sys STATIC
+ Compiler.h
+ CpuTime.h
+ CpuTime.cpp
+ Memory.h
+ Memory.cpp
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sys/Compiler.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SYS_COMPILER_H
+#define LIBBOARDGAME_SYS_COMPILER_H
+
+#include <string>
+#include <typeinfo>
+#ifdef __GNUC__
+#include <cstdlib>
+#include <cxxabi.h>
+#endif
+
+namespace libboardgame_sys {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+#ifdef __GNUC__
+#define LIBBOARDGAME_FORCE_INLINE inline __attribute__((always_inline))
+#elif defined _MSC_VER
+#define LIBBOARDGAME_FORCE_INLINE inline __forceinline
+#else
+#define LIBBOARDGAME_FORCE_INLINE inline
+#endif
+
+#ifdef __GNUC__
+#define LIBBOARDGAME_NOINLINE __attribute__((noinline))
+#elif defined _MSC_VER
+#define LIBBOARDGAME_NOINLINE __declspec(noinline)
+#else
+#define LIBBOARDGAME_NOINLINE
+#endif
+
+#if defined __GNUC__ && ! defined __ICC && ! defined __clang__
+#define LIBBOARDGAME_FLATTEN __attribute__((flatten))
+#else
+#define LIBBOARDGAME_FLATTEN
+#endif
+
+template<typename T>
+string get_type_name(const T& t)
+{
+#ifdef __GNUC__
+ int status;
+ char* name_ptr = abi::__cxa_demangle(typeid(t).name(), nullptr, nullptr,
+ &status);
+ if (status == 0)
+ {
+ string result(name_ptr);
+ free(name_ptr);
+ return result;
+ }
+#endif
+ return typeid(t).name();
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sys
+
+#endif // LIBBOARDGAME_SYS_COMPILER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sys/CpuTime.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "CpuTime.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+#if HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+#if HAVE_SYS_TIMES_H
+#include <sys/times.h>
+#endif
+
+namespace libboardgame_sys {
+
+//-----------------------------------------------------------------------------
+
+double cpu_time()
+{
+#ifdef _WIN32
+
+ FILETIME create;
+ FILETIME exit;
+ FILETIME sys;
+ FILETIME user;
+ if (! GetProcessTimes(GetCurrentProcess(), &create, &exit, &sys, &user))
+ return -1;
+ ULARGE_INTEGER sys_int;
+ sys_int.LowPart = sys.dwLowDateTime;
+ sys_int.HighPart = sys.dwHighDateTime;
+ ULARGE_INTEGER user_int;
+ user_int.LowPart = user.dwLowDateTime;
+ user_int.HighPart = user.dwHighDateTime;
+ return (sys_int.QuadPart + user_int.QuadPart) * 1e-7;
+
+#elif HAVE_UNISTD_H && HAVE_SYS_TIMES_H
+ static double ticks_per_second = double(sysconf(_SC_CLK_TCK));
+ struct tms buf;
+ if (times(&buf) == clock_t(-1))
+ return -1;
+ clock_t clock_ticks =
+ buf.tms_utime + buf.tms_stime + buf.tms_cutime + buf.tms_cstime;
+ return double(clock_ticks) / ticks_per_second;
+
+#else
+
+ return -1;
+
+#endif
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sys
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sys/CpuTime.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SYS_CPU_TIME_H
+#define LIBBOARDGAME_SYS_CPU_TIME_H
+
+namespace libboardgame_sys {
+
+//-----------------------------------------------------------------------------
+
+/** Return the CPU time of the current process.
+ @return The CPU time of the current process in seconds or -1, if the
+ CPU time cannot be determined. */
+double cpu_time();
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sys
+
+#endif // LIBBOARDGAME_SYS_CPU_TIME_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sys/Memory.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Memory.h"
+
+#ifdef _WIN32
+#include <algorithm>
+#include <windows.h>
+#else
+#include <unistd.h>
+#endif
+// sysctl() is unsupported on Linux with x32 ABI (last checked on Ubuntu 14.10)
+#if HAVE_SYS_SYSCTL_H && ! (defined __x86_64__ && defined __ILP32__)
+#include <sys/sysctl.h>
+#endif
+
+namespace libboardgame_sys {
+
+//-----------------------------------------------------------------------------
+
+size_t get_memory()
+{
+#ifdef _WIN32
+
+ MEMORYSTATUSEX status;
+ status.dwLength = sizeof(status);
+ if (! GlobalMemoryStatusEx(&status))
+ return 0;
+ auto total_virtual = static_cast<size_t>(status.ullTotalVirtual);
+ auto total_phys = static_cast<size_t>(status.ullTotalPhys);
+ return min(total_virtual, total_phys);
+
+#elif defined _SC_PHYS_PAGES
+
+ long phys_pages = sysconf(_SC_PHYS_PAGES);
+ if (phys_pages < 0)
+ return 0;
+ long page_size = sysconf(_SC_PAGE_SIZE);
+ if (page_size < 0)
+ return 0;
+ return static_cast<size_t>(phys_pages) * static_cast<size_t>(page_size);
+
+#elif defined HW_PHYSMEM // Mac OS X
+
+ unsigned int phys_mem;
+ size_t len = sizeof(phys_mem);
+ int name[2] = { CTL_HW, HW_PHYSMEM };
+ if (sysctl(name, 2, &phys_mem, &len, nullptr, 0) != 0
+ || len != sizeof(phys_mem))
+ return 0;
+ else
+ return phys_mem;
+
+#else
+
+ return 0;
+
+#endif
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sys
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sys/Memory.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SYS_MEMORY_H
+#define LIBBOARDGAME_SYS_MEMORY_H
+
+#include <cstddef>
+
+namespace libboardgame_sys {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Get the physical memory available on the system.
+ @return The memory in bytes or 0 if the memory could not be determined. */
+size_t get_memory();
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sys
+
+#endif // LIBBOARDGAME_SYS_MEMORY_H
--- /dev/null
+add_library(boardgame_test STATIC
+ Test.h
+ Test.cpp
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_test/Test.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Test.h"
+
+#include <sstream>
+#include <map>
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_util/Log.h"
+
+namespace libboardgame_test {
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+map<string, TestFunction>& get_all_tests()
+{
+ static map<string, TestFunction> all_tests;
+ return all_tests;
+}
+
+string get_fail_msg(const char* file, int line, const string& s)
+{
+ ostringstream msg;
+ msg << file << ":" << line << ": " << s;
+ return msg.str();
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+TestFail::TestFail(const char* file, int line, const string& s)
+ : logic_error(get_fail_msg(file, line, s))
+{
+}
+
+//-----------------------------------------------------------------------------
+
+void add_test(const string& name, TestFunction function)
+{
+ auto& all_tests = get_all_tests();
+ LIBBOARDGAME_ASSERT(all_tests.find(name) == all_tests.end());
+ all_tests.insert(make_pair(name, function));
+}
+
+bool run_all_tests()
+{
+ unsigned nu_fail = 0;
+ LIBBOARDGAME_LOG("Running ", get_all_tests().size(), " tests...");
+ for (auto& i : get_all_tests())
+ {
+ try
+ {
+ (i.second)();
+ }
+ catch (const TestFail& e)
+ {
+ LIBBOARDGAME_LOG(e.what());
+ ++nu_fail;
+ }
+ }
+ if (nu_fail == 0)
+ {
+ LIBBOARDGAME_LOG("OK");
+ return true;
+ }
+ else
+ {
+ LIBBOARDGAME_LOG(nu_fail, " tests failed.\nFAIL");
+ return false;
+ }
+}
+
+bool run_test(const string& name)
+{
+ for (auto& i : get_all_tests())
+ if (i.first == name)
+ {
+ LIBBOARDGAME_LOG("Running ", name, "...");
+ try
+ {
+ (i.second)();
+ LIBBOARDGAME_LOG("OK");
+ return true;
+ }
+ catch (const TestFail& e)
+ {
+ LIBBOARDGAME_LOG(e.what(), "\nFAIL");
+ return false;
+ }
+ }
+ LIBBOARDGAME_LOG("Test not found: ", name);
+ return false;
+}
+
+int test_main(int argc, char* argv[])
+{
+ if (argc < 2)
+ return run_all_tests() ? 0 : 1;
+ int result = 0;
+ for (int i = 1; i < argc; ++i)
+ if (! run_test(argv[i]))
+ result = 1;
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_test
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_test/Test.h
+ Provides functionality similar to Boost.Test.
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_TEST_TEST_H
+#define LIBBOARDGAME_TEST_TEST_H
+
+#include <cmath>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+namespace libboardgame_test {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+typedef void (*TestFunction)();
+
+//-----------------------------------------------------------------------------
+
+class TestFail
+ : public logic_error
+{
+public:
+ TestFail(const char* file, int line, const string& s);
+};
+
+//-----------------------------------------------------------------------------
+
+void add_test(const string& name, TestFunction function);
+
+bool run_all_tests();
+
+bool run_test(const string& name);
+
+/** Main function that runs all tests (if no arguments) or only the tests
+ given as arguments. */
+int test_main(int argc, char* argv[]);
+
+//-----------------------------------------------------------------------------
+
+/** Helper class that automatically adds a test when an instance is
+ declared. */
+struct TestRegistrar
+{
+ TestRegistrar(const string& name, TestFunction function)
+ {
+ add_test(name, function);
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_test
+
+//-----------------------------------------------------------------------------
+
+#define LIBBOARDGAME_TEST_CASE(name) \
+ void name(); \
+ libboardgame_test::TestRegistrar name##_registrar(#name, name); \
+ void name()
+
+
+#define LIBBOARDGAME_CHECK(expr) \
+ if (! (expr)) \
+ throw libboardgame_test::TestFail(__FILE__, __LINE__, "check failed")
+
+#define LIBBOARDGAME_CHECK_EQUAL(expr1, expr2) \
+ { \
+ using libboardgame_test::TestFail; \
+ auto result1 = (expr1); \
+ auto result2 = (expr2); \
+ if (result1 != result2) \
+ { \
+ ostringstream msg; \
+ msg << "'" << result1 << " != " << "'" << result2 << "'"; \
+ throw TestFail(__FILE__, __LINE__, msg.str()); \
+ } \
+ }
+
+#define LIBBOARDGAME_CHECK_THROW(expr, exception) \
+ { \
+ using libboardgame_test::TestFail; \
+ bool was_thrown = false; \
+ try \
+ { \
+ expr; \
+ } \
+ catch (const exception&) \
+ { \
+ was_thrown = true; \
+ } \
+ if (! was_thrown) \
+ { \
+ ostringstream msg; \
+ msg << "Exception '" << #exception << "' was not thrown"; \
+ throw TestFail(__FILE__, __LINE__, msg.str()); \
+ } \
+ }
+
+#define LIBBOARDGAME_CHECK_NO_THROW(expr) \
+ { \
+ using libboardgame_test::TestFail; \
+ try \
+ { \
+ expr; \
+ } \
+ catch (...) \
+ { \
+ throw TestFail(__FILE__, __LINE__, \
+ "Unexcpected exception was thrown"); \
+ } \
+ }
+
+/** Compare floating points using a tolerance in percent. */
+#define LIBBOARDGAME_CHECK_CLOSE(expr1, expr2, tolerance) \
+ { \
+ using libboardgame_test::TestFail; \
+ auto result1 = (expr1); \
+ auto result2 = (expr2); \
+ if (fabs(result1 - result2) > 0.01 * tolerance * result1) \
+ { \
+ ostringstream msg; \
+ msg << "Difference between " << result1 << " and " \
+ << result2 << " exceeds " << (0.01 * tolerance) \
+ << " percent"; \
+ throw TestFail(__FILE__, __LINE__, msg.str()); \
+ } \
+ }
+
+/** Compare floating points using an epsilon. */
+#define LIBBOARDGAME_CHECK_CLOSE_EPS(expr1, expr2, epsilon) \
+ { \
+ using libboardgame_test::TestFail; \
+ auto result1 = (expr1); \
+ auto result2 = (expr2); \
+ if (fabs(result1 - result2) > epsilon) \
+ { \
+ ostringstream msg; \
+ msg << "Difference between " << result1 << " and " \
+ << result2 << " exceeds " << epsilon; \
+ throw TestFail(__FILE__, __LINE__, msg.str()); \
+ } \
+ }
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBBOARDGAME_TEST_TEST_H
--- /dev/null
+add_library(boardgame_test_main STATIC Main.cpp)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_test_main/Main.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_test/Test.h"
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+ return libboardgame_test::test_main(argc, argv);
+}
+
+//----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Abort.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Abort.h"
+
+//----------------------------------------------------------------------------
+
+namespace libboardgame_util {
+
+using namespace std;
+
+atomic<bool> abort(false);
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Abort.h
+ Global flag to interrupt move generation or other commands.
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_ABORT_H
+#define LIBBOARDGAME_UTIL_ABORT_H
+
+#include <atomic>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+extern atomic<bool> abort;
+
+inline void clear_abort()
+{
+ abort.store(false, memory_order_seq_cst);
+}
+
+inline bool get_abort()
+{
+ return abort.load(memory_order_relaxed);
+}
+
+inline void set_abort()
+{
+ abort.store(true, memory_order_seq_cst);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_ABORT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/ArrayList.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_ARRAY_LIST_H
+#define LIBBOARDGAME_UTIL_ARRAY_LIST_H
+
+#include <algorithm>
+#include <array>
+#include <initializer_list>
+#include <iostream>
+#include "Assert.h"
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Array-based list with maximum number of elements.
+ The user is responsible for not inserting more than the maximum number of
+ elements. The elements must be default-constructible. If the size of the
+ list shrinks, the destructor of elements will be not be called and the
+ elements above the current size are still usable with get_unchecked().
+ The list contains iterator definitions that are compatible with STL
+ containers.
+ @tparam T The type of the elements
+ @tparam M The maximum number of elements
+ @tparam I The integer type for the array size */
+template<typename T, unsigned M, typename I = unsigned>
+class ArrayList
+{
+public:
+ typedef I IntType;
+
+ static_assert(numeric_limits<IntType>::is_integer, "");
+
+ static const IntType max_size = M;
+
+ typedef typename array<T, max_size>::iterator iterator;
+
+ typedef typename array<T, max_size>::const_iterator const_iterator;
+
+ typedef T value_type;
+
+
+ ArrayList() = default;
+
+ /** Construct list with a single element. */
+ explicit ArrayList(const T& t);
+
+ explicit ArrayList(const initializer_list<T>& l);
+
+ /** Assignment operator.
+ Copies only elements with index below the current size. */
+ ArrayList& operator=(const ArrayList& l);
+
+ T& operator[](I i);
+
+ const T& operator[](I i) const;
+
+ /** Get an element whose index may be higher than the current size. */
+ T& get_unchecked(I i);
+
+ /** Get an element whose index may be higher than the current size. */
+ const T& get_unchecked(I i) const;
+
+ bool operator==(const ArrayList& array_list) const;
+
+ bool operator!=(const ArrayList& array_list) const;
+
+ iterator begin();
+
+ const_iterator begin() const;
+
+ iterator end();
+
+ const_iterator end() const;
+
+ T& back();
+
+ const T& back() const;
+
+ I size() const;
+
+ bool empty() const;
+
+ const T& pop_back();
+
+ void push_back(const T& t);
+
+ void clear();
+
+ void assign(const T& t);
+
+ /** Change the size of the list.
+ Does not call constructors on new elements if the size grows or
+ destructors of elements if the size shrinks. */
+ void resize(I size);
+
+ bool contains(const T& t) const;
+
+ /** Push back element if not already contained in list.
+ @return @c true if element was not already in list. */
+ bool include(const T& t);
+
+ /** Removal of first occurrence of value.
+ Preserves the order of elements.
+ @return @c true if value was removed. */
+ bool remove(const T& t);
+
+ /** Fast removal of element.
+ Does not preserve the order of elements. The element will be replaced
+ with the last element and the list size decremented. */
+ void remove_fast(iterator i);
+
+ /** Fast removal of first occurrence of value.
+ Does not preserve the order of elements. If the value is found,
+ it will be replaced with the last element and the list size
+ decremented.
+ @return @c true if value was removed. */
+ bool remove_fast(const T& t);
+
+private:
+ array<T, max_size> m_a;
+
+ I m_size = 0;
+};
+
+template<typename T, unsigned M, typename I>
+inline ArrayList<T, M, I>::ArrayList(const T& t)
+{
+ assign(t);
+}
+
+template<typename T, unsigned M, typename I>
+ArrayList<T, M, I>::ArrayList(const initializer_list<T>& l)
+ : m_size(0)
+{
+ for (auto& t : l)
+ push_back(t);
+}
+
+template<typename T, unsigned M, typename I>
+auto ArrayList<T, M, I>::operator=(const ArrayList& l) -> ArrayList&
+{
+ m_size = l.size();
+ copy(l.begin(), l.end(), begin());
+ return *this;
+}
+
+template<typename T, unsigned M, typename I>
+inline T& ArrayList<T, M, I>::operator[](I i)
+{
+ LIBBOARDGAME_ASSERT(i < m_size);
+ return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::operator[](I i) const
+{
+ LIBBOARDGAME_ASSERT(i < m_size);
+ return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::operator==(const ArrayList& array_list) const
+{
+ if (m_size != array_list.m_size)
+ return false;
+ return equal(begin(), end(), array_list.begin());
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::operator!=(const ArrayList& array_list) const
+{
+ return ! operator==(array_list);
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::assign(const T& t)
+{
+ m_size = 1;
+ m_a[0] = t;
+}
+
+template<typename T, unsigned M, typename I>
+inline T& ArrayList<T, M, I>::back()
+{
+ LIBBOARDGAME_ASSERT(m_size > 0);
+ return m_a[m_size - 1];
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::back() const
+{
+ LIBBOARDGAME_ASSERT(m_size > 0);
+ return m_a[m_size - 1];
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::begin() -> iterator
+{
+ return m_a.begin();
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::begin() const -> const_iterator
+{
+ return m_a.begin();
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::clear()
+{
+ m_size = 0;
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::contains(const T& t) const
+{
+ return find(begin(), end(), t) != end();
+}
+
+template<typename T, unsigned M, typename I>
+inline bool ArrayList<T, M, I>::empty() const
+{
+ return m_size == 0;
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::end() -> iterator
+{
+ return begin() + m_size;
+}
+
+template<typename T, unsigned M, typename I>
+inline auto ArrayList<T, M, I>::end() const -> const_iterator
+{
+ return begin() + m_size;
+}
+
+template<typename T, unsigned M, typename I>
+inline T& ArrayList<T, M, I>::get_unchecked(I i)
+{
+ LIBBOARDGAME_ASSERT(i < max_size);
+ return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::get_unchecked(I i) const
+{
+ LIBBOARDGAME_ASSERT(i < max_size);
+ return m_a[i];
+}
+
+template<typename T, unsigned M, typename I>
+bool ArrayList<T, M, I>::include(const T& t)
+{
+ if (contains(t))
+ return false;
+ push_back(t);
+ return true;
+}
+
+template<typename T, unsigned M, typename I>
+inline const T& ArrayList<T, M, I>::pop_back()
+{
+ LIBBOARDGAME_ASSERT(m_size > 0);
+ return m_a[--m_size];
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::push_back(const T& t)
+{
+ LIBBOARDGAME_ASSERT(m_size < max_size);
+ m_a[m_size++] = t;
+}
+
+template<typename T, unsigned M, typename I>
+inline bool ArrayList<T, M, I>::remove(const T& t)
+{
+ auto end = this->end();
+ for (auto i = begin(); i != end; ++i)
+ if (*i == t)
+ {
+ --end;
+ for ( ; i != end; ++i)
+ *i = *(i + 1);
+ --m_size;
+ return true;
+ }
+ return false;
+}
+
+template<typename T, unsigned M, typename I>
+inline bool ArrayList<T, M, I>::remove_fast(const T& t)
+{
+ auto end = this->end();
+ for (auto i = this->begin(); i != end; ++i)
+ if (*i == t)
+ {
+ remove_fast(i);
+ return true;
+ }
+ return false;
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::remove_fast(iterator i)
+{
+ LIBBOARDGAME_ASSERT(i >= begin());
+ LIBBOARDGAME_ASSERT(i < end());
+ --m_size;
+ *i = *(begin() + m_size);
+}
+
+template<typename T, unsigned M, typename I>
+inline void ArrayList<T, M, I>::resize(I size)
+{
+ LIBBOARDGAME_ASSERT(size <= max_size);
+ m_size = size;
+}
+
+template<typename T, unsigned M, typename I>
+inline I ArrayList<T, M, I>::size() const
+{
+ return m_size;
+}
+
+//-----------------------------------------------------------------------------
+
+template<typename T, unsigned M, typename I>
+ostream& operator<<(ostream& out, const ArrayList<T, M, I>& l)
+{
+ auto begin = l.begin();
+ auto end = l.end();
+ if (begin != end)
+ {
+ out << *begin;
+ for (auto i = begin + 1; i != end; ++i)
+ out << ' ' << *i;
+ }
+ return out;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_ARRAY_LIST_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Assert.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Assert.h"
+
+#include <list>
+
+#if LIBBOARDGAME_DEBUG
+#include <algorithm>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <vector>
+#include "Log.h"
+#endif
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+list<AssertionHandler*>& get_all_handlers()
+{
+ static list<AssertionHandler*> all_handlers;
+ return all_handlers;
+}
+
+} // namespace
+
+//----------------------------------------------------------------------------
+
+AssertionHandler::AssertionHandler()
+{
+ get_all_handlers().push_back(this);
+}
+
+AssertionHandler::~AssertionHandler()
+{
+ get_all_handlers().remove(this);
+}
+
+//----------------------------------------------------------------------------
+
+#if LIBBOARDGAME_DEBUG
+
+void handle_assertion(const char* expression, const char* file, int line)
+{
+ static bool is_during_handle_assertion = false;
+ LIBBOARDGAME_LOG(file, ":", line, ": Assertion '", expression, "' failed");
+ flush_log();
+ if (! is_during_handle_assertion)
+ {
+ is_during_handle_assertion = true;
+ for_each(get_all_handlers().begin(), get_all_handlers().end(),
+ mem_fun(&AssertionHandler::run));
+ }
+ abort();
+}
+
+#endif
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Assert.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_ASSERT_H
+#define LIBBOARDGAME_UTIL_ASSERT_H
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+class AssertionHandler
+{
+public:
+ /** Construct and register assertion handler. */
+ AssertionHandler();
+
+ /** Destruct and unregister assertion handler. */
+ virtual ~AssertionHandler();
+
+ virtual void run() = 0;
+};
+
+#if LIBBOARDGAME_DEBUG
+
+/** Function used by the LIBBOARDGAME_ASSERT macro to run all assertion
+ handlers. */
+[[noreturn]] void handle_assertion(const char* expression, const char* file,
+ int line);
+
+#endif
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+//-----------------------------------------------------------------------------
+
+/** @def LIBBOARDGAME_ASSERT
+ Enhanced assert macro.
+ This macro is similar to the assert macro in the standard library, but it
+ allows the user to register assertion handlers that are executed before the
+ program is aborted. Assertions are only enabled if the macro
+ LIBBOARDGAME_DEBUG is true. */
+#if LIBBOARDGAME_DEBUG
+#define LIBBOARDGAME_ASSERT(expr) \
+ ((expr) ? (static_cast<void>(0)) \
+ : libboardgame_util::handle_assertion(#expr, __FILE__, __LINE__))
+#else
+#define LIBBOARDGAME_ASSERT(expr) (static_cast<void>(0))
+#endif
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBBOARDGAME_UTIL_ASSERT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Barrier.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Barrier.h"
+
+#include "Assert.h"
+
+namespace libboardgame_util {
+
+//----------------------------------------------------------------------------
+
+Barrier::Barrier(unsigned count)
+ : m_threshold(count),
+ m_count(count)
+{
+ LIBBOARDGAME_ASSERT(count > 0);
+}
+
+void Barrier::wait()
+{
+ unique_lock<mutex> lock(m_mutex);
+ unsigned current = m_current;
+ if (--m_count == 0)
+ {
+ ++m_current;
+ m_count = m_threshold;
+ m_condition.notify_all();
+ }
+ else
+ while (current == m_current)
+ m_condition.wait(lock);
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Barrier.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_BARRIER_H
+#define LIBBOARDGAME_UTIL_BARRIER_H
+
+#include <condition_variable>
+#include <mutex>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Similar to boost::barrier, which does not exist in C++11 */
+class Barrier
+{
+public:
+ explicit Barrier(unsigned count);
+
+ void wait();
+
+private:
+ mutex m_mutex;
+
+ condition_variable m_condition;
+
+ unsigned m_threshold;
+
+ unsigned m_count;
+
+ unsigned m_current = 0;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_BARRIER_H
--- /dev/null
+add_library(boardgame_util STATIC
+ Abort.h
+ Abort.cpp
+ ArrayList.h
+ Assert.h
+ Assert.cpp
+ Barrier.h
+ Barrier.cpp
+ CpuTimeSource.h
+ CpuTimeSource.cpp
+ FmtSaver.h
+ IntervalChecker.h
+ IntervalChecker.cpp
+ Log.h
+ Log.cpp
+ MathUtil.h
+ Options.h
+ Options.cpp
+ RandomGenerator.h
+ RandomGenerator.cpp
+ Range.h
+ Statistics.h
+ StringUtil.h
+ StringUtil.cpp
+ TimeIntervalChecker.h
+ TimeIntervalChecker.cpp
+ Timer.h
+ Timer.cpp
+ TimeSource.h
+ TimeSource.cpp
+ Unused.h
+ WallTimeSource.h
+ WallTimeSource.cpp
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/CpuTimeSource.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "CpuTimeSource.h"
+
+#include "libboardgame_sys/CpuTime.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+double CpuTimeSource::operator()()
+{
+ return libboardgame_sys::cpu_time();
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/CpuTimeSource.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H
+#define LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H
+
+#include "TimeSource.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+/** CPU time.
+ @ref libboardgame_doc_threadsafe_after_construction */
+class CpuTimeSource
+ : public TimeSource
+{
+public:
+ double operator()() override;
+};
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_CPU_TIME_SOURCE_H
--- /dev/null
+//----------------------------------------------------------------------------
+/** @file libboardgame_util/FmtSaver.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_FMT_SAVER_H
+#define LIBBOARDGAME_UTIL_FMT_SAVER_H
+
+#include <iostream>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Saves the formatting state of a stream and restores it in its
+ destructor. */
+class FmtSaver
+{
+public:
+ explicit FmtSaver(ostream& out)
+ : m_out(out)
+ {
+ m_dummy.copyfmt(out);
+ }
+
+ ~FmtSaver()
+ {
+ m_out.copyfmt(m_dummy);
+ }
+
+private:
+ ostream& m_out;
+
+ ios m_dummy{nullptr};
+};
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_FMT_SAVER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/IntervalChecker.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "IntervalChecker.h"
+
+#include <limits>
+#include "Assert.h"
+#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG
+#include "Log.h"
+#endif
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG
+#define LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG 0
+#endif
+
+//-----------------------------------------------------------------------------
+
+IntervalChecker::IntervalChecker(TimeSource& time_source, double time_interval,
+ function<bool()> f)
+ : m_time_source(time_source),
+ m_time_interval(time_interval),
+ m_function(move(f))
+{
+#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG
+ log(format("IntervalChecker::IntervalChecker: time_interval=%1%")
+ % time_interval);
+#endif
+ LIBBOARDGAME_ASSERT(time_interval > 0);
+}
+
+bool IntervalChecker::check_expensive()
+{
+ if (m_result)
+ return true;
+ if (m_is_deterministic)
+ {
+ m_result = m_function();
+ m_count = m_count_interval;
+ return m_result;
+ }
+ double time = m_time_source();
+ if (! m_is_first_check)
+ {
+
+ double diff = time - m_last_time;
+ double adjust_factor;
+ if (diff == 0)
+ adjust_factor = 10;
+ else
+ {
+ adjust_factor = m_time_interval / diff;
+ if (adjust_factor > 10)
+ adjust_factor = 10;
+ else if (adjust_factor < 0.1)
+ adjust_factor = 0.1;
+ }
+ double new_count_interval = adjust_factor * double(m_count_interval);
+ if (new_count_interval > double(numeric_limits<unsigned>::max()))
+ m_count_interval = numeric_limits<unsigned>::max();
+ else if (new_count_interval < 1)
+ m_count_interval = 1;
+ else
+ m_count_interval = (unsigned)(new_count_interval);
+ m_result = m_function();
+#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG
+ log(format("IntervalChecker::check_expensive: "
+ "diff=%1% adjust_factor=%2% count_interval=%3%")
+ % diff % adjust_factor % m_count_interval);
+#endif
+ }
+ else
+ {
+#if LIBBOARDGAME_UTIL_INTERVAL_CHECKER_DEBUG
+ log("IntervalChecker::check_expensive: is_first_check");
+#endif
+ m_is_first_check = false;
+ }
+ m_last_time = time;
+ m_count = m_count_interval;
+ return m_result;
+}
+
+void IntervalChecker::set_deterministic(unsigned interval)
+{
+ LIBBOARDGAME_ASSERT(interval >= 1);
+ m_is_deterministic = true;
+ m_count = interval;
+ m_count_interval = interval;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/IntervalChecker.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H
+#define LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H
+
+#include <functional>
+#include "libboardgame_util/TimeSource.h"
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Reduces regular calls to an expensive function to a given time interval.
+ The class assumes that its check() function is called in regular time
+ intervals and forwards only every n'th call to the expensive function with
+ n being adjusted dynamically to a given time interval. check() returns
+ true, if the expensive function was called and returned true in the
+ past. */
+class IntervalChecker
+{
+public:
+ /** Constructor.
+ @param time_source (@ref libboardgame_doc_storesref)
+ @param time_interval The time interval in seconds
+ @param f The expensive function */
+ IntervalChecker(TimeSource& time_source, double time_interval,
+ function<bool()> f);
+
+ bool operator()();
+
+ /** Disable the dynamic updating of the interval.
+ Can be used if the non-reproducability of the time measurement used
+ for dynamic updating of the check interval is undesirable.
+ @param interval The fixed interval (number of calls) to use for calling
+ the expensive function. (Must be greater zero). */
+ void set_deterministic(unsigned interval);
+
+protected:
+ TimeSource& m_time_source;
+
+private:
+ bool m_is_first_check = true;
+
+ bool m_is_deterministic = false;
+
+ bool m_result = false;
+
+ unsigned m_count = 1;
+
+ unsigned m_count_interval = 1;
+
+ double m_time_interval;
+
+ double m_last_time;
+
+ function<bool()> m_function;
+
+ bool check_expensive();
+};
+
+inline bool IntervalChecker::operator()()
+{
+ if (--m_count == 0)
+ return check_expensive();
+ else
+ return m_result;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_INTERVAL_CHECKER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Log.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#if ! LIBBOARDGAME_DISABLE_LOG
+
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Log.h"
+
+#include <iostream>
+
+#if defined ANDROID || defined __ANDROID__
+#include <android/log.h>
+#endif
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+#if defined ANDROID || defined __ANDROID__
+
+class AndroidBuf
+ : public streambuf
+{
+public:
+ AndroidBuf();
+
+protected:
+ int_type overflow(int_type c) override;
+
+ int sync() override;
+
+private:
+ static const unsigned buffer_size = 8192;
+
+ char m_buffer[buffer_size];
+};
+
+AndroidBuf::AndroidBuf()
+{
+ setp(m_buffer, m_buffer + buffer_size - 1);
+}
+
+auto AndroidBuf::overflow(int_type c) -> int_type
+{
+ if (c == traits_type::eof())
+ {
+ *pptr() = traits_type::to_char_type(c);
+ sbumpc();
+ }
+ return sync() ? traits_type::eof(): traits_type::not_eof(c);
+}
+
+int AndroidBuf::sync()
+{
+ int n = 0;
+ if (pbase() != pptr())
+ {
+ __android_log_print(ANDROID_LOG_INFO, "Native", "%s",
+ string(pbase(), pptr() - pbase()).c_str());
+ n = 0;
+ setp(m_buffer, m_buffer + buffer_size - 1);
+ }
+ return n;
+}
+
+AndroidBuf android_buffer;
+
+#endif // defined(ANDROID) || defined(__ANDROID__)
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+ostream* _log_stream = &cerr;
+
+//-----------------------------------------------------------------------------
+
+void _log(const string& s)
+{
+ if (! _log_stream)
+ return;
+ if (s.empty())
+ *_log_stream << '\n';
+ else if (s.back() == '\n')
+ *_log_stream << s;
+ else
+ {
+ string line = s;
+ line += '\n';
+ *_log_stream << line;
+ }
+}
+
+void _log_close()
+{
+#if defined ANDROID || defined __ANDROID__
+ cerr.rdbuf(nullptr);
+#endif
+}
+
+void _log_init()
+{
+#if defined ANDROID || defined __ANDROID__
+ cerr.rdbuf(&android_buffer);
+#endif
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+//-----------------------------------------------------------------------------
+
+#endif // ! LIBBOARDGAME_DISABLE_LOG
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Log.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_LOG_H
+#define LIBBOARDGAME_UTIL_LOG_H
+
+#include <sstream>
+#include <string>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+#if ! LIBBOARDGAME_DISABLE_LOG
+extern ostream* _log_stream;
+#endif
+
+inline void disable_logging()
+{
+#if ! LIBBOARDGAME_DISABLE_LOG
+ _log_stream = nullptr;
+#endif
+}
+
+inline ostream* get_log_stream()
+{
+#if ! LIBBOARDGAME_DISABLE_LOG
+ return _log_stream;
+#else
+ return nullptr;
+#endif
+}
+
+inline void flush_log()
+{
+#if ! LIBBOARDGAME_DISABLE_LOG
+ if (_log_stream)
+ _log_stream->flush();
+#endif
+}
+
+//-----------------------------------------------------------------------------
+
+#if ! LIBBOARDGAME_DISABLE_LOG
+
+/** Initializes the logging functionality.
+ This is necessary to call on some platforms at the start of the program
+ before any calls to log().
+ @see LogInitializer */
+void _log_init();
+
+/** Closes the logging functionality.
+ This is necessary to call on some platforms before the program exits.
+ @see LogInitializer */
+void _log_close();
+
+/** Helper function needed for log(const Ts&...) */
+template<typename T>
+void _log_buffered(ostream& buffer, const T& t)
+{
+ buffer << t;
+}
+
+/** Helper function needed for log(const Ts&...) */
+template<typename T, typename... Ts>
+void _log_buffered(ostream& buffer, const T& first, const Ts&... rest)
+{
+ buffer << first;
+ _log_buffered(buffer, rest...);
+}
+
+/** Write a string to the log stream.
+ Appends a newline if the output has no newline at the end. */
+void _log(const string& s);
+
+/** Write a number of arguments to the log stream.
+ Writes to a buffer first so there is only a single write to the log
+ stream. Appends a newline if the output has no newline at the end. */
+template<typename... Ts>
+void _log(const Ts&... args)
+{
+ if (! _log_stream)
+ return;
+ ostringstream buffer;
+ _log_buffered(buffer, args...);
+ _log(buffer.str());
+}
+
+#endif // ! LIBBOARDGAME_DISABLE_LOG
+
+//-----------------------------------------------------------------------------
+
+class LogInitializer
+{
+public:
+ LogInitializer()
+ {
+#if ! LIBBOARDGAME_DISABLE_LOG
+ _log_init();
+#endif
+ }
+
+ ~LogInitializer()
+ {
+#if ! LIBBOARDGAME_DISABLE_LOG
+ _log_close();
+#endif
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+//-----------------------------------------------------------------------------
+
+#if ! LIBBOARDGAME_DISABLE_LOG
+#define LIBBOARDGAME_LOG(...) libboardgame_util::_log(__VA_ARGS__)
+#else
+#define LIBBOARDGAME_LOG(...) (static_cast<void>(0))
+#endif
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBBOARDGAME_UTIL_LOG_H
--- /dev/null
+//----------------------------------------------------------------------------
+/** @file libboardgame_util/MathUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_MATH_UTIL_H
+#define LIBBOARDGAME_UTIL_MATH_UTIL_H
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Fast approximation of exp(x).
+ The error is less than 15% for abs(x) \< 10 */
+template<typename T>
+inline T fast_exp(T x)
+{
+ x = static_cast<T>(1) + x / static_cast<T>(256);
+ x *= x;
+ x *= x;
+ x *= x;
+ x *= x;
+ x *= x;
+ x *= x;
+ x *= x;
+ x *= x;
+ return x;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_MATH_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Options.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Options.h"
+
+namespace libboardgame_util {
+
+//----------------------------------------------------------------------------
+
+Options::Options(int argc, const char** argv, const vector<string>& specs)
+{
+ for (auto& s : specs)
+ {
+ auto pos = s.find("|");
+ if (pos == string::npos)
+ pos = s.find(":");
+ if (pos != string::npos)
+ m_names.insert(s.substr(0, pos));
+ else
+ m_names.insert(s);
+ }
+
+ bool end_of_options = false;
+ for (int n = 1; n < argc; ++n)
+ {
+ const string arg = argv[n];
+ if (! end_of_options && arg.find("-") == 0 && arg != "-")
+ {
+ if (arg == "--")
+ {
+ end_of_options = true;
+ continue;
+ }
+ string name;
+ string value;
+ bool needs_arg = false;
+ if (arg.find("--") == 0)
+ {
+ // Long option
+ name = arg.substr(2);
+ auto sz = name.size();
+ bool found = false;
+ for (auto& spec : specs)
+ if (spec.find(name) == 0
+ && (spec.size() == sz || spec[sz] == '|'
+ || spec[sz] == ':' ))
+ {
+ found = true;
+ needs_arg = (! spec.empty() && spec.back() == ':');
+ break;
+ }
+ if (! found)
+ throw OptionError("Unknown option " + arg);
+ }
+ else
+ {
+ // Short options
+ for (unsigned i = 1; i < arg.size(); ++i)
+ {
+ auto c = arg[i];
+ bool found = false;
+ for (auto& spec : specs)
+ {
+ auto pos = spec.find("|" + string(1, c));
+ if (pos != string::npos)
+ {
+ name = spec.substr(0, pos);
+ found = true;
+ if (! spec.empty() && spec.back() == ':')
+ {
+ // If not last option, no space was used to
+ // append the value
+ if (i != arg.size() - 1)
+ value = arg.substr(i + 1);
+ else
+ needs_arg = true;
+ }
+ break;
+ }
+ }
+ if (! found)
+ throw OptionError("Unknown option -" + string(1, c));
+ if (needs_arg || ! value.empty())
+ break;
+ m_map.insert(make_pair(name, ""));
+ }
+ }
+ if (needs_arg)
+ {
+ bool value_found = false;
+ ++n;
+ if (n < argc)
+ {
+ value = argv[n];
+ if (value.empty() || value[0] != '-')
+ value_found = true;
+ }
+ if (! value_found)
+ throw OptionError("Option --" + name + " needs value");
+ }
+ m_map.insert(make_pair(name, value));
+ }
+ else
+ m_args.push_back(arg);
+ }
+}
+
+Options::Options(int argc, char** argv, const vector<string>& specs)
+ : Options(argc, const_cast<const char**>(argv), specs)
+{
+}
+
+Options::~Options()
+{
+}
+
+void Options::check_name(const string& name) const
+{
+ if (m_names.count(name) == 0)
+ throw OptionError("Internal error: invalid option name " + name);
+}
+
+bool Options::contains(const string& name) const
+{
+ check_name(name);
+ return m_map.count(name) > 0;
+}
+
+string Options::get(const string& name) const
+{
+ check_name(name);
+ auto pos = m_map.find(name);
+ if (pos == m_map.end())
+ throw OptionError("Missing option --" + name);
+ return pos->second;
+}
+
+string Options::get(const string& name, const string& default_value) const
+{
+ check_name(name);
+ auto pos = m_map.find(name);
+ if (pos == m_map.end())
+ return default_value;
+ return pos->second;
+}
+
+string Options::get(const string& name, const char* default_value) const
+{
+ return get(name, string(default_value));
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Options.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_OPTIONS_H
+#define LIBBOARDGAME_UTIL_OPTIONS_H
+
+#include <map>
+#include <set>
+#include <stdexcept>
+#include <string>
+#include <vector>
+#include "StringUtil.h"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libboardgame_util {
+
+using namespace std;
+using libboardgame_sys::get_type_name;
+
+//----------------------------------------------------------------------------
+
+class OptionError
+ : public runtime_error
+{
+ using runtime_error::runtime_error;
+};
+
+//----------------------------------------------------------------------------
+
+/** Parser for command line options.
+ The syntax of options is similar to GNU getopt. Options start with "--"
+ and an option name. Options have optional short (single-character) names
+ that are used with a single "-" and can be combined if all but the last
+ option have no value. A single "--" stops option parsing to support
+ non-option arguments that start with "-". */
+class Options
+{
+public:
+ /** Create options from arguments to main().
+ @param argc
+ @param argv
+ @param specs A string per option that describes the option. The
+ description is the long name of the option, followed by and optional
+ '|' and a character for the short name of the option, followed by an
+ optional ':' if the option needs a value.
+ @throws OptionError on error */
+ Options(int argc, const char** argv, const vector<string>& specs);
+
+ /** Overloaded version for con-const character strings in argv.
+ Needed because the portable signature of main is (int, char**).
+ argv is not modified by this constructor. */
+ Options(int argc, char** argv, const vector<string>& specs);
+
+ ~Options();
+
+ /** Check if an option exists in the command line arguments.
+ @param name The (long) option name. */
+ bool contains(const string& name) const;
+
+ string get(const string& name) const;
+
+ string get(const string& name, const string& default_value) const;
+
+ string get(const string& name, const char* default_value) const;
+
+ /** Get option value.
+ @param name The (long) option name.
+ @throws OptionError If option does not exist or has the wrong type. */
+ template<typename T>
+ T get(const string& name) const;
+
+ /** Get option value or default value.
+ @param name The (long) option name.
+ @param default_value A default value.
+ @return The option value or the default value if the option does not
+ exist. */
+ template<typename T>
+ T get(const string& name, const T& default_value) const;
+
+ /** Remaining command line arguments that are not an option or an option
+ value. */
+ const vector<string>& get_args() const;
+
+private:
+ set<string> m_names;
+
+ vector<string> m_args;
+
+ map<string, string> m_map;
+
+ void check_name(const string& name) const;
+};
+
+template<typename T>
+T Options::get(const string& name) const
+{
+ T t;
+ if (! from_string(get(name), t))
+ throw OptionError("Option --" + name + " needs type "
+ + get_type_name(t));
+ return t;
+}
+
+template<typename T>
+T Options::get(const string& name, const T& default_value) const
+{
+ if (! contains(name))
+ return default_value;
+ return get<T>(name);
+}
+
+inline const vector<string>& Options::get_args() const
+{
+ return m_args;
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_OPTIONS_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/RandomGenerator.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "RandomGenerator.h"
+
+#include <list>
+
+namespace libboardgame_util {
+
+//----------------------------------------------------------------------------
+
+namespace {
+
+bool is_seed_set = false;
+
+RandomGenerator::ResultType the_seed;
+
+list<RandomGenerator*>& get_all_generators()
+{
+ static list<RandomGenerator*> all_generators;
+ return all_generators;
+}
+
+RandomGenerator::ResultType get_nondet_seed()
+{
+ random_device generator;
+ return generator();
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+RandomGenerator::RandomGenerator()
+{
+ set_seed(is_seed_set ? the_seed : get_nondet_seed());
+ get_all_generators().push_back(this);
+}
+
+RandomGenerator::~RandomGenerator()
+{
+ get_all_generators().remove(this);
+}
+
+bool RandomGenerator::has_global_seed()
+{
+ return is_seed_set;
+}
+
+void RandomGenerator::set_global_seed(ResultType seed)
+{
+ is_seed_set = true;
+ the_seed = seed;
+ for (RandomGenerator* i : get_all_generators())
+ i->set_seed(the_seed);
+}
+
+void RandomGenerator::set_global_seed_last()
+{
+ if (is_seed_set)
+ for (RandomGenerator* i : get_all_generators())
+ i->set_seed(the_seed);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/RandomGenerator.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H
+#define LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H
+
+#include <random>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Fast pseudo-random number generator.
+ This is a fast and low-quality pseudo-random number generator for tasks
+ like opening book move selection or even playouts in Monte-Carlo tree
+ search (does not seem to be sensitive to the quality of the generator).
+ All instances of this class register themselves automatically at a
+ global list of random generators, such that the random seed can be
+ changed at all existing generators with a single function call.
+ (@ref libboardgame_doc_threadsafe_after_construction) */
+class RandomGenerator
+{
+public:
+ typedef minstd_rand Generator;
+
+ typedef Generator::result_type ResultType;
+
+
+ /** Set seed for all currently existing and future generators.
+ If this function is never called, a non-deterministic seed is used. */
+ static void set_global_seed(ResultType seed);
+
+ /** Set seed to last seed for all currently existing and future
+ generators.
+ Sets the seed to the last seed that was set with set_seed(). If no seed
+ was explicitely defined with set_seed(), then this function does
+ nothing. */
+ static void set_global_seed_last();
+
+ /** Check if a global seed was set.
+ User code might want to take more measures if a global seed was set to
+ become fully deterministic (e.g. avoid decisions based on time
+ measurements). */
+ static bool has_global_seed();
+
+
+ /** Constructor.
+ Constructs the random generator with the global seed, if one was
+ defined, otherwise with a non-deterministic seed. */
+ RandomGenerator();
+
+ ~RandomGenerator();
+
+ void set_seed(ResultType seed);
+
+ ResultType generate();
+
+ /** Generate a float in [a..b]. */
+ float generate_float(float a, float b);
+
+ /** Generate a double in [a..b]. */
+ double generate_double(double a, double b);
+
+private:
+ Generator m_generator;
+};
+
+inline RandomGenerator::ResultType RandomGenerator::generate()
+{
+ return m_generator();
+}
+
+inline double RandomGenerator::generate_double(double a, double b)
+{
+ uniform_real_distribution<double> distribution(a, b);
+ return distribution(m_generator);
+}
+
+inline float RandomGenerator::generate_float(float a, float b)
+{
+ uniform_real_distribution<float> distribution(a, b);
+ return distribution(m_generator);
+}
+
+inline void RandomGenerator::set_seed(ResultType seed)
+{
+ m_generator.seed(seed);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_RANDOM_GENERATOR_H
--- /dev/null
+//----------------------------------------------------------------------------
+/** @file libboardgame_util/Range.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_RANGE_H
+#define LIBBOARDGAME_UTIL_RANGE_H
+
+#include <cstddef>
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+template<typename T>
+class Range
+{
+public:
+ Range(T* begin, T* end)
+ : m_begin(begin),
+ m_end(end)
+ { }
+
+ T* begin() const { return m_begin; }
+
+ T* end() const { return m_end; }
+
+ size_t size() const { return m_end - m_begin; }
+
+ bool contains(T& t) const;
+
+private:
+ T* m_begin;
+
+ T* m_end;
+};
+
+template<typename T>
+bool Range<T>::contains(T& t) const
+{
+ for (auto& i : *this)
+ if (i == t)
+ return true;
+ return false;
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_RANGE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Statistics.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_STATISTICS_H
+#define LIBBOARDGAME_UTIL_STATISTICS_H
+
+#include <atomic>
+#include <cmath>
+#include <iomanip>
+#include <iosfwd>
+#include <limits>
+#include <sstream>
+#include <string>
+#include "FmtSaver.h"
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+template<typename FLOAT = double>
+class StatisticsBase
+{
+public:
+ /** Constructor.
+ @param init_val The value to return in get_mean() if count is 0. This
+ value does not affect the mean returned if count is greater 0. */
+ explicit StatisticsBase(FLOAT init_val = 0);
+
+ void add(FLOAT val);
+
+ void clear(FLOAT init_val = 0);
+
+ FLOAT get_count() const;
+
+ FLOAT get_mean() const;
+
+ void write(ostream& out, bool fixed = false,
+ unsigned precision = 6) const;
+
+private:
+ FLOAT m_count;
+
+ FLOAT m_mean;
+};
+
+template<typename FLOAT>
+inline StatisticsBase<FLOAT>::StatisticsBase(FLOAT init_val)
+{
+ clear(init_val);
+}
+
+template<typename FLOAT>
+void StatisticsBase<FLOAT>::add(FLOAT val)
+{
+ FLOAT count = m_count;
+ ++count;
+ val -= m_mean;
+ m_mean += val / count;
+ m_count = count;
+}
+
+template<typename FLOAT>
+inline void StatisticsBase<FLOAT>::clear(FLOAT init_val)
+{
+ m_count = 0;
+ m_mean = init_val;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsBase<FLOAT>::get_count() const
+{
+ return m_count;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsBase<FLOAT>::get_mean() const
+{
+ return m_mean;
+}
+
+template<typename FLOAT>
+void StatisticsBase<FLOAT>::write(ostream& out, bool fixed,
+ unsigned precision) const
+{
+ FmtSaver saver(out);
+ if (fixed)
+ out << std::fixed;
+ out << setprecision(precision) << m_mean;
+}
+
+//----------------------------------------------------------------------------
+
+template<typename FLOAT = double>
+class Statistics
+{
+public:
+ explicit Statistics(FLOAT init_val = 0);
+
+ void add(FLOAT val);
+
+ void clear(FLOAT init_val = 0);
+
+ FLOAT get_mean() const;
+
+ FLOAT get_count() const;
+
+ FLOAT get_deviation() const;
+
+ FLOAT get_error() const;
+
+ FLOAT get_variance() const;
+
+ void write(ostream& out, bool fixed = false,
+ unsigned precision = 6) const;
+
+private:
+ StatisticsBase<FLOAT> m_statistics_base;
+
+ FLOAT m_variance;
+};
+
+template<typename FLOAT>
+inline Statistics<FLOAT>::Statistics(FLOAT init_val)
+{
+ clear(init_val);
+}
+
+template<typename FLOAT>
+void Statistics<FLOAT>::add(FLOAT val)
+{
+ if (get_count() > 0)
+ {
+ FLOAT count_old = get_count();
+ FLOAT mean_old = get_mean();
+ m_statistics_base.add(val);
+ FLOAT mean = get_mean();
+ FLOAT count = get_count();
+ m_variance = (count_old * (m_variance + mean_old * mean_old)
+ + val * val) / count - mean * mean;
+ }
+ else
+ {
+ m_statistics_base.add(val);
+ m_variance = 0;
+ }
+}
+
+template<typename FLOAT>
+inline void Statistics<FLOAT>::clear(FLOAT init_val)
+{
+ m_statistics_base.clear(init_val);
+ m_variance = 0;
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_count() const
+{
+ return m_statistics_base.get_count();
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_deviation() const
+{
+ // m_variance can become negative (due to rounding errors?)
+ return m_variance < 0 ? 0 : sqrt(m_variance);
+}
+
+template<typename FLOAT>
+FLOAT Statistics<FLOAT>::get_error() const
+{
+ auto count = get_count();
+ return count == 0 ? 0 : get_deviation() / sqrt(count);
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_mean() const
+{
+ return m_statistics_base.get_mean();
+}
+
+template<typename FLOAT>
+inline FLOAT Statistics<FLOAT>::get_variance() const
+{
+ return m_variance;
+}
+
+template<typename FLOAT>
+void Statistics<FLOAT>::write(ostream& out, bool fixed,
+ unsigned precision) const
+{
+ FmtSaver saver(out);
+ if (fixed)
+ out << std::fixed;
+ out << setprecision(precision) << get_mean() << " dev="
+ << get_deviation();
+}
+
+//----------------------------------------------------------------------------
+
+template<typename FLOAT = double>
+class StatisticsExt
+{
+public:
+ explicit StatisticsExt(FLOAT init_val = 0);
+
+ void add(FLOAT val);
+
+ void clear(FLOAT init_val = 0);
+
+ FLOAT get_mean() const;
+
+ FLOAT get_error() const;
+
+ FLOAT get_count() const;
+
+ FLOAT get_max() const;
+
+ FLOAT get_min() const;
+
+ FLOAT get_deviation() const;
+
+ FLOAT get_variance() const;
+
+ void write(ostream& out, bool fixed = false, unsigned precision = 6,
+ bool integer_values = false, bool with_error = false) const;
+
+ string to_string(bool fixed = false, unsigned precision = 6,
+ bool integer_values = false,
+ bool with_error = false) const;
+
+private:
+ Statistics<FLOAT> m_statistics;
+
+ FLOAT m_max;
+
+ FLOAT m_min;
+};
+
+template<typename FLOAT>
+inline StatisticsExt<FLOAT>::StatisticsExt(FLOAT init_val)
+{
+ clear(init_val);
+}
+
+template<typename FLOAT>
+void StatisticsExt<FLOAT>::add(FLOAT val)
+{
+ m_statistics.add(val);
+ if (val > m_max)
+ m_max = val;
+ if (val < m_min)
+ m_min = val;
+}
+
+template<typename FLOAT>
+inline void StatisticsExt<FLOAT>::clear(FLOAT init_val)
+{
+ m_statistics.clear(init_val);
+ m_min = numeric_limits<FLOAT>::max();
+ m_max = -numeric_limits<FLOAT>::max();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_count() const
+{
+ return m_statistics.get_count();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_deviation() const
+{
+ return m_statistics.get_deviation();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_error() const
+{
+ return m_statistics.get_error();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_max() const
+{
+ return m_max;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_mean() const
+{
+ return m_statistics.get_mean();
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_min() const
+{
+ return m_min;
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsExt<FLOAT>::get_variance() const
+{
+ return m_statistics.get_variance();
+}
+
+template<typename FLOAT>
+string StatisticsExt<FLOAT>::to_string(bool fixed, unsigned precision,
+ bool integer_values,
+ bool with_error) const
+{
+ ostringstream s;
+ write(s, fixed, precision, integer_values, with_error);
+ return s.str();
+}
+
+template<typename FLOAT>
+void StatisticsExt<FLOAT>::write(ostream& out, bool fixed, unsigned precision,
+ bool integer_values, bool with_error) const
+{
+ FmtSaver saver(out);
+ out << setprecision(precision);
+ if (fixed)
+ out << std::fixed;
+ out << get_mean();
+ if (with_error)
+ out << "+-" << get_error();
+ out << " dev=" << get_deviation();
+ if (integer_values)
+ out << setprecision(0);
+ out << " min=";
+ if (m_min == numeric_limits<FLOAT>::max())
+ out << "-";
+ else
+ out << m_min;
+ out << " max=";
+ if (m_max == -numeric_limits<FLOAT>::max())
+ out << "-";
+ else
+ out << m_max;
+}
+
+//----------------------------------------------------------------------------
+
+/** Like StatisticsBase, but for lock-free multithreading with potentially
+ lost updates.
+ Updates and accesses of the moving average and the count are atomic but
+ not synchronized and use memory_order_relaxed. Therefore, updates can be
+ lost. Initializing via the constructor, operator= or clear() uses
+ memory_order_seq_cst */
+template<typename FLOAT = double>
+class StatisticsDirtyLockFree
+{
+public:
+ /** Constructor.
+ @param init_val See StatisticBase::StatisticBase() */
+ explicit StatisticsDirtyLockFree(FLOAT init_val = 0);
+
+ StatisticsDirtyLockFree& operator=(const StatisticsDirtyLockFree& s);
+
+ void add(FLOAT val, FLOAT weight = 1);
+
+ void clear(FLOAT init_val = 0);
+
+ void init(FLOAT mean, FLOAT count);
+
+ FLOAT get_count() const;
+
+ FLOAT get_mean() const;
+
+ void write(ostream& out, bool fixed = false,
+ unsigned precision = 6) const;
+
+private:
+ atomic<FLOAT> m_count;
+
+ atomic<FLOAT> m_mean;
+};
+
+template<typename FLOAT>
+inline StatisticsDirtyLockFree<FLOAT>::StatisticsDirtyLockFree(FLOAT init_val)
+{
+ clear(init_val);
+}
+
+template<typename FLOAT>
+StatisticsDirtyLockFree<FLOAT>&
+StatisticsDirtyLockFree<FLOAT>::operator=(const StatisticsDirtyLockFree& s)
+{
+ m_count = s.m_count.load();
+ m_mean = s.m_mean.load();
+ return *this;
+}
+
+template<typename FLOAT>
+void StatisticsDirtyLockFree<FLOAT>::add(FLOAT val, FLOAT weight)
+{
+ FLOAT count = m_count.load(memory_order_relaxed);
+ FLOAT mean = m_mean.load(memory_order_relaxed);
+ count += weight;
+ mean += weight * (val - mean) / count;
+ m_mean.store(mean, memory_order_relaxed);
+ m_count.store(count, memory_order_relaxed);
+}
+
+template<typename FLOAT>
+inline void StatisticsDirtyLockFree<FLOAT>::clear(FLOAT init_val)
+{
+ init(init_val, 0);
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsDirtyLockFree<FLOAT>::get_count() const
+{
+ return m_count.load(memory_order_relaxed);
+}
+
+template<typename FLOAT>
+inline FLOAT StatisticsDirtyLockFree<FLOAT>::get_mean() const
+{
+ return m_mean.load(memory_order_relaxed);
+}
+
+template<typename FLOAT>
+inline void StatisticsDirtyLockFree<FLOAT>::init(FLOAT mean, FLOAT count)
+{
+ m_count = count;
+ m_mean = mean;
+}
+
+template<typename FLOAT>
+void StatisticsDirtyLockFree<FLOAT>::write(ostream& out, bool fixed,
+ unsigned precision) const
+{
+ FmtSaver saver(out);
+ if (fixed)
+ out << std::fixed;
+ out << setprecision(precision) << get_mean();
+}
+
+//----------------------------------------------------------------------------
+
+template<typename FLOAT>
+inline ostream& operator<<(ostream& out, const StatisticsExt<FLOAT>& s)
+{
+ s.write(out);
+ return out;
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_STATISTICS_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/StringUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "StringUtil.h"
+
+#include <cctype>
+#include <iomanip>
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+template<>
+bool from_string(const string& s, string& t)
+{
+ t = s;
+ return true;
+}
+
+string get_letter_coord(unsigned i)
+{
+ string result;
+ while (true)
+ {
+ result.insert(0, 1, char('a' + i % 26));
+ i /= 26;
+ if (i == 0)
+ break;
+ --i;
+ }
+ return result;
+}
+
+vector<string> split(const string& s, char separator)
+{
+ vector<string> result;
+ string current;
+ for (char c : s)
+ {
+ if (c == separator)
+ {
+ result.push_back(current);
+ current.clear();
+ continue;
+ }
+ current.push_back(c);
+ }
+ if (! current.empty() || ! result.empty())
+ result.push_back(current);
+ return result;
+}
+
+string time_to_string(double seconds, bool with_seconds_as_double)
+{
+ int int_seconds = int(seconds + 0.5);
+ int hours = int_seconds / 3600;
+ int_seconds -= hours * 3600;
+ int minutes = int_seconds / 60;
+ int_seconds -= minutes * 60;
+ ostringstream s;
+ s << setfill('0');
+ if (hours > 0)
+ s << hours << ':';
+ s << setw(2) << minutes << ':' << setw(2) << int_seconds;
+ if (with_seconds_as_double)
+ s << " (" << seconds << ')';
+ return s.str();
+}
+
+string to_lower(string s)
+{
+ for (auto& c : s)
+ c = static_cast<char>(tolower(c));
+ return s;
+}
+
+string trim(const string& s)
+{
+ string::size_type begin = 0;
+ auto end = s.size();
+ while (begin != end && isspace(s[begin]))
+ ++begin;
+ while (end > begin && isspace(s[end - 1]))
+ --end;
+ return s.substr(begin, end - begin);
+}
+
+string trim_right(const string& s)
+{
+ auto end = s.size();
+ while (end > 0 && isspace(s[end - 1]))
+ --end;
+ return s.substr(0, end);
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/StringUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_STRING_UTIL_H
+#define LIBBOARDGAME_UTIL_STRING_UTIL_H
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace libboardgame_util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+template<typename T>
+bool from_string(const string& s, T& t)
+{
+ istringstream in(s);
+ in >> t;
+ return ! in.fail();
+}
+
+template<>
+bool from_string(const string& s, string& t);
+
+/** Get a letter representing a coordinate.
+ Returns 'a' to 'z' for i between 0 and 25 and continues with 'aa','ab'...
+ for coordinates larger than 25. */
+string get_letter_coord(unsigned i);
+
+vector<string> split(const string& s, char separator);
+
+string time_to_string(double seconds, bool with_seconds_as_double = false);
+
+template<typename T>
+string to_string(const T& t)
+{
+ ostringstream buffer;
+ buffer << t;
+ return buffer.str();
+}
+
+string to_lower(string s);
+
+string trim(const string& s);
+
+string trim_right(const string& s);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_STRING_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file TimeIntervalChecker.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TimeIntervalChecker.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+TimeIntervalChecker::TimeIntervalChecker(TimeSource& time_source,
+ double time_interval,
+ double max_time)
+ : IntervalChecker(time_source, time_interval,
+ bind(&TimeIntervalChecker::check_time, this)),
+ m_max_time(max_time),
+ m_start_time(m_time_source())
+{
+}
+
+TimeIntervalChecker::TimeIntervalChecker(TimeSource& time_source,
+ double max_time)
+ : IntervalChecker(time_source, max_time > 1 ? 0.1 : 0.1 * max_time,
+ bind(&TimeIntervalChecker::check_time, this)),
+ m_max_time(max_time),
+ m_start_time(m_time_source())
+{
+}
+
+bool TimeIntervalChecker::check_time()
+{
+ return m_time_source() - m_start_time > m_max_time;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file TimeIntervalChecker.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H
+#define LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H
+
+#include "IntervalChecker.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+/** IntervalChecker that checks if a maximum total time was reached. */
+class TimeIntervalChecker
+ : public IntervalChecker
+{
+public:
+ TimeIntervalChecker(TimeSource& time_source, double time_interval,
+ double max_time);
+
+ /** Constructor with automatically set time_interval.
+ The time interval will be set to 0.1, if max_time > 1, otherwise
+ to 0.1 * max_time */
+ TimeIntervalChecker(TimeSource& time_source, double max_time);
+
+private:
+ double m_max_time;
+
+ double m_start_time;
+
+ bool check_time();
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_TIME_INTERVAL_CHECKER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/TimeSource.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TimeSource.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+TimeSource::~TimeSource()
+{
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/TimeSource.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_TIME_SOURCE_H
+#define LIBBOARDGAME_UTIL_TIME_SOURCE_H
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+/** Abstract time source for measuring thinking times for move generation.
+ Typical implementations are wall time, CPU time or mock time sources
+ for unit tests. They do not need to provide high resolutions (but should
+ support at least 100 ms) and should support maximum times of days (or even
+ months).
+ @ref libboardgame_doc_threadsafe_after_construction */
+class TimeSource
+{
+public:
+ virtual ~TimeSource();
+
+ /** Get the current time in seconds. */
+ virtual double operator()() = 0;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_TIME_SOURCE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Timer.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Timer.h"
+
+#include "Assert.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+Timer::Timer(TimeSource& time_source)
+ : m_start(time_source()),
+ m_time_source(&time_source)
+{ }
+
+double Timer::operator()() const
+{
+ LIBBOARDGAME_ASSERT(m_time_source);
+ return (*m_time_source)() - m_start;
+}
+
+void Timer::reset()
+{
+ m_start = (*m_time_source)();
+}
+
+void Timer::reset(TimeSource& time_source)
+{
+ m_time_source = &time_source;
+ reset();
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Timer.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_TIMER_H
+#define LIBBOARDGAME_UTIL_TIMER_H
+
+#include "TimeSource.h"
+
+namespace libboardgame_util {
+
+class Timer
+{
+public:
+ /** Constructor without time source.
+ If constructed without time source, the timer cannot be used before
+ reset(TimeSource&) was called. */
+ Timer() = default;
+
+ /** Constructor.
+ @param time_source (@ref libboardgame_doc_storesref) */
+ explicit Timer(TimeSource& time_source);
+
+ double operator()() const;
+
+ void reset();
+
+ void reset(TimeSource& time_source);
+
+private:
+ double m_start;
+
+ TimeSource* m_time_source = nullptr;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_TIMER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Unused.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_UNUSED_H
+#define LIBBOARDGAME_UTIL_UNUSED_H
+
+//-----------------------------------------------------------------------------
+
+template<class T> static void LIBBOARDGAME_UNUSED(const T&) { }
+
+#if LIBBOARDGAME_DEBUG
+#define LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(x)
+#else
+#define LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(x) LIBBOARDGAME_UNUSED(x)
+#endif
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBBOARDGAME_UTIL_UNUSED_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/WallTimeSource.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "WallTimeSource.h"
+
+#include <chrono>
+
+namespace libboardgame_util {
+
+using namespace std::chrono;
+
+//-----------------------------------------------------------------------------
+
+double WallTimeSource::operator()()
+{
+ auto t = system_clock::now().time_since_epoch();
+ return duration_cast<duration<double>>(t).count();
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/WallTimeSource.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H
+#define LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H
+
+#include "TimeSource.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+/** Wall time.
+ @ref libboardgame_doc_threadsafe_after_construction */
+class WallTimeSource
+ : public TimeSource
+{
+public:
+ double operator()() override;
+};
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+#endif // LIBBOARDGAME_UTIL_WALL_TIME_SOURCE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Board.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Board.h"
+
+#include <functional>
+#include "CallistoGeometry.h"
+#include "MoveMarker.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void write_x_coord(ostream& out, unsigned width, unsigned offset)
+{
+ for (unsigned i = 0; i < offset; ++i)
+ out << ' ';
+ char c = 'A';
+ for (unsigned x = 0; x < width; ++x, ++c)
+ {
+ if (x < 26)
+ out << ' ';
+ else
+ out << 'A';
+ if (x == 26)
+ c = 'A';
+ out << c;
+ }
+ out << '\n';
+}
+
+void set_color(ostream& out, const char* esc_sequence)
+{
+ if (Board::color_output)
+ out << esc_sequence;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+bool Board::color_output = false;
+
+Board::Board(Variant variant)
+{
+ m_color_char[Color(0)] = 'X';
+ m_color_char[Color(1)] = 'O';
+ m_color_char[Color(2)] = '#';
+ m_color_char[Color(3)] = '@';
+ for_each_color([&](Color c) {
+ m_state_color[c].forbidden[Point::null()] = false;
+ });
+ init_variant(variant);
+ init();
+#if LIBBOARDGAME_DEBUG
+ m_snapshot.moves_size =
+ numeric_limits<decltype(m_snapshot.moves_size)>::max();
+#endif
+}
+
+void Board::copy_from(const Board& bd)
+{
+ if (m_variant != bd.m_variant)
+ init_variant(bd.m_variant);
+ m_moves = bd.m_moves;
+ m_setup.to_play = bd.m_setup.to_play;
+ m_state_base = bd.m_state_base;
+ for (Color c : get_colors())
+ {
+ m_state_color[c] = bd.m_state_color[c];
+ m_setup.placements[c] = bd.m_setup.placements[c];
+ m_attach_points[c] = bd.m_attach_points[c];
+ }
+}
+
+const Transform* Board::find_transform(Move mv) const
+{
+ auto& geo = get_geometry();
+ PiecePoints points;
+ for (Point p : get_move_points(mv))
+ points.push_back(CoordPoint(geo.get_x(p), geo.get_y(p)));
+ return get_piece_info(get_move_piece(mv)).find_transform(geo, points);
+}
+
+void Board::gen_moves(Color c, MoveMarker& marker, MoveList& moves) const
+{
+ moves.clear();
+ bool is_callisto = (m_piece_set == PieceSet::callisto);
+ if (! is_callisto && is_first_piece(c))
+ {
+ for (Point p : get_starting_points(c))
+ if (! m_state_color[c].forbidden[p])
+ {
+ auto adj_status = get_adj_status(p, c);
+ for (Piece piece : m_state_color[c].pieces_left)
+ gen_moves(c, p, piece, adj_status, marker, moves);
+ }
+ return;
+ }
+ if (is_callisto && is_piece_left(c, m_one_piece))
+ for (auto p : *m_geo)
+ if (! is_forbidden(p, c) && ! m_is_center_section[p])
+ gen_moves(c, p, m_one_piece, get_adj_status(p, c), marker,
+ moves);
+ for (Point p : get_attach_points(c))
+ if (! m_state_color[c].forbidden[p])
+ {
+ auto adj_status = get_adj_status(p, c);
+ for (Piece piece : m_state_color[c].pieces_left)
+ if (! is_callisto || piece != m_one_piece)
+ gen_moves(c, p, piece, adj_status, marker, moves);
+ }
+}
+
+void Board::gen_moves(Color c, Point p, Piece piece, unsigned adj_status,
+ MoveMarker& marker, MoveList& moves) const
+{
+ for (Move mv : m_bc->get_moves(piece, p, adj_status))
+ if (! marker[mv] && ! is_forbidden(c, mv))
+ {
+ moves.push_back(mv);
+ marker.set(mv);
+ }
+}
+
+ScoreType Board::get_bonus(Color c) const
+{
+ if (get_pieces_left(c).size() > 0)
+ return 0;
+ auto bonus = m_bonus_all_pieces;
+ unsigned i = m_moves.size();
+ while (i > 0)
+ {
+ --i;
+ if (m_moves[i].color == c)
+ {
+ auto piece = get_move_piece(m_moves[i].move);
+ if (m_score_points[piece] == 1)
+ bonus += m_bonus_one_piece;
+ break;
+ }
+ }
+ return bonus;
+}
+
+Color Board::get_effective_to_play() const
+{
+ return get_effective_to_play(get_to_play());
+}
+
+Color Board::get_effective_to_play(Color c) const
+{
+ Color result = c;
+ do
+ {
+ if (has_moves(result))
+ return result;
+ result = get_next(result);
+ }
+ while (result != c);
+ return result;
+}
+
+void Board::get_place(Color c, unsigned& place, bool& is_shared) const
+{
+ bool break_ties = (m_piece_set == PieceSet::callisto);
+ array<ScoreType, Color::range> all_scores;
+ for (Color::IntType i = 0; i < Color::range; ++i)
+ {
+ all_scores[i] = get_score(Color(i));
+ if (break_ties)
+ all_scores[i] += i * 0.0001f;
+ }
+ auto score = all_scores[c.to_int()];
+ sort(all_scores.begin(), all_scores.begin() + m_nu_players,
+ greater<ScoreType>());
+ is_shared = false;
+ bool found = false;
+ for (unsigned i = 0; i < m_nu_players; ++i)
+ if (all_scores[i] == score)
+ {
+ if (! found)
+ {
+ place = i;
+ found = true;
+ }
+ else
+ is_shared = true;
+ }
+}
+
+Move Board::get_move_at(Point p) const
+{
+ auto s = get_point_state(p);
+ if (s.is_color())
+ {
+ auto c = s.to_color();
+ for (Move mv : m_setup.placements[c])
+ if (get_move_points(mv).contains(p))
+ return mv;
+ for (ColorMove color_mv : m_moves)
+ if (color_mv.color == c)
+ {
+ Move mv = color_mv.move;
+ if (get_move_points(mv).contains(p))
+ return mv;
+ }
+ }
+ return Move::null();
+}
+
+bool Board::has_moves(Color c) const
+{
+ bool is_callisto = (m_piece_set == PieceSet::callisto);
+ if (is_callisto && is_piece_left(c, m_one_piece))
+ for (auto p : *m_geo)
+ if (! is_forbidden(p, c) && ! m_is_center_section[p])
+ return true;
+ if (! is_callisto && is_first_piece(c))
+ {
+ for (auto p : get_starting_points(c))
+ if (has_moves(c, p))
+ return true;
+ return false;
+ }
+ for (auto p : get_attach_points(c))
+ if (has_moves(c, p))
+ return true;
+ return false;
+}
+
+bool Board::has_moves(Color c, Point p) const
+{
+ if (is_forbidden(p, c))
+ return false;
+ bool is_callisto = (m_piece_set == PieceSet::callisto);
+ if (is_callisto && is_piece_left(c, m_one_piece))
+ if (m_is_center_section[p])
+ return true;
+ auto adj_status = get_adj_status(p, c);
+ for (auto piece : m_state_color[c].pieces_left)
+ {
+ if (piece == m_one_piece && is_callisto)
+ continue;
+ for (auto mv : m_bc->get_moves(piece, p, adj_status))
+ if (! is_forbidden(c, mv))
+ return true;
+ }
+ return false;
+}
+
+bool Board::has_setup() const
+{
+ for (Color c : get_colors())
+ if (! m_setup.placements[c].empty())
+ return true;
+ return false;
+}
+
+void Board::init(Variant variant, const Setup* setup)
+{
+ if (variant != m_variant)
+ init_variant(variant);
+
+ // If you make changes here, make sure that you also update copy_from()
+
+ m_state_base.point_state.fill(PointState::empty(), *m_geo);
+ for (Color c : get_colors())
+ {
+ auto& state = m_state_color[c];
+ state.forbidden.fill(false, *m_geo);
+ state.is_attach_point.fill(false, *m_geo);
+ state.pieces_left.clear();
+ state.nu_onboard_pieces = 0;
+ state.points = 0;
+ for (Piece::IntType i = 0; i < get_nu_uniq_pieces(); ++i)
+ {
+ Piece piece(i);
+ state.pieces_left.push_back(piece);
+ state.nu_left_piece[piece] =
+ static_cast<uint_fast8_t>(get_nu_piece_instances(piece));
+ }
+ m_attach_points[c].clear();
+ }
+ m_state_base.nu_onboard_pieces_all = 0;
+ if (! setup)
+ {
+ m_setup.clear();
+ m_state_base.to_play = Color(0);
+ }
+ else
+ {
+ m_setup = *setup;
+ place_setup(m_setup);
+ m_state_base.to_play = setup->to_play;
+ optimize_attach_point_lists();
+ for (Color c : get_colors())
+ if (m_state_color[c].pieces_left.empty())
+ m_state_color[c].points += m_bonus_all_pieces;
+ }
+ m_moves.clear();
+}
+
+void Board::init_variant(Variant variant)
+{
+ m_variant = variant;
+ m_nu_colors = libpentobi_base::get_nu_colors(variant);
+ if (m_nu_colors == 2)
+ {
+ m_color_name[Color(0)] = "Blue";
+ m_color_name[Color(1)] = "Green";
+ m_color_esc_sequence[Color(0)] = "\x1B[1;34;47m";
+ m_color_esc_sequence[Color(1)] = "\x1B[1;32;47m";
+ m_color_esc_sequence_text[Color(0)] = "\x1B[1;34m";
+ m_color_esc_sequence_text[Color(1)] = "\x1B[1;32m";
+ }
+ else
+ {
+ m_color_name[Color(0)] = "Blue";
+ m_color_name[Color(1)] = "Yellow";
+ m_color_name[Color(2)] = "Red";
+ m_color_name[Color(3)] = "Green";
+ m_color_esc_sequence[Color(0)] = "\x1B[1;34;47m";
+ m_color_esc_sequence[Color(1)] = "\x1B[1;33;47m";
+ m_color_esc_sequence[Color(2)] = "\x1B[1;31;47m";
+ m_color_esc_sequence[Color(3)] = "\x1B[1;32;47m";
+ m_color_esc_sequence_text[Color(0)] = "\x1B[1;34m";
+ m_color_esc_sequence_text[Color(1)] = "\x1B[1;33m";
+ m_color_esc_sequence_text[Color(2)] = "\x1B[1;31m";
+ m_color_esc_sequence_text[Color(3)] = "\x1B[1;32m";
+ }
+ m_nu_players = libpentobi_base::get_nu_players(variant);
+ m_bc = &BoardConst::get(variant);
+ m_piece_set = m_bc->get_piece_set();
+ m_is_callisto = (m_piece_set == PieceSet::callisto);
+ if ((m_piece_set == PieceSet::classic && variant != Variant::junior)
+ || m_piece_set == PieceSet::trigon)
+ {
+ m_bonus_all_pieces = 15;
+ m_bonus_one_piece = 5;
+ }
+ else if (m_piece_set == PieceSet::nexos)
+ {
+ m_bonus_all_pieces = 10;
+ m_bonus_one_piece = 0;
+ }
+ else
+ {
+ m_bonus_all_pieces = 0;
+ m_bonus_one_piece = 0;
+ }
+ m_max_piece_size = m_bc->get_max_piece_size();
+ m_max_adj_attach = m_bc->get_max_adj_attach();
+ m_geo = &m_bc->get_geometry();
+ m_move_info_array = m_bc->get_move_info_array();
+ m_move_info_ext_array = m_bc->get_move_info_ext_array();
+ m_move_info_ext_2_array = m_bc->get_move_info_ext_2_array();
+ m_starting_points.init(variant, *m_geo);
+ if (m_piece_set == PieceSet::callisto)
+ for (Point p : *m_geo)
+ m_is_center_section[p] =
+ CallistoGeometry::is_center_section(m_geo->get_x(p),
+ m_geo->get_y(p),
+ m_nu_players);
+ else
+ m_is_center_section.fill(false, *m_geo);
+ for (Color c : get_colors())
+ {
+ if (m_nu_players == 2 && m_nu_colors == 4)
+ m_second_color[c] = get_next(get_next(c));
+ else
+ m_second_color[c] = c;
+ }
+ for (Piece::IntType i = 0; i < get_nu_uniq_pieces(); ++i)
+ {
+ Piece piece(i);
+ auto& piece_info = get_piece_info(piece);
+ m_score_points[piece] = piece_info.get_score_points();
+ if (piece_info.get_points().size() == 1)
+ m_one_piece = piece;
+ }
+}
+
+bool Board::is_game_over() const
+{
+ for (Color c : get_colors())
+ if (has_moves(c))
+ return false;
+ return true;
+}
+
+bool Board::is_legal(Color c, Move mv) const
+{
+ auto piece = get_move_piece(mv);
+ if (! is_piece_left(c, piece))
+ return false;
+ auto points = get_move_points(mv);
+ auto i = points.begin();
+ auto end = points.end();
+ bool has_attach_point = false;
+ do
+ {
+ if (m_state_color[c].forbidden[*i])
+ return false;
+ if (is_attach_point(*i, c))
+ has_attach_point = true;
+ }
+ while (++i != end);
+ if (m_is_callisto)
+ {
+ if (m_state_color[c].nu_left_piece[m_one_piece] > 1
+ && piece != m_one_piece)
+ return false;
+ if (piece == m_one_piece)
+ return ! m_is_center_section[*points.begin()];
+ }
+ if (has_attach_point)
+ return true;
+ if (! is_first_piece(c))
+ return false;
+ i = points.begin();
+ do
+ if (is_colorless_starting_point(*i)
+ || (is_colored_starting_point(*i)
+ && get_starting_point_color(*i) == c))
+ return true;
+ while (++i != end);
+ return false;
+}
+
+/** Remove forbidden points from attach point lists.
+ The attach point lists do not guarantee that they contain only
+ non-forbidden attach points because that would be too expensive to
+ update incrementally but at certain times that are not performance
+ critical (e.g. before taking a snapshot), we can remove them. */
+void Board::optimize_attach_point_lists()
+{
+ PointList l;
+ for (Color c : get_colors())
+ {
+ l.clear();
+ for (Point p : m_attach_points[c])
+ if (! is_forbidden(p, c))
+ l.push_back(p);
+ m_attach_points[c] = l;
+ }
+}
+
+/** Place setup moves on board. */
+void Board::place_setup(const Setup& setup)
+{
+ if (m_max_piece_size == 5)
+ for (Color c : get_colors())
+ for (Move mv : setup.placements[c])
+ place<5, 16>(c, mv);
+ else if (m_max_piece_size == 6)
+ for (Color c : get_colors())
+ for (Move mv : setup.placements[c])
+ place<6, 22>(c, mv);
+ else
+ for (Color c : get_colors())
+ for (Move mv : setup.placements[c])
+ place<7, 12>(c, mv);
+}
+
+void Board::play(Color c, Move mv)
+{
+ if (m_max_piece_size == 5)
+ play<5, 16>(c, mv);
+ else if (m_max_piece_size == 6)
+ play<6, 22>(c, mv);
+ else
+ play<7, 12>(c, mv);
+}
+
+void Board::take_snapshot()
+{
+ optimize_attach_point_lists();
+ m_snapshot.moves_size = m_moves.size();
+ m_snapshot.state_base.to_play = m_state_base.to_play;
+ m_snapshot.state_base.nu_onboard_pieces_all =
+ m_state_base.nu_onboard_pieces_all;
+ m_snapshot.state_base.point_state.copy_from(m_state_base.point_state,
+ *m_geo);
+ for (Color c : get_colors())
+ {
+ m_snapshot.attach_points_size[c] = m_attach_points[c].size();
+ const auto& state = m_state_color[c];
+ auto& snapshot_state = m_snapshot.state_color[c];
+ snapshot_state.forbidden.copy_from(state.forbidden, *m_geo);
+ snapshot_state.is_attach_point.copy_from(state.is_attach_point,
+ *m_geo);
+ snapshot_state.pieces_left = state.pieces_left;
+ snapshot_state.nu_left_piece = state.nu_left_piece;
+ snapshot_state.nu_onboard_pieces = state.nu_onboard_pieces;
+ snapshot_state.points = state.points;
+ }
+}
+
+void Board::write(ostream& out, bool mark_last_move) const
+{
+ // Sort lists of left pieces by name
+ ColorMap<PiecesLeftList> pieces_left;
+ for (Color c : get_colors())
+ {
+ pieces_left[c] = m_state_color[c].pieces_left;
+ sort(pieces_left[c].begin(), pieces_left[c].end(),
+ [&](Piece p1, Piece p2)
+ {
+ return
+ get_piece_info(p1).get_name()
+ < get_piece_info(p2).get_name();
+ });
+ }
+
+ ColorMove last_mv = ColorMove::null();
+ if (mark_last_move)
+ {
+ unsigned n = get_nu_moves();
+ if (n > 0)
+ last_mv = get_move(n - 1);
+ }
+ unsigned width = m_geo->get_width();
+ unsigned height = m_geo->get_height();
+ bool is_info_location_right = (width <= 20);
+ bool is_trigon = (m_piece_set == PieceSet::trigon);
+ bool is_nexos = (m_piece_set == PieceSet::nexos);
+ bool is_callisto = (m_piece_set == PieceSet::callisto);
+ for (unsigned y = 0; y < height; ++y)
+ {
+ if (height - y < 10)
+ out << ' ';
+ out << (height - y) << ' ';
+ for (unsigned x = 0; x < width; ++x)
+ {
+ Point p = m_geo->get_point(x, y);
+ bool is_offboard = p.is_null();
+ auto point_type = m_geo->get_point_type(x, y);
+ if ((x > 0 || (is_trigon && x == 0 && m_geo->is_onboard(x + 1, y)))
+ && ! is_offboard)
+ {
+ // Print a space horizontally between fields on the board. On a
+ // Trigon board, a slash or backslash is used instead of the
+ // space to indicate the orientation of the triangles. A
+ // less-than/greater-than character is used instead of the
+ // space to mark the last piece played.
+ if (! last_mv.is_null()
+ && get_move_points(last_mv.move).contains(p)
+ && (x == 0 || ! m_geo->is_onboard(x - 1, y)
+ || get_point_state(m_geo->get_point(x - 1, y))
+ != last_mv.color))
+ {
+ set_color(out, "\x1B[1;37;47m");
+ out << '>';
+ last_mv = ColorMove::null();
+ }
+ else if (! last_mv.is_null()
+ && x > 0 && m_geo->is_onboard(x - 1, y)
+ && get_move_points(last_mv.move).contains(
+ m_geo->get_point(x - 1, y))
+ && get_point_state(p) != last_mv.color
+ && get_point_state(m_geo->get_point(x - 1, y))
+ == last_mv.color)
+ {
+ set_color(out, "\x1B[1;37;47m");
+ out << '<';
+ last_mv = ColorMove::null();
+ }
+ else if (is_trigon)
+ {
+ set_color(out, "\x1B[1;30;47m");
+ out << (point_type == 1 ? '\\' : '/');
+ }
+ else
+ {
+ set_color(out, "\x1B[1;30;47m");
+ out << ' ';
+ }
+ }
+ if (is_offboard)
+ {
+ if (is_trigon && x > 0 && m_geo->is_onboard(x - 1, y))
+ {
+ set_color(out, "\x1B[1;30;47m");
+ out << (point_type == 1 ? '\\' : '/');
+ }
+ else if (is_callisto && x == 0)
+ {
+ set_color(out, "\x1B[0m");
+ out << ' ';
+ }
+ else
+ {
+ set_color(out, is_nexos ? "\x1B[1;30;47m" : "\x1B[0m");
+ out << " ";
+ }
+ }
+ else
+ {
+ PointState s = get_point_state(p);
+ if (s.is_empty())
+ {
+ if (is_colored_starting_point(p) && ! is_nexos)
+ {
+ Color c = get_starting_point_color(p);
+ set_color(out, m_color_esc_sequence[c]);
+ out << '+';
+ }
+ else if (is_colorless_starting_point(p))
+ {
+ set_color(out, "\x1B[1;30;47m");
+ out << '+';
+ }
+ else
+ {
+ set_color(out, "\x1B[1;30;47m");
+ if (is_trigon)
+ out << ' ';
+ else if (is_nexos && point_type == 1)
+ out << '-';
+ else if (is_nexos && point_type == 2)
+ out << '|';
+ else if (is_nexos && point_type == 0)
+ out << '+';
+ else if (is_callisto && is_center_section(p))
+ out << ',';
+ else
+ out << '.';
+ }
+ }
+ else
+ {
+ Color color = s.to_color();
+ set_color(out, m_color_esc_sequence[color]);
+ if (is_nexos && m_geo->get_point_type(p) == 0)
+ out << '*'; // Uncrossable junction
+ else
+ out << m_color_char[color];
+ }
+ }
+ }
+ if (is_trigon)
+ {
+ if (m_geo->is_onboard(width - 1, y))
+ {
+ set_color(out, "\x1B[1;30;47m");
+ out << (m_geo->get_point_type(width - 1, y) != 1 ? '\\' : '/');
+ }
+ else
+ {
+ set_color(out, "\x1B[0m");
+ out << " ";
+ }
+ }
+ set_color(out, "\x1B[0m");
+ if (is_info_location_right)
+ write_info_line(out, y, pieces_left);
+ out << '\n';
+ }
+ write_x_coord(out, width, is_trigon ? 3 : 2);
+ if (! is_info_location_right)
+ for (Color c : get_colors())
+ {
+ write_color_info_line1(out, c);
+ out << " ";
+ write_color_info_line2(out, c, pieces_left[c]);
+ out << ' ';
+ write_color_info_line3(out, c, pieces_left[c]);
+ out << '\n';
+ }
+}
+
+void Board::write_color_info_line1(ostream& out, Color c) const
+{
+ set_color(out, m_color_esc_sequence_text[c]);
+ if (! is_game_over() && get_effective_to_play() == c)
+ out << '(' << (get_nu_moves() + 1) << ") ";
+ out << m_color_name[c] << "(" << m_color_char[c] << "): " << get_points(c);
+ if (! has_moves(c))
+ out << '!';
+ set_color(out, "\x1B[0m");
+}
+
+void Board::write_color_info_line2(ostream& out, Color c,
+ const PiecesLeftList& pieces_left) const
+{
+ if (m_variant == Variant::junior)
+ write_pieces_left(out, c, pieces_left, 0, 6);
+ else
+ write_pieces_left(out, c, pieces_left, 0, 10);
+}
+
+void Board::write_color_info_line3(ostream& out, Color c,
+ const PiecesLeftList& pieces_left) const
+{
+ if (m_variant == Variant::junior)
+ write_pieces_left(out, c, pieces_left, 6, get_nu_uniq_pieces());
+ else
+ write_pieces_left(out, c, pieces_left, 10, get_nu_uniq_pieces());
+}
+
+void Board::write_info_line(ostream& out, unsigned y,
+ const ColorMap<PiecesLeftList>& pieces_left) const
+{
+ if (y == 0)
+ {
+ out << " ";
+ write_color_info_line1(out, Color(0));
+ }
+ else if (y == 1)
+ {
+ out << " ";
+ write_color_info_line2(out, Color(0), pieces_left[Color(0)]);
+ }
+ else if (y == 2)
+ {
+ out << " ";
+ write_color_info_line3(out, Color(0), pieces_left[Color(0)]);
+ }
+ else if (y == 4)
+ {
+ out << " ";
+ write_color_info_line1(out, Color(1));
+ }
+ else if (y == 5)
+ {
+ out << " ";
+ write_color_info_line2(out, Color(1), pieces_left[Color(1)]);
+ }
+ else if (y == 6)
+ {
+ out << " ";
+ write_color_info_line3(out, Color(1), pieces_left[Color(1)]);
+ }
+ else if (y == 8 && m_nu_colors > 2)
+ {
+ out << " ";
+ write_color_info_line1(out, Color(2));
+ }
+ else if (y == 9 && m_nu_colors > 2)
+ {
+ out << " ";
+ write_color_info_line2(out, Color(2), pieces_left[Color(2)]);
+ }
+ else if (y == 10 && m_nu_colors > 2)
+ {
+ out << " ";
+ write_color_info_line3(out, Color(2), pieces_left[Color(2)]);
+ }
+ else if (y == 12 && m_nu_colors > 3)
+ {
+ out << " ";
+ write_color_info_line1(out, Color(3));
+ }
+ else if (y == 13 && m_nu_colors > 3)
+ {
+ out << " ";
+ write_color_info_line2(out, Color(3), pieces_left[Color(3)]);
+ }
+ else if (y == 14 && m_nu_colors > 3)
+ {
+ out << " ";
+ write_color_info_line3(out, Color(3), pieces_left[Color(3)]);
+ }
+}
+
+void Board::write_pieces_left(ostream& out, Color c,
+ const PiecesLeftList& pieces_left,
+ unsigned begin, unsigned end) const
+{
+ for (unsigned i = begin; i < end; ++i)
+ if (i < pieces_left.size())
+ {
+ if (i > begin)
+ out << ' ';
+ Piece piece = pieces_left[i];
+ auto& name = get_piece_info(piece).get_name();
+ unsigned nu_left = m_state_color[c].nu_left_piece[piece];
+ for (unsigned j = 0; j < nu_left; ++j)
+ {
+ if (j > 0)
+ out << ' ';
+ out << name;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Board.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_BOARD_H
+#define LIBPENTOBI_BASE_BOARD_H
+
+#include "BoardConst.h"
+#include "ColorMap.h"
+#include "ColorMove.h"
+#include "Variant.h"
+#include "Geometry.h"
+#include "Grid.h"
+#include "MoveList.h"
+#include "PointList.h"
+#include "PointState.h"
+#include "Setup.h"
+#include "StartingPoints.h"
+
+namespace libpentobi_base {
+
+class MoveMarker;
+
+//-----------------------------------------------------------------------------
+
+/** Blokus board.
+ @note @ref libboardgame_avoid_stack_allocation */
+class Board
+{
+public:
+ typedef Grid<PointState> PointStateGrid;
+
+ /** Maximum number of pieces per player in any game variant. */
+ static const unsigned max_pieces = Setup::max_pieces;
+
+ typedef ArrayList<Piece, Piece::max_pieces> PiecesLeftList;
+
+ static const unsigned max_player_moves = max_pieces;
+
+ /** Maximum number of moves in any game variant. */
+ static const unsigned max_game_moves = Color::range * max_player_moves;
+
+ /** Use ANSI escape sequences for colored text output in operator>> */
+ static bool color_output;
+
+ explicit Board(Variant variant);
+
+ /** Not implemented to avoid unintended copies.
+ Use copy_from() to copy a board state. */
+ Board(const Board&) = delete;
+
+ /** Not implemented to avoid unintended copies.
+ Use copy_from() to copy a board state. */
+ Board& operator=(const Board&) = delete;
+
+ Geometry::Iterator begin() const { return m_geo->begin(); }
+
+ Geometry::Iterator end() const { return m_geo->end(); }
+
+ Variant get_variant() const;
+
+ Color::IntType get_nu_colors() const;
+
+ Color::Range get_colors() const { return Color::Range(m_nu_colors); }
+
+ /** Number of colors that are not played alternately.
+ This is equal to get_nu_colors() apart from Variant::classic_3. */
+ Color::IntType get_nu_nonalt_colors() const;
+
+ unsigned get_nu_players() const;
+
+ Piece::IntType get_nu_uniq_pieces() const;
+
+ /** Number of instances of a unique piece per color. */
+ unsigned get_nu_piece_instances(Piece piece) const;
+
+ Color get_next(Color c) const;
+
+ Color get_previous(Color c) const;
+
+ const PieceTransforms& get_transforms() const;
+
+ /** Get the state of an on-board point. */
+ PointState get_point_state(Point p) const;
+
+ const PointStateGrid& get_point_state() const;
+
+ /** Get next color to play.
+ The next color to play is the next color of the color of the last move
+ played even if it has no more moves to play. */
+ Color get_to_play() const;
+
+ /** Get the player who plays the next move for the 4th color in
+ Variant::classic_3. */
+ Color::IntType get_alt_player() const;
+
+ /** Equivalent to get_effective_to_play(get_to_play()) */
+ Color get_effective_to_play() const;
+
+ /** Get next color to play that still has moves.
+ Colors are tried in their playing order starting with c. If no color
+ has moves left, c is returned. */
+ Color get_effective_to_play(Color c) const;
+
+ const PiecesLeftList& get_pieces_left(Color c) const;
+
+ bool is_piece_left(Color c, Piece piece) const;
+
+ /** Check if no piece of a color has been placed on the board yet.
+ This includes setup pieces and played moves. */
+ bool is_first_piece(Color c) const;
+
+ /** Get number of instances left of a piece.
+ This value can be greater 1 in game variants that use multiple instances
+ of a unique piece per player. */
+ unsigned get_nu_left_piece(Color c, Piece piece) const;
+
+ /** Get number of points of a color including the bonus. */
+ ScoreType get_points(Color c) const { return m_state_color[c].points; }
+
+ /** Get number of bonus points of a color. */
+ ScoreType get_bonus(Color c) const;
+
+ /** Is a point a potential attachment point for a color.
+ Does not check if the point is forbidden. */
+ bool is_attach_point(Point p, Color c) const;
+
+ /** Get potential attachment points for a color.
+ Does not check if the point is forbidden. */
+ const PointList& get_attach_points(Color c) const;
+
+ /** Initialize the current board for a given game variant.
+ @param variant The game variant
+ @param setup An optional setup position to initialize the board
+ with. */
+ void init(Variant variant, const Setup* setup = nullptr);
+
+ /** Clear the current board without changing the current game variant.
+ See init(Variant,const Setup*) */
+ void init(const Setup* setup = nullptr);
+
+ /** Copy the board state and move history from another board.
+ This is like an assignment operator but because boards are rarely
+ copied by value and copying is expensive, it is an explicit function to
+ avoid accidental copying. */
+ void copy_from(const Board& bd);
+
+ /** Play a move.
+ @pre ! mv.is_null()
+ @pre get_nu_moves() < max_game_moves */
+ void play(Color c, Move mv);
+
+ /** More efficient version of play() if maximum piece size of current
+ game variant is known at compile time. */
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void play(Color c, Move mv);
+
+ /** Play a move.
+ @pre ! mv.move.is_null()
+ @pre get_nu_moves() < max_game_moves */
+ void play(ColorMove mv);
+
+ void set_to_play(Color c);
+
+ void write(ostream& out, bool mark_last_move = true) const;
+
+ /** Get the setup of the board before any moves were played.
+ If the board was initialized without setup, the return value contains
+ a setup with empty placement lists and Color(0) as the color to
+ play. */
+ const Setup& get_setup() const;
+
+ bool has_setup() const;
+
+ /** Get the total number of moves played by all colors.
+ Does not include setup pieces.
+ @see get_nu_onboard_pieces() */
+ unsigned get_nu_moves() const;
+
+ /** Get the number of pieces on board.
+ This is the number of setup pieces, if the board was initialized
+ with a setup position, plus the number of pieces played as moves. */
+ unsigned get_nu_onboard_pieces() const;
+
+ /** Get the number of pieces on board of a color.
+ This is the number of setup pieces, if the board was initialized
+ with a setup position, plus the number of pieces played as moves. */
+ unsigned get_nu_onboard_pieces(Color c) const;
+
+ ColorMove get_move(unsigned n) const;
+
+ const ArrayList<ColorMove, max_game_moves>& get_moves() const;
+
+ /** Generate all legal moves for a color.
+ @param c The color
+ @param marker A move marker reused for efficiency (needs to be clear)
+ @param[out] moves The list of moves. */
+ void gen_moves(Color c, MoveMarker& marker, MoveList& moves) const;
+
+ bool has_moves(Color c) const;
+
+ /** Check that no color has any moves left. */
+ bool is_game_over() const;
+
+ /** Check if a move is legal.
+ @pre ! mv.is_null() */
+ bool is_legal(Color c, Move mv) const;
+
+ /** Check if a move is legal for the current color to play.
+ @pre ! mv.is_null() */
+ bool is_legal(Move mv) const;
+
+ /** Check that point is not already occupied or adjacent to own color.
+ Point::null() is an allowed argument and returns false. */
+ bool is_forbidden(Point p, Color c) const;
+
+ const GridExt<bool>& is_forbidden(Color c) const;
+
+ /** Check that no points of move are already occupied or adjacent to own
+ color.
+ Does not check if the move is diagonally adjacent to an existing
+ occupied point of the same color. */
+ bool is_forbidden(Color c, Move mv) const;
+
+ const BoardConst& get_board_const() const { return *m_bc; }
+
+ BoardType get_board_type() const;
+
+ PieceSet get_piece_set() const { return m_piece_set; }
+
+ unsigned get_adj_status(Point p, Color c) const;
+
+ /** Is a point in the center section that is forbidden for the 1-piece in
+ Callisto?
+ Always returns false for other game variants. */
+ bool is_center_section(Point p) const { return m_is_center_section[p]; }
+
+ PrecompMoves::Range get_moves(Piece piece, Point p,
+ unsigned adj_status) const;
+
+ /** Get score.
+ The score is the number of points for a color minus the number of
+ points of the opponent (or the average score of the opponents if there
+ are more than two players). */
+ ScoreType get_score(Color c) const;
+
+ /** Specialized version of get_score().
+ @pre get_nu_colors() == 2 */
+ ScoreType get_score_twocolor(Color c) const;
+
+ /** Specialized version of get_score().
+ @pre get_nu_players() == 4 && get_nu_colors() == 4 */
+ ScoreType get_score_multicolor(Color c) const;
+
+ /** Specialized version of get_score().
+ @pre get_nu_players() > 2 */
+ ScoreType get_score_multiplayer(Color c) const;
+
+ /** Specialized version of get_score().
+ @pre get_nu_players() == 2 */
+ ScoreType get_score_twoplayer(Color c) const;
+
+ /** Get the place of a player in the game result.
+ @param c The color of the player.
+ @param[out] place The place of the player with that color. The place
+ numbers start with 0. A place can be shared if several players have the
+ same score. If a place is shared by n players, the following n-1 places
+ are not used.
+ @param[out] is_shared True if the place was shared. */
+ void get_place(Color c, unsigned& place, bool& is_shared) const;
+
+ const Geometry& get_geometry() const { return *m_geo; }
+
+ /** See BoardConst::to_string() */
+ string to_string(Move mv, bool with_piece_name = false) const;
+
+ /** See BoardConst::from_string() */
+ Move from_string(const string& s) const;
+
+ bool find_move(const MovePoints& points, Move& mv) const;
+
+ bool find_move(const MovePoints& points, Piece piece, Move& mv) const;
+
+ const Transform* find_transform(Move mv) const;
+
+ const PieceInfo& get_piece_info(Piece piece) const;
+
+ bool get_piece_by_name(const string& name, Piece& piece) const;
+
+ /** The 1x1 piece. */
+ Piece get_one_piece() const { return m_one_piece; }
+
+ Range<const Point> get_move_points(Move mv) const;
+
+ Piece get_move_piece(Move mv) const;
+
+ const MoveInfoExt2& get_move_info_ext_2(Move mv) const;
+
+ bool is_colored_starting_point(Point p) const;
+
+ bool is_colorless_starting_point(Point p) const;
+
+ Color get_starting_point_color(Point p) const;
+
+ const ArrayList<Point,StartingPoints::max_starting_points>&
+ get_starting_points(Color c) const;
+
+ /** Get the second color in game variants in which a player plays two
+ colors.
+ @return The second color of the player that plays color c, or c if
+ the player plays only one color in the current game variant or
+ if the game variant is classic_3. */
+ Color get_second_color(Color c) const;
+
+ bool is_same_player(Color c1, Color c2) const;
+
+ Move get_move_at(Point p) const;
+
+ /** Remember the board state to quickly restore it later.
+ A snapshot can only be restored from a position that was reached
+ after playing moves from the snapshot position. */
+ void take_snapshot();
+
+ /** See take_snapshot() */
+ void restore_snapshot();
+
+private:
+ /** Color-independent part of the board state. */
+ struct StateBase
+ {
+ Color to_play;
+
+ unsigned nu_onboard_pieces_all;
+
+ PointStateGrid point_state;
+ };
+
+ /** Color-dependent part of the board state. */
+ struct StateColor
+ {
+ GridExt<bool> forbidden;
+
+ Grid<bool> is_attach_point;
+
+ PiecesLeftList pieces_left;
+
+ PieceMap<uint_fast8_t> nu_left_piece;
+
+ unsigned nu_onboard_pieces;
+
+ ScoreType points;
+ };
+
+ /** Snapshot for fast restoration of a previous position. */
+ struct Snapshot
+ {
+ StateBase state_base;
+
+ ColorMap<StateColor> state_color;
+
+ unsigned moves_size;
+
+ ColorMap<unsigned> attach_points_size;
+ };
+
+
+ StateBase m_state_base;
+
+ ColorMap<StateColor> m_state_color;
+
+ Variant m_variant;
+
+ PieceSet m_piece_set;
+
+ Color::IntType m_nu_colors;
+
+ bool m_is_callisto;
+
+ unsigned m_nu_players;
+
+ /** Caches m_bc->get_max_piece_size(). */
+ unsigned m_max_piece_size;
+
+ /** Caches m_bc->get_max_adj_attach(). */
+ unsigned m_max_adj_attach;
+
+ /** Bonus for playing all pieces. */
+ ScoreType m_bonus_all_pieces;
+
+ /** Bonus for playing the 1-piece last. */
+ ScoreType m_bonus_one_piece;
+
+ /** Caches get_piece_info(piece).get_score_points() */
+ PieceMap<ScoreType> m_score_points;
+
+ const BoardConst* m_bc;
+
+ /** Caches m_bc->get_move_info_array() */
+ BoardConst::MoveInfoArray m_move_info_array;
+
+ /** Caches m_bc->get_move_info_ext_array() */
+ BoardConst::MoveInfoExtArray m_move_info_ext_array;
+
+ /** Caches m_bc->get_move_info_ext_2_array() */
+ const MoveInfoExt2* m_move_info_ext_2_array;
+
+ const Geometry* m_geo;
+
+ /** See is_center_section(). */
+ Grid<bool> m_is_center_section;
+
+ /** The 1x1 piece. */
+ Piece m_one_piece;
+
+ ColorMap<PointList> m_attach_points;
+
+ /** See get_second_color() */
+ ColorMap<Color> m_second_color;
+
+ ColorMap<char> m_color_char;
+
+ ColorMap<const char*> m_color_esc_sequence;
+
+ ColorMap<const char*> m_color_esc_sequence_text;
+
+ ColorMap<const char*> m_color_name;
+
+ ArrayList<ColorMove, max_game_moves> m_moves;
+
+ Snapshot m_snapshot;
+
+ Setup m_setup;
+
+ StartingPoints m_starting_points;
+
+
+ void gen_moves(Color c, Point p, Piece piece, unsigned adj_status,
+ MoveMarker& marker, MoveList& moves) const;
+
+ bool has_moves(Color c, Point p) const;
+
+ void init_variant(Variant variant);
+
+ void optimize_attach_point_lists();
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void place(Color c, Move mv);
+
+ void place_setup(const Setup& setup);
+
+ void write_pieces_left(ostream& out, Color c,
+ const PiecesLeftList& pieces_left, unsigned begin,
+ unsigned end) const;
+
+ void write_color_info_line1(ostream& out, Color c) const;
+
+ void write_color_info_line2(ostream& out, Color c,
+ const PiecesLeftList& pieces_left) const;
+
+ void write_color_info_line3(ostream& out, Color c,
+ const PiecesLeftList& pieces_left) const;
+
+ void write_info_line(ostream& out, unsigned y,
+ const ColorMap<PiecesLeftList>& pieces_left) const;
+};
+
+
+inline bool Board::find_move(const MovePoints& points, Move& mv) const
+{
+ return m_bc->find_move(points, mv);
+}
+
+inline bool Board::find_move(const MovePoints& points, Piece piece,
+ Move& mv) const
+{
+ return m_bc->find_move(points, piece, mv);
+}
+
+inline Move Board::from_string(const string& s) const
+{
+ return m_bc->from_string(s);
+}
+
+inline unsigned Board::get_adj_status(Point p, Color c) const
+{
+ auto i = m_bc->get_adj_status_list(p).begin();
+ unsigned result = is_forbidden(*i, c); // bool converted to integer is 1
+ for (unsigned j = 1; j < PrecompMoves::adj_status_nu_adj; ++j)
+ result |= (is_forbidden(*(++i), c) << j);
+ return result;
+}
+
+inline Color::IntType Board::get_alt_player() const
+{
+ LIBBOARDGAME_ASSERT(m_variant == Variant::classic_3);
+ return static_cast<Color::IntType>(get_nu_onboard_pieces(Color(3)) % 3);
+}
+
+inline const PointList& Board::get_attach_points(Color c) const
+{
+ return m_attach_points[c];
+}
+
+inline BoardType Board::get_board_type() const
+{
+ return m_bc->get_board_type();
+}
+
+inline ColorMove Board::get_move(unsigned n) const
+{
+ return m_moves[n];
+}
+
+inline const MoveInfoExt2& Board::get_move_info_ext_2(Move mv) const
+{
+ LIBBOARDGAME_ASSERT(! mv.is_null());
+ LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves());
+ return *(m_move_info_ext_2_array + mv.to_int());
+}
+
+inline Piece Board::get_move_piece(Move mv) const
+{
+ return m_bc->get_move_piece(mv);
+}
+
+inline Range<const Point> Board::get_move_points(Move mv) const
+{
+ return m_bc->get_move_points(mv);
+}
+
+inline auto Board::get_moves() const
+-> const ArrayList<ColorMove, max_game_moves>&
+{
+ return m_moves;
+}
+
+inline PrecompMoves::Range Board::get_moves(Piece piece, Point p,
+ unsigned adj_status) const
+{
+ return m_bc->get_moves(piece, p, adj_status);
+}
+
+inline Color Board::get_next(Color c) const
+{
+ return c.get_next(m_nu_colors);
+}
+
+inline Color::IntType Board::get_nu_colors() const
+{
+ return m_nu_colors;
+}
+
+inline unsigned Board::get_nu_left_piece(Color c, Piece piece) const
+{
+ LIBBOARDGAME_ASSERT(piece.to_int() < get_nu_uniq_pieces());
+ return m_state_color[c].nu_left_piece[piece];
+}
+
+inline unsigned Board::get_nu_moves() const
+{
+ return m_moves.size();
+}
+
+inline Color::IntType Board::get_nu_nonalt_colors() const
+{
+ return m_variant != Variant::classic_3 ? m_nu_colors : 3;
+}
+
+inline unsigned Board::get_nu_onboard_pieces() const
+{
+ return m_state_base.nu_onboard_pieces_all;
+}
+
+inline unsigned Board::get_nu_onboard_pieces(Color c) const
+{
+ return m_state_color[c].nu_onboard_pieces;
+}
+
+inline unsigned Board::get_nu_players() const
+{
+ return m_nu_players;
+}
+
+inline unsigned Board::get_nu_piece_instances(Piece piece) const
+{
+ return m_bc->get_piece_info(piece).get_nu_instances();
+}
+
+inline Piece::IntType Board::get_nu_uniq_pieces() const
+{
+ return m_bc->get_nu_pieces();
+}
+
+inline const PieceInfo& Board::get_piece_info(Piece piece) const
+{
+ return m_bc->get_piece_info(piece);
+}
+
+inline bool Board::get_piece_by_name(const string& name, Piece& piece) const
+{
+ return m_bc->get_piece_by_name(name, piece);
+}
+
+inline const Board::PiecesLeftList& Board::get_pieces_left(Color c) const
+{
+ return m_state_color[c].pieces_left;
+}
+
+inline PointState Board::get_point_state(Point p) const
+{
+ return PointState(m_state_base.point_state[p].to_int());
+}
+
+inline const Board::PointStateGrid& Board::get_point_state() const
+{
+ return m_state_base.point_state;
+}
+
+inline Color Board::get_previous(Color c) const
+{
+ return c.get_previous(m_nu_colors);
+}
+
+inline ScoreType Board::get_score(Color c) const
+{
+ if (m_nu_colors == 2)
+ return get_score_twocolor(c);
+ else if (m_nu_players == 2)
+ return get_score_multicolor(c);
+ else
+ return get_score_multiplayer(c);
+}
+
+inline ScoreType Board::get_score_twocolor(Color c) const
+{
+ LIBBOARDGAME_ASSERT(m_nu_colors == 2);
+ auto points0 = get_points(Color(0));
+ auto points1 = get_points(Color(1));
+ if (c == Color(0))
+ return points0 - points1;
+ else
+ return points1 - points0;
+}
+
+inline ScoreType Board::get_score_twoplayer(Color c) const
+{
+ LIBBOARDGAME_ASSERT(m_nu_players == 2);
+ if (m_nu_colors == 2)
+ return get_score_twocolor(c);
+ else
+ return get_score_multicolor(c);
+}
+
+inline ScoreType Board::get_score_multicolor(Color c) const
+{
+ LIBBOARDGAME_ASSERT(m_nu_players == 2 && m_nu_colors == 4);
+ auto points0 = get_points(Color(0)) + get_points(Color(2));
+ auto points1 = get_points(Color(1)) + get_points(Color(3));
+ if (c == Color(0) || c == Color(2))
+ return points0 - points1;
+ else
+ return points1 - points0;
+}
+
+inline ScoreType Board::get_score_multiplayer(Color c) const
+{
+ LIBBOARDGAME_ASSERT(m_nu_players > 2);
+ ScoreType score = 0;
+ auto nu_players = static_cast<Color::IntType>(m_nu_players);
+ for (Color i : get_colors())
+ if (i != c)
+ score -= get_points(i);
+ score = get_points(c) + score / (static_cast<ScoreType>(nu_players) - 1);
+ return score;
+}
+
+inline Color Board::get_second_color(Color c) const
+{
+ return m_second_color[c];
+}
+
+inline const Setup& Board::get_setup() const
+{
+ return m_setup;
+}
+
+inline Color Board::get_starting_point_color(Point p) const
+{
+ return m_starting_points.get_starting_point_color(p);
+}
+
+inline const ArrayList<Point,StartingPoints::max_starting_points>&
+ Board::get_starting_points(Color c) const
+{
+ return m_starting_points.get_starting_points(c);
+}
+
+inline Color Board::get_to_play() const
+{
+ return m_state_base.to_play;
+}
+
+inline const PieceTransforms& Board::get_transforms() const
+{
+ return m_bc->get_transforms();
+}
+
+inline Variant Board::get_variant() const
+{
+ return m_variant;
+}
+
+inline void Board::init(const Setup* setup)
+{
+ init(m_variant, setup);
+}
+
+inline bool Board::is_attach_point(Point p, Color c) const
+{
+ return m_state_color[c].is_attach_point[p];
+}
+
+inline bool Board::is_colored_starting_point(Point p) const
+{
+ return m_starting_points.is_colored_starting_point(p);
+}
+
+inline bool Board::is_colorless_starting_point(Point p) const
+{
+ return m_starting_points.is_colorless_starting_point(p);
+}
+
+inline bool Board::is_first_piece(Color c) const
+{
+ return m_state_color[c].nu_onboard_pieces == 0;
+}
+
+inline bool Board::is_forbidden(Point p, Color c) const
+{
+ return m_state_color[c].forbidden[p];
+}
+
+inline const GridExt<bool>& Board::is_forbidden(Color c) const
+{
+ return m_state_color[c].forbidden;
+}
+
+inline bool Board::is_forbidden(Color c, Move mv) const
+{
+ auto points = get_move_points(mv);
+ auto i = points.begin();
+ auto end = points.end();
+ do
+ if (m_state_color[c].forbidden[*i])
+ return true;
+ while (++i != end);
+ return false;
+}
+
+inline bool Board::is_legal(Move mv) const
+{
+ return is_legal(m_state_base.to_play, mv);
+}
+
+inline bool Board::is_piece_left(Color c, Piece piece) const
+{
+ LIBBOARDGAME_ASSERT(piece.to_int() < get_nu_uniq_pieces());
+ return m_state_color[c].nu_left_piece[piece] > 0;
+}
+
+inline bool Board::is_same_player(Color c1, Color c2) const
+{
+ return c1 == c2 || c1 == m_second_color[c2];
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void Board::place(Color c, Move mv)
+{
+ LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE);
+ LIBBOARDGAME_ASSERT(m_max_adj_attach == MAX_ADJ_ATTACH);
+ auto& info = BoardConst::get_move_info<MAX_SIZE>(mv, m_move_info_array);
+ auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+ mv, m_move_info_ext_array);
+ auto piece = info.get_piece();
+ auto& state_color = m_state_color[c];
+ LIBBOARDGAME_ASSERT(state_color.nu_left_piece[piece] > 0);
+ auto score_points = m_score_points[piece];
+ if (--state_color.nu_left_piece[piece] == 0)
+ {
+ state_color.pieces_left.remove_fast(piece);
+ if (state_color.pieces_left.empty())
+ {
+ state_color.points += m_bonus_all_pieces;
+ if (MAX_SIZE == 7) // Nexos
+ LIBBOARDGAME_ASSERT(m_bonus_one_piece == 0);
+ else if (score_points == 1)
+ state_color.points += m_bonus_one_piece;
+ }
+ }
+ ++m_state_base.nu_onboard_pieces_all;
+ ++state_color.nu_onboard_pieces;
+ state_color.points += score_points;
+ auto i = info.begin();
+ auto end = info.end();
+ do
+ {
+ m_state_base.point_state[*i] = PointState(c);
+ for_each_color([&](Color c) {
+ m_state_color[c].forbidden[*i] = true;
+ });
+ }
+ while (++i != end);
+ if (MAX_SIZE == 7) // Nexos
+ {
+ LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0);
+ i = info_ext.begin_attach();
+ end = i + info_ext.size_attach_points;
+ }
+ else
+ {
+ end = info_ext.end_adj();
+ for (i = info_ext.begin_adj(); i != end; ++i)
+ state_color.forbidden[*i] = true;
+ LIBBOARDGAME_ASSERT(i == info_ext.begin_attach());
+ end += info_ext.size_attach_points;
+ }
+ auto& attach_points = m_attach_points[c];
+ auto n = attach_points.size();
+ do
+ if (! state_color.forbidden[*i] && ! state_color.is_attach_point[*i])
+ {
+ state_color.is_attach_point[*i] = true;
+ attach_points.get_unchecked(n) = *i;
+ ++n;
+ }
+ while (++i != end);
+ attach_points.resize(n);
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void Board::play(Color c, Move mv)
+{
+ place<MAX_SIZE, MAX_ADJ_ATTACH>(c, mv);
+ m_moves.push_back(ColorMove(c, mv));
+ m_state_base.to_play = get_next(c);
+}
+
+inline void Board::play(ColorMove mv)
+{
+ play(mv.color, mv.move);
+}
+
+inline void Board::restore_snapshot()
+{
+ LIBBOARDGAME_ASSERT(m_snapshot.moves_size <= m_moves.size());
+ auto& geo = get_geometry();
+ m_moves.resize(m_snapshot.moves_size);
+ m_state_base.to_play = m_snapshot.state_base.to_play;
+ m_state_base.nu_onboard_pieces_all =
+ m_snapshot.state_base.nu_onboard_pieces_all;
+ m_state_base.point_state.memcpy_from(m_snapshot.state_base.point_state,
+ geo);
+ for (Color c : get_colors())
+ {
+ const auto& snapshot_state = m_snapshot.state_color[c];
+ auto& state = m_state_color[c];
+ state.forbidden.copy_from(snapshot_state.forbidden, geo);
+ state.is_attach_point.copy_from(snapshot_state.is_attach_point, geo);
+ state.pieces_left = snapshot_state.pieces_left;
+ state.nu_left_piece = snapshot_state.nu_left_piece;
+ state.nu_onboard_pieces = snapshot_state.nu_onboard_pieces;
+ state.points = snapshot_state.points;
+ m_attach_points[c].resize(m_snapshot.attach_points_size[c]);
+ }
+}
+
+inline void Board::set_to_play(Color c)
+{
+ m_state_base.to_play = c;
+}
+
+inline string Board::to_string(Move mv, bool with_piece_name) const
+{
+ return m_bc->to_string(mv, with_piece_name);
+}
+
+//-----------------------------------------------------------------------------
+
+inline ostream& operator<<(ostream& out, const Board& bd)
+{
+ bd.write(out);
+ return out;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_BOARD_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardConst.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "BoardConst.h"
+
+#include <algorithm>
+#include "Marker.h"
+#include "PieceTransformsClassic.h"
+#include "PieceTransformsTrigon.h"
+#include "libboardgame_base/Transform.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::Transform;
+using libboardgame_util::split;
+using libboardgame_util::to_lower;
+using libboardgame_util::trim;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const bool log_move_creation = false;
+
+/** Local variable used during construction.
+ Making this variable global slightly speeds up construction and a
+ thread-safe construction is not needed. */
+Marker g_marker;
+
+/** Non-compact representation of lists of moves of a piece at a point
+ constrained by the forbidden status of adjacent points.
+ Only used during construction. See g_marker why this variable is global. */
+Grid<array<ArrayList<Move, 40>, PrecompMoves::nu_adj_status>>
+ g_full_move_table;
+
+
+bool is_reverse(MovePoints::const_iterator begin1, const Point* begin2, unsigned size)
+{
+ auto j = begin2 + size - 1;
+ for (auto i = begin1; i != begin1 + size; ++i, --j)
+ if (*i != *j)
+ return false;
+ return true;
+}
+
+// Sort points using the ordering used in blksgf files (switches the direction
+// of the y axis!)
+void sort_piece_points(PiecePoints& points)
+{
+ auto check = [&](unsigned short a, unsigned short b)
+ {
+ if ((points[a].y == points[b].y && points[a].x > points[b].x)
+ || points[a].y < points[b].y)
+ swap(points[a], points[b]);
+ };
+ // Minimal number of necessary comparisons with sorting networks
+ auto size = points.size();
+ switch (size)
+ {
+ case 7:
+ check(1, 2);
+ check(3, 4);
+ check(5, 6);
+ check(0, 2);
+ check(3, 5);
+ check(4, 6);
+ check(0, 1);
+ check(4, 5);
+ check(2, 6);
+ check(0, 4);
+ check(1, 5);
+ check(0, 3);
+ check(2, 5);
+ check(1, 3);
+ check(2, 4);
+ check(2, 3);
+ break;
+ case 6:
+ check(1, 2);
+ check(4, 5);
+ check(0, 2);
+ check(3, 5);
+ check(0, 1);
+ check(3, 4);
+ check(2, 5);
+ check(0, 3);
+ check(1, 4);
+ check(2, 4);
+ check(1, 3);
+ check(2, 3);
+ break;
+ case 5:
+ check(0, 1);
+ check(3, 4);
+ check(2, 4);
+ check(2, 3);
+ check(1, 4);
+ check(0, 3);
+ check(0, 2);
+ check(1, 3);
+ check(1, 2);
+ break;
+ case 4:
+ check(0, 1);
+ check(2, 3);
+ check(0, 2);
+ check(1, 3);
+ check(1, 2);
+ break;
+ case 3:
+ check(1, 2);
+ check(0, 2);
+ check(0, 1);
+ break;
+ case 2:
+ check(0, 1);
+ break;
+ default:
+ LIBBOARDGAME_ASSERT(size == 1);
+ }
+}
+
+vector<PieceInfo> create_pieces_callisto(const Geometry& geo,
+ PieceSet piece_set,
+ const PieceTransforms& transforms)
+{
+ vector<PieceInfo> pieces;
+ pieces.reserve(19);
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 3);
+ pieces.emplace_back("W",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1),
+ CoordPoint(0, 0), CoordPoint(0, 1),
+ CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("X",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1),
+ CoordPoint(0, 0), CoordPoint(0, 1),
+ CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("T5",
+ PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("U",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1),
+ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("L",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("Z",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("V",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("I",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_classic(const Geometry& geo,
+ PieceSet piece_set,
+ const PieceTransforms& transforms)
+{
+ vector<PieceInfo> pieces;
+ // Define the 21 standard pieces. The piece names are the standard names as
+ // in http://blokusstrategy.com/?p=48. The default orientation is chosen
+ // such that it resembles the letter.
+ pieces.reserve(21);
+ pieces.emplace_back("V5",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(0, -2), CoordPoint(1, 0),
+ CoordPoint(2, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("L5",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(1, 1),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(0, -2) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("Z5",
+ PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("N",
+ PiecePoints{ CoordPoint(-1, 1), CoordPoint(-1, 0),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(0, -2)},
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("W",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1),
+ CoordPoint(0, 0), CoordPoint(0, 1),
+ CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("X",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1),
+ CoordPoint(0, 0), CoordPoint(0, 1),
+ CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("F",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(1, -1),
+ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("I5",
+ PiecePoints{ CoordPoint(0, 2), CoordPoint(0, 1),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(0, -2) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("T5",
+ PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, 1),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("Y",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(0, 1),
+ CoordPoint(0, 2) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("P",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("U",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(-1, -1),
+ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("L4",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(0, 2) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("Z4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("V3",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_junior(const Geometry& geo,
+ PieceSet piece_set,
+ const PieceTransforms& transforms)
+{
+ vector<PieceInfo> pieces;
+ pieces.reserve(12);
+ pieces.emplace_back("L5",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(1, 1),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(0, -2) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("P",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("I5",
+ PiecePoints{ CoordPoint(0, 2), CoordPoint(0, 1),
+ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(0, -2) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(1, -1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("Z4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("L4",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(0, -2) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("V3",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0), 2);
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_trigon(const Geometry& geo,
+ PieceSet piece_set,
+ const PieceTransforms& transforms)
+{
+ vector<PieceInfo> pieces;
+ // Define the 22 standard Trigon pieces. The piece names are similar to one
+ // of the possible notations from the thread "Trigon book: how to play, how
+ // to win" from August 2010 in the Blokus forums
+ // http://forum.blokus.refreshed.be/viewtopic.php?f=2&t=2539#p9867
+ // apart from that the smallest pieces are named '2' and '1' like in
+ // Classic to avoid to many pieces with letter 'I' and that numbers are
+ // only used if there is more than one piece with the same letter.
+ pieces.reserve(22);
+ pieces.emplace_back("I6",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(-1, 1), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("L6",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("V",
+ PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, -1),
+ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("S",
+ PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(-1, 1), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("P6",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(-1, 1), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("F",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1),
+ CoordPoint(2, 1), CoordPoint(1, 2) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("W",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(2, 0), CoordPoint(3, 0) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("A6",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(0, 1), CoordPoint(2, 1) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("G",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1),
+ CoordPoint(1, 1), CoordPoint(2, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("Y",
+ PiecePoints{ CoordPoint(-1, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(-1, 1), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("X",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(-1, 1),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("I5",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(-1, 1),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("L5",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1),
+ CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("C5",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1),
+ CoordPoint(2, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("P5",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(-1, 1), CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("C4",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("A4",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, piece_set, CoordPoint(0, 0));
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_nexos(const Geometry& geo,
+ PieceSet piece_set,
+ const PieceTransforms& transforms)
+{
+ vector<PieceInfo> pieces;
+ pieces.reserve(24);
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2),
+ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(0, 2),
+ CoordPoint(0, 3) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("L4",
+ PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2),
+ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 2) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("Y",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 1), CoordPoint(0, 2),
+ CoordPoint(0, 3)},
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("N",
+ PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 1), CoordPoint(0, 2),
+ CoordPoint(0, 3)},
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("V4",
+ PiecePoints{ CoordPoint(-3, 0), CoordPoint(-2, 0),
+ CoordPoint(-1, 0), CoordPoint(0, -1),
+ CoordPoint(0, -2), CoordPoint(0, -3) },
+ geo, transforms, piece_set, CoordPoint(-1, 0));
+ pieces.emplace_back("W",
+ PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 2)},
+ geo, transforms, piece_set, CoordPoint(-1, 0));
+ pieces.emplace_back("Z4",
+ PiecePoints{ CoordPoint(-1, -2), CoordPoint(0, -1),
+ CoordPoint(0, 0), CoordPoint(0, 1),
+ CoordPoint(1, 2) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(0, 2),
+ CoordPoint(0, 3) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("E",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(-1, 2)},
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("U4",
+ PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(2, -1) },
+ geo, transforms, piece_set, CoordPoint(-1, 0));
+ pieces.emplace_back("X",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1)},
+ geo, transforms, piece_set, CoordPoint(0, -1));
+ pieces.emplace_back("F",
+ PiecePoints{ CoordPoint(1, -2), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(0, 1)},
+ geo, transforms, piece_set, CoordPoint(0, -1));
+ pieces.emplace_back("H",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(2, 1)},
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("J",
+ PiecePoints{ CoordPoint(0, -3), CoordPoint(0, -2),
+ CoordPoint(0, -1), CoordPoint(-1, 0),
+ CoordPoint(-2, -1) },
+ geo, transforms, piece_set, CoordPoint(-1, 0));
+ pieces.emplace_back("G",
+ PiecePoints{ CoordPoint(2, -1), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 2)},
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(1, 0), CoordPoint(2, 1),
+ CoordPoint(0, 1), CoordPoint(1, 2)},
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(0, 2),
+ CoordPoint(0, 3) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("L3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 2) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("T3",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("Z3",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 1),
+ CoordPoint(1, 2) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("U3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(2, -1) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ pieces.emplace_back("V2",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1) },
+ geo, transforms, piece_set, CoordPoint(-1, 0));
+ pieces.emplace_back("I2",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, piece_set, CoordPoint(0, 1));
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(1, 0) },
+ geo, transforms, piece_set, CoordPoint(1, 0));
+ return pieces;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+BoardConst::BoardConst(BoardType board_type, PieceSet piece_set)
+ : m_board_type(board_type),
+ m_piece_set(piece_set),
+ m_geo(libpentobi_base::get_geometry(board_type))
+{
+ switch (board_type)
+ {
+ case BoardType::classic:
+ m_nu_moves = Move::onboard_moves_classic + 1;
+ break;
+ case BoardType::trigon:
+ m_nu_moves = Move::onboard_moves_trigon + 1;
+ break;
+ case BoardType::trigon_3:
+ m_nu_moves = Move::onboard_moves_trigon_3 + 1;
+ break;
+ case BoardType::duo:
+ if (piece_set == PieceSet::classic)
+ m_nu_moves = Move::onboard_moves_duo + 1;
+ else
+ {
+ LIBBOARDGAME_ASSERT(piece_set == PieceSet::junior);
+ m_nu_moves = Move::onboard_moves_junior + 1;
+ }
+ break;
+ case BoardType::nexos:
+ m_nu_moves = Move::onboard_moves_nexos + 1;
+ break;
+ case BoardType::callisto:
+ m_nu_moves = Move::onboard_moves_callisto + 1;
+ break;
+ case BoardType::callisto_2:
+ m_nu_moves = Move::onboard_moves_callisto_2 + 1;
+ break;
+ case BoardType::callisto_3:
+ m_nu_moves = Move::onboard_moves_callisto_3 + 1;
+ break;
+ }
+ switch (piece_set)
+ {
+ case PieceSet::classic:
+ m_transforms.reset(new PieceTransformsClassic);
+ m_pieces = create_pieces_classic(m_geo, piece_set, *m_transforms);
+ m_max_piece_size = 5;
+ m_max_adj_attach = 16;
+ m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>)));
+ m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>)));
+ break;
+ case PieceSet::junior:
+ m_transforms.reset(new PieceTransformsClassic);
+ m_pieces = create_pieces_junior(m_geo, piece_set, *m_transforms);
+ m_max_piece_size = 5;
+ m_max_adj_attach = 16;
+ m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>)));
+ m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>)));
+ break;
+ case PieceSet::trigon:
+ m_transforms.reset(new PieceTransformsTrigon);
+ m_pieces = create_pieces_trigon(m_geo, piece_set, *m_transforms);
+ m_max_piece_size = 6;
+ m_max_adj_attach = 22;
+ m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<6>)));
+ m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<22>)));
+ break;
+ case PieceSet::nexos:
+ m_transforms.reset(new PieceTransformsClassic);
+ m_pieces = create_pieces_nexos(m_geo, piece_set, *m_transforms);
+ m_max_piece_size = 7;
+ m_max_adj_attach = 12;
+ m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<7>)));
+ m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<12>)));
+ break;
+ case PieceSet::callisto:
+ m_transforms.reset(new PieceTransformsClassic);
+ m_pieces = create_pieces_callisto(m_geo, piece_set, *m_transforms);
+ m_max_piece_size = 5;
+ // m_max_adj_attach is actually 10 in Callisto, but we care more about
+ // the performance in the classic Blokus variants and some code is
+ // faster if we don't have to handle different values for
+ // m_max_adj_attach for the same m_max_piece_size.
+ m_max_adj_attach = 16;
+ m_move_info.reset(calloc(m_nu_moves, sizeof(MoveInfo<5>)));
+ m_move_info_ext.reset(calloc(m_nu_moves, sizeof(MoveInfoExt<16>)));
+ break;
+ }
+ m_move_info_ext_2.reset(new MoveInfoExt2[m_nu_moves]);
+ m_nu_pieces = static_cast<Piece::IntType>(m_pieces.size());
+ init_adj_status();
+ auto width = m_geo.get_width();
+ auto height = m_geo.get_height();
+ for (Point p : m_geo)
+ m_compare_val[p] =
+ (height - m_geo.get_y(p) - 1) * width + m_geo.get_x(p);
+ create_moves();
+ switch (piece_set)
+ {
+ case PieceSet::classic:
+ LIBBOARDGAME_ASSERT(m_nu_pieces == 21);
+ break;
+ case PieceSet::junior:
+ LIBBOARDGAME_ASSERT(m_nu_pieces == 12);
+ break;
+ case PieceSet::trigon:
+ LIBBOARDGAME_ASSERT(m_nu_pieces == 22);
+ break;
+ case PieceSet::nexos:
+ LIBBOARDGAME_ASSERT(m_nu_pieces == 24);
+ break;
+ case PieceSet::callisto:
+ LIBBOARDGAME_ASSERT(m_nu_pieces == 12);
+ break;
+ }
+ if (board_type == BoardType::duo || board_type == BoardType::callisto_2)
+ init_symmetry_info<5>();
+ else if (board_type == BoardType::trigon)
+ init_symmetry_info<6>();
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void BoardConst::create_move(unsigned& moves_created, Piece piece,
+ const MovePoints& points, Point label_pos)
+{
+ LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE);
+ LIBBOARDGAME_ASSERT(m_max_adj_attach == MAX_ADJ_ATTACH);
+ LIBBOARDGAME_ASSERT(moves_created < m_nu_moves);
+ Move mv(static_cast<Move::IntType>(moves_created));
+ void* place =
+ static_cast<MoveInfo<MAX_SIZE>*>(m_move_info.get())
+ + moves_created;
+ new(place) MoveInfo<MAX_SIZE>(piece, points);
+ place =
+ static_cast<MoveInfoExt<MAX_ADJ_ATTACH>*>(m_move_info_ext.get())
+ + moves_created;
+ auto& info_ext = *new(place) MoveInfoExt<MAX_ADJ_ATTACH>();
+ auto& info_ext_2 = m_move_info_ext_2[moves_created];
+ ++moves_created;
+ auto scored_points = &info_ext_2.scored_points[0];
+ for (auto p : points)
+ if (m_board_type != BoardType::nexos || m_geo.get_point_type(p) != 0)
+ *(scored_points++) = p;
+ info_ext_2.scored_points_size = static_cast<uint_least8_t>(
+ scored_points - &info_ext_2.scored_points[0]);
+ auto begin = info_ext_2.begin_scored_points();
+ auto end = info_ext_2.end_scored_points();
+ g_marker.clear();
+ for (auto i = begin; i != end; ++i)
+ g_marker.set(*i);
+ for (auto i = begin; i != end; ++i)
+ {
+ auto j = m_adj_status_list[*i].begin();
+ unsigned adj_status = g_marker[*j];
+ for (unsigned k = 1; k < PrecompMoves::adj_status_nu_adj; ++k)
+ adj_status |= (g_marker[*(++j)] << k);
+ for (unsigned j = 0; j < PrecompMoves::nu_adj_status; ++j)
+ if ((j & adj_status) == 0)
+ g_full_move_table[*i][j].push_back(mv);
+ }
+ Point* p = info_ext.points;
+ for (auto i = begin; i != end; ++i)
+ for (Point j : m_geo.get_adj(*i))
+ if (! g_marker[j])
+ {
+ g_marker.set(j);
+ *(p++) = j;
+ }
+ info_ext.size_adj_points = static_cast<uint_least8_t>(p - info_ext.points);
+ for (auto i = begin; i != end; ++i)
+ for (Point j : m_geo.get_diag(*i))
+ if (! g_marker[j])
+ {
+ g_marker.set(j);
+ *(p++) = j;
+ }
+ info_ext.size_attach_points =
+ static_cast<uint_least8_t>(p - info_ext.end_adj());
+ info_ext_2.label_pos = label_pos;
+ info_ext_2.breaks_symmetry = false;
+ info_ext_2.symmetric_move = Move::null();
+ m_nu_attach_points[piece] =
+ max(m_nu_attach_points[piece],
+ static_cast<unsigned>(info_ext.size_attach_points));
+ if (log_move_creation)
+ {
+ Grid<char> grid;
+ grid.fill('.', m_geo);
+ for (auto i = begin; i != end; ++i)
+ grid[*i] = 'O';
+ for (auto i = info_ext.begin_adj(); i != info_ext.end_adj(); ++i)
+ grid[*i] = '+';
+ for (auto i = info_ext.begin_attach(); i != info_ext.end_attach(); ++i)
+ grid[*i] = '*';
+ LIBBOARDGAME_LOG("Move ", mv.to_int(), ":\n", grid.to_string(m_geo));
+ }
+}
+
+void BoardConst::create_moves()
+{
+ // Unused move infos for Move::null()
+ LIBBOARDGAME_ASSERT(Move::null().to_int() == 0);
+ unsigned moves_created = 1;
+
+ unsigned n = 0;
+ for (Piece::IntType i = 0; i < m_nu_pieces; ++i)
+ {
+ Piece piece(i);
+ if (m_max_piece_size == 5)
+ create_moves<5, 16>(moves_created, piece);
+ else if (m_max_piece_size == 6)
+ create_moves<6, 22>(moves_created, piece);
+ else
+ create_moves<7, 12>(moves_created, piece);
+ for (Point p : m_geo)
+ for (unsigned j = 0; j < PrecompMoves::nu_adj_status; ++j)
+ {
+ auto& list = g_full_move_table[p][j];
+ m_precomp_moves.set_list_range(p, j, piece, n,
+ list.size());
+ for (auto mv : list)
+ m_precomp_moves.set_move(n++, mv);
+ list.clear();
+ }
+ }
+ LIBBOARDGAME_ASSERT(moves_created == m_nu_moves);
+ if (log_move_creation)
+ LIBBOARDGAME_LOG("Created moves: ", moves_created, ", precomp: ", n);
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+void BoardConst::create_moves(unsigned& moves_created, Piece piece)
+{
+ auto& piece_info = m_pieces[piece.to_int()];
+ if (log_move_creation)
+ LIBBOARDGAME_LOG("Creating moves for piece ", piece_info.get_name());
+ auto& transforms = piece_info.get_transforms();
+ auto nu_transforms = transforms.size();
+ vector<PiecePoints> transformed_points(nu_transforms);
+ vector<CoordPoint> transformed_label_pos(nu_transforms);
+ for (size_t i = 0; i < nu_transforms; ++i)
+ {
+ auto transform = transforms[i];
+ transformed_points[i] = piece_info.get_points();
+ transform->transform(transformed_points[i].begin(),
+ transformed_points[i].end());
+ sort_piece_points(transformed_points[i]);
+ transformed_label_pos[i] =
+ transform->get_transformed(piece_info.get_label_pos());
+ }
+ auto piece_size =
+ static_cast<MovePoints::IntType>(piece_info.get_points().size());
+ MovePoints points;
+ for (MovePoints::IntType i = 0; i < MovePoints::max_size; ++i)
+ points.get_unchecked(i) = Point::null();
+ points.resize(piece_size);
+ // Make outer loop iterator over geometry for better memory locality
+ for (Point p : m_geo)
+ {
+ if (log_move_creation)
+ LIBBOARDGAME_LOG("Creating moves at ", m_geo.to_string(p));
+ auto x = m_geo.get_x(p);
+ auto y = m_geo.get_y(p);
+ auto point_type = m_geo.get_point_type(p);
+ for (size_t i = 0; i < nu_transforms; ++i)
+ {
+ if (log_move_creation)
+ {
+#if ! LIBBOARDGAME_DISABLE_LOG
+ auto& transform = *transforms[i];
+ LIBBOARDGAME_LOG("Transformation ", typeid(transform).name());
+#endif
+ }
+ if (transforms[i]->get_new_point_type() != point_type)
+ continue;
+ bool is_onboard = true;
+ for (MovePoints::IntType j = 0; j < piece_size; ++j)
+ {
+ auto& pp = transformed_points[i][j];
+ int xx = pp.x + x;
+ int yy = pp.y + y;
+ if (! m_geo.is_onboard(CoordPoint(xx, yy)))
+ {
+ is_onboard = false;
+ break;
+ }
+ points[j] = m_geo.get_point(xx, yy);
+ }
+ if (! is_onboard)
+ continue;
+ CoordPoint label_pos = transformed_label_pos[i];
+ label_pos.x += x;
+ label_pos.y += y;
+ create_move<MAX_SIZE, MAX_ADJ_ATTACH>(
+ moves_created, piece, points,
+ m_geo.get_point(label_pos.x, label_pos.y));
+ }
+ }
+}
+
+Move BoardConst::from_string(const string& s) const
+{
+ string trimmed = to_lower(trim(s));
+ if (trimmed == "null")
+ return Move::null();
+ vector<string> v = split(trimmed, ',');
+ if (v.size() > PieceInfo::max_size)
+ throw runtime_error("illegal move (too many points)");
+ bool is_nexos = (m_board_type == BoardType::nexos);
+ MovePoints points;
+ for (const auto& s : v)
+ {
+ Point p;
+ if (! m_geo.from_string(s, p))
+ throw runtime_error("illegal move (invalid point)");
+ if (is_nexos)
+ {
+ auto point_type = m_geo.get_point_type(p);
+ if (point_type != 1 && point_type != 2)
+ // Silently discard points that are not line segments, such
+ // files were written by some (unreleased) versions of Pentobi.
+ continue;
+ }
+ points.push_back(p);
+ }
+ Move mv;
+ if (! find_move(points, mv))
+ throw runtime_error("illegal move");
+ return mv;
+}
+
+const BoardConst& BoardConst::get(Variant variant)
+{
+ static map<BoardType, map<PieceSet, unique_ptr<BoardConst>>> board_const;
+ auto board_type = libpentobi_base::get_board_type(variant);
+ auto piece_set = libpentobi_base::get_piece_set(variant);
+ auto& bc = board_const[board_type][piece_set];
+ if (! bc)
+ bc.reset(new BoardConst(board_type, piece_set));
+ return *bc;
+}
+
+bool BoardConst::get_piece_by_name(const string& name, Piece& piece) const
+{
+ for (Piece::IntType i = 0; i < m_nu_pieces; ++i)
+ if (get_piece_info(Piece(i)).get_name() == name)
+ {
+ piece = Piece(i);
+ return true;
+ }
+ return false;
+}
+
+bool BoardConst::find_move(const MovePoints& points, Move& move) const
+{
+ MovePoints sorted_points = points;
+ sort(sorted_points);
+ for (Piece::IntType i = 0; i < m_pieces.size(); ++i)
+ {
+ Piece piece(i);
+ for (auto mv : get_moves(piece, points[0]))
+ {
+ auto& info_ext_2 = get_move_info_ext_2(mv);
+ if (sorted_points.size() == info_ext_2.scored_points_size
+ && equal(sorted_points.begin(), sorted_points.end(),
+ info_ext_2.begin_scored_points()))
+ {
+ move = mv;
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool BoardConst::find_move(const MovePoints& points, Piece piece,
+ Move& move) const
+{
+ MovePoints sorted_points = points;
+ sort(sorted_points);
+ for (auto mv : get_moves(piece, points[0]))
+ if (equal(sorted_points.begin(), sorted_points.end(),
+ get_move_points_begin(mv)))
+ {
+ move = mv;
+ return true;
+ }
+ return false;
+}
+
+void BoardConst::init_adj_status()
+{
+ for (Point p : m_geo)
+ {
+ auto& l = m_adj_status_list[p];
+ for (Point pp : m_geo.get_adj(p))
+ {
+ if (l.size() == PrecompMoves::adj_status_nu_adj)
+ break;
+ l.push_back(pp);
+ }
+ for (Point pp : m_geo.get_diag(p))
+ {
+ if (l.size() == PrecompMoves::adj_status_nu_adj)
+ break;
+ l.push_back(pp);
+ }
+ for (auto i = l.end(); i < l.begin() + PrecompMoves::adj_status_nu_adj;
+ ++i)
+ *i = Point::null();
+ }
+}
+
+template<unsigned MAX_SIZE>
+void BoardConst::init_symmetry_info()
+{
+ m_symmetric_points.init(m_geo);
+ for (Move::IntType i = 1; i < m_nu_moves; ++i)
+ {
+ Move mv(i);
+ auto& info = get_move_info<MAX_SIZE>(mv);
+ auto& info_ext_2 = m_move_info_ext_2[i];
+ info_ext_2.breaks_symmetry = false;
+ array<Point, PieceInfo::max_size> sym_points;
+ MovePoints::IntType n = 0;
+ for (Point p : info)
+ {
+ auto symm_p = m_symmetric_points[p];
+ auto end = info.end();
+ if (find(info.begin(), end, symm_p) != end)
+ info_ext_2.breaks_symmetry = true;
+ sym_points[n++] = symm_p;
+ }
+ for (auto mv : get_moves(info.get_piece(), sym_points[0]))
+ if (is_reverse(sym_points.begin(),
+ get_move_info<MAX_SIZE>(mv).begin(), n))
+ {
+ info_ext_2.symmetric_move = mv;
+ break;
+ }
+ }
+}
+
+inline void BoardConst::sort(MovePoints& points) const
+{
+ auto check = [&](unsigned short a, unsigned short b)
+ {
+ if (m_compare_val[points[a]] > m_compare_val[points[b]])
+ swap(points[a], points[b]);
+ };
+ // Minimal number of necessary comparisons with sorting networks
+ auto size = points.size();
+ switch (size)
+ {
+ case 7:
+ check(1, 2);
+ check(3, 4);
+ check(5, 6);
+ check(0, 2);
+ check(3, 5);
+ check(4, 6);
+ check(0, 1);
+ check(4, 5);
+ check(2, 6);
+ check(0, 4);
+ check(1, 5);
+ check(0, 3);
+ check(2, 5);
+ check(1, 3);
+ check(2, 4);
+ check(2, 3);
+ break;
+ case 6:
+ check(1, 2);
+ check(4, 5);
+ check(0, 2);
+ check(3, 5);
+ check(0, 1);
+ check(3, 4);
+ check(2, 5);
+ check(0, 3);
+ check(1, 4);
+ check(2, 4);
+ check(1, 3);
+ check(2, 3);
+ break;
+ case 5:
+ check(0, 1);
+ check(3, 4);
+ check(2, 4);
+ check(2, 3);
+ check(1, 4);
+ check(0, 3);
+ check(0, 2);
+ check(1, 3);
+ check(1, 2);
+ break;
+ case 4:
+ check(0, 1);
+ check(2, 3);
+ check(0, 2);
+ check(1, 3);
+ check(1, 2);
+ break;
+ case 3:
+ check(1, 2);
+ check(0, 2);
+ check(0, 1);
+ break;
+ case 2:
+ check(0, 1);
+ break;
+ default:
+ LIBBOARDGAME_ASSERT(size == 1);
+ }
+}
+
+string BoardConst::to_string(Move mv, bool with_piece_name) const
+{
+ if (mv.is_null())
+ return "null";
+ auto& info_ext_2 = get_move_info_ext_2(mv);
+ ostringstream s;
+ if (with_piece_name)
+ s << '[' << get_piece_info(get_move_piece(mv)).get_name() << "]";
+ bool is_first = true;
+ for (auto i = info_ext_2.begin_scored_points();
+ i != info_ext_2.end_scored_points(); ++i)
+ {
+ if (! is_first)
+ s << ',';
+ else
+ is_first = false;
+ s << m_geo.to_string(*i);
+ }
+ return s.str();
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardConst.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_BOARD_CONST_H
+#define LIBPENTOBI_BASE_BOARD_CONST_H
+
+#include "Geometry.h"
+#include "MoveInfo.h"
+#include "PieceInfo.h"
+#include "PieceTransforms.h"
+#include "PrecompMoves.h"
+#include "SymmetricPoints.h"
+#include "Variant.h"
+#include "libboardgame_util/ArrayList.h"
+#include "libboardgame_util/Range.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_util::ArrayList;
+using libboardgame_util::Range;
+
+//-----------------------------------------------------------------------------
+
+/** Constant precomputed data that is shared between all instances of Board
+ with a given board type and set of unique pieces per color. */
+class BoardConst
+{
+public:
+ /** See get_adj_status_list() */
+ typedef
+ ArrayList<Point, PrecompMoves::adj_status_nu_adj, unsigned short>
+ AdjStatusList;
+
+ /** Start of the MoveInfo array, which can be cached by the user in
+ performance-critical code and then passed into the static version of
+ get_move_info(). */
+ typedef const void* MoveInfoArray;
+
+ /** Start of the MoveInfoExt array, which can be cached by the user in
+ performance-critical code and then passed into the static version of
+ get_move_info_ext(). */
+ typedef const void* MoveInfoExtArray;
+
+ /** Get the single instance for a given board size.
+ The instance is created the first time this function is called.
+ This function is not thread-safe. */
+ static const BoardConst& get(Variant variant);
+
+ template<unsigned MAX_SIZE>
+ static const MoveInfo<MAX_SIZE>&
+ get_move_info(Move mv, MoveInfoArray move_info_array);
+
+ template<unsigned MAX_ADJ_ATTACH>
+ static const MoveInfoExt<MAX_ADJ_ATTACH>&
+ get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array);
+
+
+ Piece::IntType get_nu_pieces() const;
+
+ const PieceInfo& get_piece_info(Piece piece) const;
+
+ unsigned get_nu_attach_points(Piece piece) const;
+
+ bool get_piece_by_name(const string& name, Piece& piece) const;
+
+ const PieceTransforms& get_transforms() const;
+
+ unsigned get_max_piece_size() const { return m_max_piece_size; }
+
+ unsigned get_max_adj_attach() const { return m_max_adj_attach; }
+
+ Range<const Point> get_move_points(Move mv) const;
+
+ /** Return start of move points array.
+ For unrolling loops, there are guaranteed to be as many elements
+ as the maximum piece size in the current game variant. If the piece
+ is smaller, the remaining points are guaranteed to be Point::null(). */
+ const Point* get_move_points_begin(Move mv) const;
+
+ template<unsigned MAX_SIZE>
+ const Point* get_move_points_begin(Move mv) const;
+
+ Piece get_move_piece(Move mv) const;
+
+ template<unsigned MAX_SIZE>
+ Piece get_move_piece(Move mv) const;
+
+ MoveInfoArray get_move_info_array() const { return m_move_info.get(); }
+
+ /** Get pointer to extended move info array.
+ Can be used to speed up the access to the move info by avoiding the
+ multiple pointer dereferencing of Board::get_move_info_ext(Move) */
+ MoveInfoExtArray get_move_info_ext_array() const;
+
+ const MoveInfoExt2& get_move_info_ext_2(Move mv) const;
+
+ const MoveInfoExt2* get_move_info_ext_2_array() const;
+
+ unsigned get_nu_moves() const;
+
+ bool find_move(const MovePoints& points, Move& move) const;
+
+ bool find_move(const MovePoints& points, Piece piece, Move& move) const;
+
+ /** Get all moves of a piece at a point constrained by the forbidden
+ status of adjacent points. */
+ PrecompMoves::Range get_moves(Piece piece, Point p,
+ unsigned adj_status = 0) const
+ {
+ return m_precomp_moves.get_moves(piece, p, adj_status);
+ }
+
+ const PrecompMoves& get_precomp_moves() const { return m_precomp_moves; }
+
+ BoardType get_board_type() const { return m_board_type; };
+
+ PieceSet get_piece_set() const { return m_piece_set; }
+
+ const Geometry& get_geometry() const;
+
+ /** List containing the points used for the adjacent status.
+ Contains the first PrecompMoves::adj_status_nu_adj points of
+ Geometry::get_adj() concatenated with Geometry::get_diag().
+ Elements above end() may be accessed and contain Point::null()
+ for easy unrolling of loops. */
+ const AdjStatusList& get_adj_status_list(Point p) const
+ {
+ return m_adj_status_list[p];
+ }
+
+ /** Only initialized in game variants with central symmetry of board
+ including startign points. */
+ const SymmetricPoints& get_symmetrc_points() const
+ {
+ return m_symmetric_points;
+ }
+
+ /** Convert a move to its string representation.
+ The string representation is a comma-separated list of points (without
+ spaces between the commas or points). If with_piece_name is true,
+ it is prepended by the piece name in square brackets (also without any
+ spaces). The representation without the piece name is used by the SGF
+ files and GTP interface used by Pentobi (version >= 0.2). */
+ string to_string(Move mv, bool with_piece_name = false) const;
+
+ Move from_string(const string& s) const;
+
+ /** Sort move points using the ordering used in blksgf files. */
+ void sort(MovePoints& points) const;
+
+private:
+ struct MallocFree
+ {
+ void operator()(void* x) { free(x); }
+ };
+
+
+ Piece::IntType m_nu_pieces;
+
+ unsigned m_nu_moves;
+
+ unsigned m_max_piece_size;
+
+ /** See MoveInfoExt */
+ unsigned m_max_adj_attach;
+
+ BoardType m_board_type;
+
+ PieceSet m_piece_set;
+
+ const Geometry& m_geo;
+
+ vector<PieceInfo> m_pieces;
+
+ Grid<AdjStatusList> m_adj_status_list;
+
+ unique_ptr<PieceTransforms> m_transforms;
+
+ PieceMap<unsigned> m_nu_attach_points{0};
+
+ /** Array of MoveInfo<MAX_SIZE> with MAX_SIZE being the maximum piece size
+ in the corresponding game variant.
+ See comments at MoveInfo. */
+ unique_ptr<void, MallocFree> m_move_info;
+
+ /** Array of MoveInfoExt<MAX_ADJ_ATTACH> with MAX_ADJ_ATTACH being the
+ maximum total number of attach points and adjacent points of a piece in
+ the corresponding game variant.
+ See comments at MoveInfoExt. */
+ unique_ptr<void, MallocFree> m_move_info_ext;
+
+ unique_ptr<MoveInfoExt2[]> m_move_info_ext_2;
+
+ PrecompMoves m_precomp_moves;
+
+ /** Value for comparing points using the ordering used in blksgf files.
+ As specified in doc/blksgf/Pentobi-SGF.html, the order should be
+ (a1, b1, ..., a2, b2, ...) with y going upwards whereas the convention
+ for Point is that y goes downwards. */
+ Grid<unsigned> m_compare_val;
+
+ SymmetricPoints m_symmetric_points;
+
+
+ BoardConst(BoardType board_type, PieceSet piece_set);
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void create_move(unsigned& moves_created, Piece piece,
+ const MovePoints& points, Point label_pos);
+
+ void create_moves();
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void create_moves(unsigned& moves_created, Piece piece);
+
+ template<unsigned MAX_SIZE>
+ const MoveInfo<MAX_SIZE>& get_move_info(Move mv) const;
+
+ void init_adj_status();
+
+ template<unsigned MAX_SIZE>
+ void init_symmetry_info();
+};
+
+inline const Geometry& BoardConst::get_geometry() const
+{
+ return m_geo;
+}
+
+template<unsigned MAX_SIZE>
+inline const MoveInfo<MAX_SIZE>&
+BoardConst::get_move_info(Move mv, MoveInfoArray move_info_array)
+{
+ LIBBOARDGAME_ASSERT(! mv.is_null());
+ return *(static_cast<const MoveInfo<MAX_SIZE>*>(move_info_array)
+ + mv.to_int());
+}
+
+template<unsigned MAX_SIZE>
+inline const MoveInfo<MAX_SIZE>& BoardConst::get_move_info(Move mv) const
+{
+ LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE);
+ return get_move_info<MAX_SIZE>(mv, m_move_info.get());
+}
+
+template<unsigned MAX_ADJ_ATTACH>
+inline const MoveInfoExt<MAX_ADJ_ATTACH>&
+BoardConst::get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array)
+{
+ LIBBOARDGAME_ASSERT(! mv.is_null());
+ return *(static_cast<const MoveInfoExt<MAX_ADJ_ATTACH>*>(
+ move_info_ext_array) + mv.to_int());
+}
+
+inline const MoveInfoExt2& BoardConst::get_move_info_ext_2(Move mv) const
+{
+ LIBBOARDGAME_ASSERT(mv.to_int() < m_nu_moves);
+ return m_move_info_ext_2[mv.to_int()];
+}
+
+inline auto BoardConst::get_move_info_ext_array() const -> MoveInfoExtArray
+{
+ return m_move_info_ext.get();
+}
+
+inline const MoveInfoExt2* BoardConst::get_move_info_ext_2_array() const
+{
+ return m_move_info_ext_2.get();
+}
+
+template<unsigned MAX_SIZE>
+inline Piece BoardConst::get_move_piece(Move mv) const
+{
+ return get_move_info<MAX_SIZE>(mv).get_piece();
+}
+
+inline Piece BoardConst::get_move_piece(Move mv) const
+{
+ if (m_max_piece_size == 5)
+ return get_move_piece<5>(mv);
+ else if (m_max_piece_size == 6)
+ return get_move_piece<6>(mv);
+ else
+ {
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 7);
+ return get_move_piece<7>(mv);
+ }
+}
+
+inline Range<const Point> BoardConst::get_move_points(Move mv) const
+{
+ if (m_max_piece_size == 5)
+ {
+ auto& info = get_move_info<5>(mv);
+ return Range<const Point>(info.begin(), info.end());
+ }
+ else if (m_max_piece_size == 6)
+ {
+ auto& info = get_move_info<6>(mv);
+ return Range<const Point>(info.begin(), info.end());
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 7);
+ auto& info = get_move_info<7>(mv);
+ return Range<const Point>(info.begin(), info.end());
+ }
+}
+
+inline const Point* BoardConst::get_move_points_begin(Move mv) const
+{
+ if (m_max_piece_size == 5)
+ return get_move_points_begin<5>(mv);
+ else if (m_max_piece_size == 6)
+ return get_move_points_begin<6>(mv);
+ else
+ {
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 7);
+ return get_move_points_begin<7>(mv);
+ }
+}
+
+template<unsigned MAX_SIZE>
+inline const Point* BoardConst::get_move_points_begin(Move mv) const
+{
+ return get_move_info<MAX_SIZE>(mv).begin();
+}
+
+inline unsigned BoardConst::get_nu_moves() const
+{
+ return m_nu_moves;
+}
+
+inline unsigned BoardConst::get_nu_attach_points(Piece piece) const
+{
+ return m_nu_attach_points[piece];
+}
+
+inline Piece::IntType BoardConst::get_nu_pieces() const
+{
+ return m_nu_pieces;
+}
+
+inline const PieceInfo& BoardConst::get_piece_info(Piece piece) const
+{
+ LIBBOARDGAME_ASSERT(piece.to_int() < m_pieces.size());
+ return m_pieces[piece.to_int()];
+}
+
+inline const PieceTransforms& BoardConst::get_transforms() const
+{
+ return *m_transforms;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_BOARD_CONST_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardUpdater.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "BoardUpdater.h"
+
+#include <stdexcept>
+#include "BoardUtil.h"
+#include "NodeUtil.h"
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::InvalidTree;
+using libboardgame_sgf::util::get_path_from_root;
+using libpentobi_base::boardutil::get_current_position_as_setup;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** List to hold remaining pieces of a color with one entry for each instance
+ of the same piece. */
+typedef ArrayList<Piece, PieceInfo::max_instances * Piece::max_pieces>
+AllPiecesLeftList;
+
+/** Helper function used in init_setup. */
+void handle_setup_property(const SgfNode& node, const char* id, Color c,
+ const Board& bd, Setup& setup,
+ ColorMap<AllPiecesLeftList>& pieces_left)
+{
+ if (! node.has_property(id))
+ return;
+ vector<string> values = node.get_multi_property(id);
+ for (const string& s : values)
+ {
+ Move mv;
+ try
+ {
+ mv = bd.from_string(s);
+ }
+ catch (const runtime_error& e)
+ {
+ throw InvalidTree(e.what());
+ }
+ Piece piece = bd.get_move_piece(mv);
+ if (! pieces_left[c].remove(piece))
+ throw InvalidTree("piece played twice");
+ setup.placements[c].push_back(mv);
+ }
+}
+
+/** Helper function used in init_setup. */
+void handle_setup_empty(const SgfNode& node, const Board& bd, Setup& setup,
+ ColorMap<AllPiecesLeftList>& pieces_left)
+{
+ if (! node.has_property("AE"))
+ return;
+ vector<string> values = node.get_multi_property("AE");
+ for (const string& s : values)
+ {
+ Move mv;
+ try
+ {
+ mv = bd.from_string(s);
+ }
+ catch (const runtime_error& e)
+ {
+ throw InvalidTree(e.what());
+ }
+ for (Color c : bd.get_colors())
+ {
+ if (setup.placements[c].remove(mv))
+ {
+ Piece piece = bd.get_move_piece(mv);
+ LIBBOARDGAME_ASSERT(! pieces_left[c].contains(piece));
+ pieces_left[c].push_back(piece);
+ break;
+ }
+ throw InvalidTree("invalid value for AE property");
+ }
+ }
+}
+
+/** Initialize the board with a new setup position.
+ Class Board only supports setup positions before any moves are played. To
+ support setup properties in any node, we create a new setup position from
+ the current position and the setup properties from the node and initialize
+ the board with it. */
+void init_setup(Board& bd, const SgfNode& node)
+{
+ Setup setup;
+ get_current_position_as_setup(bd, setup);
+ ColorMap<AllPiecesLeftList> all_pieces_left;
+ for (Color c : bd.get_colors())
+ for (Piece piece : bd.get_pieces_left(c))
+ for (unsigned i = 0; i < bd.get_nu_piece_instances(piece); ++i)
+ all_pieces_left[c].push_back(piece);
+ handle_setup_property(node, "A1", Color(0), bd, setup, all_pieces_left);
+ handle_setup_property(node, "A2", Color(1), bd, setup, all_pieces_left);
+ handle_setup_property(node, "A3", Color(2), bd, setup, all_pieces_left);
+ handle_setup_property(node, "A4", Color(3), bd, setup, all_pieces_left);
+ // AB, AW are equivalent to A1, A2 but only used in games with two colors
+ handle_setup_property(node, "AB", Color(0), bd, setup, all_pieces_left);
+ handle_setup_property(node, "AW", Color(1), bd, setup, all_pieces_left);
+ handle_setup_empty(node, bd, setup, all_pieces_left);
+ Color to_play;
+ if (! libpentobi_base::node_util::get_player(node, setup.to_play))
+ {
+ // Try to guess who should be to play based on the setup pieces.
+ setup.to_play = Color(0);
+ for (Color c : bd.get_colors())
+ if (setup.placements[c].size() < setup.placements[Color(0)].size())
+ {
+ setup.to_play = c;
+ break;
+ }
+ }
+ bd.init(&setup);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+void BoardUpdater::update(Board& bd, const PentobiTree& tree,
+ const SgfNode& node)
+{
+ LIBBOARDGAME_ASSERT(tree.contains(node));
+ bd.init();
+ get_path_from_root(node, m_path);
+ for (const auto i : m_path)
+ {
+ if (libpentobi_base::node_util::has_setup(*i))
+ init_setup(bd, *i);
+ auto mv = tree.get_move(*i);
+ if (! mv.is_null())
+ {
+ if (! bd.is_piece_left(mv.color, bd.get_move_piece(mv.move)))
+ throw InvalidTree("piece played twice");
+ bd.play(mv);
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardUpdater.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_BOARD_UPDATER_H
+#define LIBPENTOBI_BASE_BOARD_UPDATER_H
+
+#include "Board.h"
+#include "PentobiTree.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Updates a board state to a node in a game tree. */
+class BoardUpdater
+{
+public:
+ /** Update the board to a node.
+ @throws Exception if tree contains invalid properties, moves that play
+ the same piece twice or other conditions that prevent the updater to
+ update the board to the given node. */
+ void update(Board& bd, const PentobiTree& tree, const SgfNode& node);
+
+private:
+ /** Local variable reused for efficiency. */
+ vector<const SgfNode*> m_path;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_BOARD_UPDATER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "BoardUtil.h"
+
+#include "PentobiSgfUtil.h"
+#if LIBBOARDGAME_DEBUG
+#include <sstream>
+#endif
+
+namespace libpentobi_base {
+namespace boardutil {
+
+using namespace std;
+using sgf_util::get_color_id;
+using sgf_util::get_setup_id;
+
+//-----------------------------------------------------------------------------
+
+#if LIBBOARDGAME_DEBUG
+string dump(const Board& bd)
+{
+ ostringstream s;
+ auto variant = bd.get_variant();
+ Writer writer(s);
+ writer.begin_tree();
+ writer.begin_node();
+ writer.write_property("GM", to_string(variant));
+ write_setup(writer, variant, bd.get_setup());
+ writer.end_node();
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ {
+ writer.begin_node();
+ auto mv = bd.get_move(i);
+ auto id = get_color_id(variant, mv.color);
+ if (! mv.is_null())
+ writer.write_property(id, bd.to_string(mv.move, false));
+ writer.end_node();
+ }
+ writer.end_tree();
+ return s.str();
+}
+#endif
+
+void get_current_position_as_setup(const Board& bd, Setup& setup)
+{
+ setup = bd.get_setup();
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ {
+ auto mv = bd.get_move(i);
+ setup.placements[mv.color].push_back(mv.move);
+ }
+ setup.to_play = bd.get_to_play();
+}
+
+Move get_transformed(const Board& bd, Move mv,
+ const PointTransform<Point>& transform)
+{
+ auto& geo = bd.get_geometry();
+ MovePoints points;
+ for (auto p : bd.get_move_points(mv))
+ points.push_back(transform.get_transformed(p, geo));
+ Move transformed_mv;
+ bd.find_move(points, bd.get_move_piece(mv), transformed_mv);
+ return transformed_mv;
+}
+
+void write_setup(Writer& writer, Variant variant, const Setup& setup)
+{
+ auto& board_const = BoardConst::get(variant);
+ for (Color c : get_colors(variant))
+ if (! setup.placements[c].empty())
+ {
+ vector<string> values;
+ for (Move mv : setup.placements[c])
+ values.push_back(board_const.to_string(mv, false));
+ writer.write_property(get_setup_id(variant, c), values);
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace boardutil
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/BoardUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_BOARDUTIL_H
+#define LIBPENTOBI_BASE_BOARDUTIL_H
+
+#include "Board.h"
+#include "libboardgame_sgf/Writer.h"
+
+namespace libpentobi_base {
+namespace boardutil {
+
+using libboardgame_sgf::Writer;
+
+//-----------------------------------------------------------------------------
+
+#if LIBBOARDGAME_DEBUG
+string dump(const Board& bd);
+#endif
+
+/** Return the current position as setup.
+ Merges all placements from Board::get_setup() and played moved into a
+ single setup and sets the setup color to play to the current color to
+ play. */
+void get_current_position_as_setup(const Board& bd, Setup& setup);
+
+void write_setup(Writer& writer, Variant variant, const Setup& setup);
+
+Move get_transformed(const Board& bd, Move mv,
+ const PointTransform<Point>& transform);
+
+//-----------------------------------------------------------------------------
+
+} // namespace boardutil
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_BOARDUTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Book.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Book.h"
+
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_util/Log.h"
+#include "libpentobi_base/BoardUtil.h"
+
+//-----------------------------------------------------------------------------
+
+namespace libpentobi_base {
+
+using libboardgame_base::PointTransfIdent;
+using libboardgame_base::PointTransfRefl;
+using libboardgame_base::PointTransfReflRot180;
+using libboardgame_base::PointTransfRot180;
+using libboardgame_base::PointTransfRot270Refl;
+using libboardgame_base::PointTransfTrigonReflRot60;
+using libboardgame_base::PointTransfTrigonReflRot120;
+using libboardgame_base::PointTransfTrigonReflRot240;
+using libboardgame_base::PointTransfTrigonReflRot300;
+using libboardgame_base::PointTransfTrigonRot60;
+using libboardgame_base::PointTransfTrigonRot120;
+using libboardgame_base::PointTransfTrigonRot240;
+using libboardgame_base::PointTransfTrigonRot300;
+using libboardgame_sgf::InvalidPropertyValue;
+using libboardgame_sgf::TreeReader;
+using boardutil::get_transformed;
+
+//-----------------------------------------------------------------------------
+
+Book::Book(Variant variant)
+ : m_tree(variant)
+{
+ get_transforms(variant, m_transforms, m_inv_transforms);
+}
+
+Book::~Book()
+{
+}
+
+Move Book::genmove(const Board& bd, Color c)
+{
+ if (bd.has_setup())
+ // Book cannot handle setup positions
+ return Move::null();
+ Move mv;
+ for (unsigned i = 0; i < m_transforms.size(); ++i)
+ if (genmove(bd, c, mv, *m_transforms[i], *m_inv_transforms[i]))
+ return mv;
+ return Move::null();
+}
+
+bool Book::genmove(const Board& bd, Color c, Move& mv,
+ const PointTransform& transform,
+ const PointTransform& inv_transform)
+{
+ LIBBOARDGAME_ASSERT(! bd.has_setup());
+ auto node = &m_tree.get_root();
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ {
+ ColorMove color_mv = bd.get_move(i);
+ color_mv.move = get_transformed(bd, color_mv.move, transform);
+ node = m_tree.find_child_with_move(*node, color_mv);
+ if (! node)
+ return false;
+ }
+ node = select_child(bd, c, m_tree, *node, inv_transform);
+ if (! node)
+ return false;
+ mv = get_transformed(bd, m_tree.get_move(*node).move, inv_transform);
+ return true;
+}
+
+void Book::load(istream& in)
+{
+ TreeReader reader;
+ try
+ {
+ reader.read(in);
+ }
+ catch (const TreeReader::ReadError& e)
+ {
+ throw runtime_error(string("could not read book: ") + e.what());
+ }
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ m_tree.init(root);
+ get_transforms(m_tree.get_variant(), m_transforms, m_inv_transforms);
+}
+
+const SgfNode* Book::select_child(const Board& bd, Color c,
+ const PentobiTree& tree, const SgfNode& node,
+ const PointTransform& inv_transform)
+{
+ unsigned nu_children = node.get_nu_children();
+ if (nu_children == 0)
+ return nullptr;
+ vector<const SgfNode*> good_moves;
+ for (unsigned i = 0; i < nu_children; ++i)
+ {
+ auto& child = node.get_child(i);
+ ColorMove color_mv = tree.get_move(child);
+ if (color_mv.is_null())
+ {
+ LIBBOARDGAME_LOG("WARNING: Book contains nodes without moves");
+ continue;
+ }
+ if (color_mv.color != c)
+ {
+ LIBBOARDGAME_LOG("WARNING: Book contains non-alternating move sequences");
+ continue;
+ }
+ auto mv = get_transformed(bd, color_mv.move, inv_transform);
+ if (! bd.is_legal(color_mv.color, mv))
+ {
+ LIBBOARDGAME_LOG("WARNING: Book contains illegal move");
+ continue;
+ }
+ if (m_tree.get_good_move(child) > 0)
+ {
+ LIBBOARDGAME_LOG(bd.to_string(mv), " !");
+ good_moves.push_back(&child);
+ }
+ else
+ LIBBOARDGAME_LOG(bd.to_string(mv));
+ }
+ if (good_moves.empty())
+ return nullptr;
+ LIBBOARDGAME_LOG("Book moves: ", good_moves.size());
+ unsigned nu_good_moves = static_cast<unsigned>(good_moves.size());
+ return good_moves[m_random.generate() % nu_good_moves];
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Book.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_BOOK_H
+#define LIBPENTOBI_BASE_BOOK_H
+
+#include <iosfwd>
+#include "Board.h"
+#include "PentobiTree.h"
+#include "libboardgame_base/PointTransform.h"
+#include "libboardgame_util/RandomGenerator.h"
+
+namespace libpentobi_base {
+
+using libboardgame_util::RandomGenerator;
+
+//-----------------------------------------------------------------------------
+
+/** Opening book.
+ Opening books are stored as trees in SGF files. Thay contain move
+ annotation properties according to the SGF standard. The book will select
+ randomly among the child nodes that have the move annotation good move
+ or very good move (TE[1] or TE[2]). */
+class Book
+{
+public:
+ explicit Book(Variant variant);
+
+ ~Book();
+
+ void load(istream& in);
+
+ Move genmove(const Board& bd, Color c);
+
+ const PentobiTree& get_tree() const;
+
+private:
+ typedef libboardgame_base::PointTransform<Point> PointTransform;
+
+ PentobiTree m_tree;
+
+ RandomGenerator m_random;
+
+ vector<unique_ptr<PointTransform>> m_transforms;
+
+ vector<unique_ptr<PointTransform>> m_inv_transforms;
+
+ bool genmove(const Board& bd, Color c, Move& mv,
+ const PointTransform& transform,
+ const PointTransform& inv_transform);
+
+ const SgfNode* select_child(const Board& bd, Color c,
+ const PentobiTree& tree, const SgfNode& node,
+ const PointTransform& inv_transform);
+};
+
+inline const PentobiTree& Book::get_tree() const
+{
+ return m_tree;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_BOOK_H
--- /dev/null
+set(pentobi_base_STAT_SRCS
+ BoardConst.h
+ BoardConst.cpp
+ Board.h
+ Board.cpp
+ BoardUpdater.h
+ BoardUpdater.cpp
+ BoardUtil.h
+ BoardUtil.cpp
+ Book.h
+ Book.cpp
+ CallistoGeometry.h
+ CallistoGeometry.cpp
+ Color.h
+ Color.cpp
+ ColorMap.h
+ ColorMove.h
+ Game.h
+ Game.cpp
+ Geometry.h
+ Grid.h
+ Marker.h
+ Move.h
+ MoveInfo.h
+ MoveList.h
+ MoveMarker.h
+ MovePoints.h
+ NexosGeometry.h
+ NexosGeometry.cpp
+ NodeUtil.h
+ NodeUtil.cpp
+ PentobiSgfUtil.h
+ PentobiSgfUtil.cpp
+ PentobiTree.h
+ PentobiTree.cpp
+ PentobiTreeWriter.h
+ PentobiTreeWriter.cpp
+ Piece.h
+ PieceInfo.h
+ PieceInfo.cpp
+ PieceMap.h
+ PieceTransformsClassic.h
+ PieceTransformsClassic.cpp
+ PieceTransformsClassic.h
+ PieceTransforms.cpp
+ PieceTransformsTrigon.h
+ PieceTransformsTrigon.cpp
+ PlayerBase.h
+ PlayerBase.cpp
+ Point.h
+ PointList.h
+ PointState.h
+ PointState.cpp
+ PrecompMoves.h
+ ScoreUtil.h
+ Setup.h
+ StartingPoints.h
+ StartingPoints.cpp
+ SymmetricPoints.h
+ SymmetricPoints.cpp
+ TreeUtil.h
+ TreeUtil.cpp
+ TrigonGeometry.h
+ TrigonGeometry.cpp
+ TrigonTransform.h
+ TrigonTransform.cpp
+ Variant.h
+ Variant.cpp
+)
+
+if (PENTOBI_BUILD_GTP)
+ set(pentobi_base_STAT_SRCS ${pentobi_base_STAT_SRCS}
+ Engine.cpp
+ Engine.h
+ )
+endif()
+
+add_library(pentobi_base STATIC ${pentobi_base_STAT_SRCS})
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/CallistoGeometry.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "CallistoGeometry.h"
+
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+unsigned get_size_callisto(unsigned nu_players)
+{
+ if (nu_players == 2)
+ return 16;
+ LIBBOARDGAME_ASSERT(nu_players == 3 || nu_players == 4);
+ return 20;
+}
+
+unsigned get_edge_callisto(unsigned nu_players)
+{
+ if (nu_players == 4)
+ return 6;
+ LIBBOARDGAME_ASSERT(nu_players == 2 || nu_players == 3);
+ return 2;
+}
+
+bool is_onboard_callisto(unsigned x, unsigned y, unsigned width,
+ unsigned height, unsigned edge)
+{
+ unsigned dy = min(y, height - y - 1);
+ unsigned min_x = (width - edge) / 2 > dy ? (width - edge) / 2 - dy : 0;
+ unsigned max_x = width - min_x - 1;
+ return x >= min_x && x <= max_x;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+map<unsigned, shared_ptr<CallistoGeometry>> CallistoGeometry::s_geometry;
+
+CallistoGeometry::CallistoGeometry(unsigned nu_players)
+{
+ unsigned sz = get_size_callisto(nu_players);
+ m_edge = get_edge_callisto(nu_players);
+ Geometry::init(sz, sz);
+}
+
+const CallistoGeometry& CallistoGeometry::get(unsigned nu_players)
+{
+ auto pos = s_geometry.find(nu_players);
+ if (pos != s_geometry.end())
+ return *pos->second;
+ shared_ptr<CallistoGeometry> geometry(new CallistoGeometry(nu_players));
+ return *s_geometry.insert(make_pair(nu_players, geometry)).first->second;
+}
+
+auto CallistoGeometry::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+ LIBBOARDGAME_UNUSED(x);
+ LIBBOARDGAME_UNUSED(y);
+ return AdjCoordList();
+}
+
+auto CallistoGeometry::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+ DiagCoordList l;
+ l.push_back(CoordPoint(x, y - 1));
+ l.push_back(CoordPoint(x - 1, y));
+ l.push_back(CoordPoint(x + 1, y));
+ l.push_back(CoordPoint(x, y + 1));
+ return l;
+}
+
+unsigned CallistoGeometry::get_period_x() const
+{
+ return 1;
+}
+
+unsigned CallistoGeometry::get_period_y() const
+{
+ return 1;
+}
+
+unsigned CallistoGeometry::get_point_type(int x, int y) const
+{
+ LIBBOARDGAME_UNUSED(x);
+ LIBBOARDGAME_UNUSED(y);
+ return 0;
+}
+
+bool CallistoGeometry::init_is_onboard(unsigned x, unsigned y) const
+{
+ return is_onboard_callisto(x, y, get_width(), get_height(), m_edge);
+}
+
+bool CallistoGeometry::is_center_section(unsigned x, unsigned y,
+ unsigned nu_players)
+{
+ auto size = get_size_callisto(nu_players);
+ if (x < size / 2 - 3 || y < size / 2 - 3)
+ return false;
+ x -= size / 2 - 3;
+ y -= size / 2 - 3;
+ if (x > 5 || y > 5)
+ return false;
+ return is_onboard_callisto(x, y, 6, 6, 2);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/CallistoGeometry.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H
+#define LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H
+
+#include <map>
+#include <memory>
+#include "Geometry.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Geometry for the board game Callisto.
+ To fit in with the assumptions of the Blokus engine, points are "diagonal"
+ to each other if they are actually adjacent on the real board and the
+ "adjacent" relationship is not used. */
+class CallistoGeometry final
+ : public Geometry
+{
+public:
+ /** Create or reuse an already created geometry.
+ @param nu_players The number of players (2, 3, or 4). */
+ static const CallistoGeometry& get(unsigned nu_players);
+
+ static bool is_center_section(unsigned x, unsigned y, unsigned nu_players);
+
+
+ AdjCoordList get_adj_coord(int x, int y) const override;
+
+ DiagCoordList get_diag_coord(int x, int y) const override;
+
+ unsigned get_point_type(int x, int y) const override;
+
+ unsigned get_period_x() const override;
+
+ unsigned get_period_y() const override;
+
+protected:
+ bool init_is_onboard(unsigned x, unsigned y) const override;
+
+private:
+ /** Stores already created geometries by number of players. */
+ static map<unsigned, shared_ptr<CallistoGeometry>> s_geometry;
+
+
+ unsigned m_edge;
+
+
+ explicit CallistoGeometry(unsigned nu_players);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Color.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Color.h"
+
+#include <sstream>
+#include "libboardgame_util/StringUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_util::to_lower;
+
+//-----------------------------------------------------------------------------
+
+Color::Color(const string& s)
+{
+ istringstream in(s);
+ in >> *this;
+ if (! in)
+ throw InvalidString("Invalid color string '" + s + "'");
+}
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, const Color& c)
+{
+ out << (c.to_int() + 1);
+ return out;
+}
+
+istream& operator>>(istream& in, Color& c)
+{
+ string s;
+ in >> s;
+ if (in)
+ {
+ s = to_lower(s);
+ if (s == "1" || s == "b" || s == "black")
+ {
+ c = Color(0);
+ return in;
+ }
+ else if (s == "2" || s == "w" || s == "white")
+ {
+ c = Color(1);
+ return in;
+ }
+ else if (s == "3")
+ {
+ c = Color(2);
+ return in;
+ }
+ else if (s == "4")
+ {
+ c = Color(3);
+ return in;
+ }
+ }
+ in.setstate(ios::failbit);
+ return in;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Color.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_COLOR_H
+#define LIBPENTOBI_BASE_COLOR_H
+
+#include <cstdint>
+#include <iosfwd>
+#include <stdexcept>
+#include <string>
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class Color
+{
+public:
+ typedef uint_fast8_t IntType;
+
+ class InvalidString
+ : public runtime_error
+ {
+ using runtime_error::runtime_error;
+ };
+
+ class Iterator
+ {
+ public:
+ explicit Iterator(IntType i)
+ {
+ m_i = i;
+ }
+
+ bool operator==(Iterator it) const
+ {
+ return m_i == it.m_i;
+ }
+
+ bool operator!=(Iterator it) const
+ {
+ return m_i != it.m_i;
+ }
+
+ void operator++()
+ {
+ ++m_i;
+ }
+
+ Color operator*() const
+ {
+ return Color(m_i);
+ }
+
+ private:
+ IntType m_i;
+ };
+
+ class Range
+ {
+ public:
+ explicit Range(IntType nu_colors)
+ : m_nu_colors(nu_colors)
+ { }
+
+ Iterator begin() const { return Iterator(0); }
+
+ Iterator end() const { return Iterator(m_nu_colors); }
+
+ private:
+ IntType m_nu_colors;
+ };
+
+ static const IntType range = 4;
+
+ Color();
+
+ explicit Color(IntType i);
+
+ explicit Color(const string& s);
+
+ bool operator==(const Color& c) const;
+
+ bool operator!=(const Color& c) const;
+
+ bool operator<(const Color& c) const;
+
+ IntType to_int() const;
+
+ Color get_next(IntType nu_colors) const;
+
+ Color get_previous(IntType nu_colors) const;
+
+private:
+ static const IntType value_uninitialized = range;
+
+ IntType m_i;
+
+ bool is_initialized() const;
+};
+
+
+inline Color::Color()
+{
+#if LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+inline Color::Color(IntType i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = i;
+}
+
+inline bool Color::operator==(const Color& c) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(c.is_initialized());
+ return m_i == c.m_i;
+}
+
+inline bool Color::operator!=(const Color& c) const
+{
+ return ! operator==(c);
+}
+
+inline bool Color::operator<(const Color& c) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(c.is_initialized());
+ return m_i < c.m_i;
+}
+
+inline Color Color::get_next(IntType nu_colors) const
+{
+ return Color(static_cast<IntType>(m_i + 1) % nu_colors);
+}
+
+inline Color Color::get_previous(IntType nu_colors) const
+{
+ return Color(static_cast<IntType>(m_i + nu_colors - 1) % nu_colors);
+}
+
+inline bool Color::is_initialized() const
+{
+ return m_i < value_uninitialized;
+}
+
+inline Color::IntType Color::to_int() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i;
+}
+
+//-----------------------------------------------------------------------------
+
+/** Output string representation of color.
+ The strings "1", "2", ... are used for the colors. */
+ostream& operator<<(ostream& out, const Color& c);
+
+/** Read color from input stream.
+ Accepts the strings "1", "2", ..., as well as "b", "w" or "black", "white"
+ for the first two colors. */
+istream& operator>>(istream& in, Color& c);
+
+//-----------------------------------------------------------------------------
+
+/** Unrolled loop over all colors. */
+template<class FUNCTION>
+inline void for_each_color(FUNCTION f)
+{
+ static_assert(Color::range == 4, "");
+ f(Color(0));
+ f(Color(1));
+ f(Color(2));
+ f(Color(3));
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_COLOR_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/ColorMap.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_COLOR_MAP_H
+#define LIBPENTOBI_BASE_COLOR_MAP_H
+
+#include <array>
+#include "Color.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Container mapping a color to another element type.
+ The elements must be default-constructible. This requirement is due to the
+ fact that elements are stored in an array for efficient access by color
+ index and arrays need default-constructible elements. */
+template<typename T>
+class ColorMap
+{
+public:
+ ColorMap() = default;
+
+ explicit ColorMap(const T& val);
+
+ T& operator[](Color c);
+
+ const T& operator[](Color c) const;
+
+ void fill(const T& val);
+
+private:
+ array<T, Color::range> m_a;
+};
+
+template<typename T>
+inline ColorMap<T>::ColorMap(const T& val)
+{
+ fill(val);
+}
+
+template<typename T>
+inline T& ColorMap<T>::operator[](Color c)
+{
+ return m_a[c.to_int()];
+}
+
+template<typename T>
+inline const T& ColorMap<T>::operator[](Color c) const
+{
+ return m_a[c.to_int()];
+}
+
+template<typename T>
+void ColorMap<T>::fill(const T& val)
+{
+ m_a.fill(val);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_COLOR_MAP_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/ColorMove.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_COLOR_MOVE_H
+#define LIBPENTOBI_BASE_COLOR_MOVE_H
+
+#include "Move.h"
+#include "libpentobi_base/Color.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+struct ColorMove
+{
+ Color color;
+
+ Move move;
+
+ /** Return a color move with a null move and an undefined color.
+ Even if the color is logically not defined, it is still initialized
+ (with Color(0)), such that this color move can be used in
+ comparisons. If you are sure that the color is never used and don't
+ want to initialize it for efficiency, use the default constructor
+ and then assign only the move. */
+ static ColorMove null();
+
+ ColorMove() = default;
+
+ ColorMove(Color c, Move mv);
+
+ /** Equality operator.
+ @pre move, color, mv.move, mv.color are initialized. */
+ bool operator==(const ColorMove& mv) const;
+
+ /** Inequality operator.
+ @pre move, color, mv.move, mv.color are initialized. */
+ bool operator!=(const ColorMove& mv) const;
+
+ bool is_null() const;
+};
+
+inline ColorMove::ColorMove(Color c, Move mv)
+ : color(c),
+ move(mv)
+{
+}
+
+inline bool ColorMove::operator==(const ColorMove& mv) const
+{
+ return move == mv.move && color == mv.color;
+}
+
+inline bool ColorMove::operator!=(const ColorMove& mv) const
+{
+ return ! operator==(mv);
+}
+
+inline bool ColorMove::is_null() const
+{
+ return move.is_null();
+}
+
+inline ColorMove ColorMove::null()
+{
+ return ColorMove(Color(0), Move::null());
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_BASE_COLOR_MOVE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Engine.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Engine.h"
+
+#include <fstream>
+#include "MoveMarker.h"
+#include "PentobiTreeWriter.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/RandomGenerator.h"
+
+namespace libpentobi_base {
+
+using libboardgame_gtp::Failure;
+using libboardgame_sgf::InvalidPropertyValue;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::util::get_last_node;
+using libboardgame_util::ArrayList;
+using libboardgame_util::RandomGenerator;
+
+//-----------------------------------------------------------------------------
+
+Engine::Engine(Variant variant)
+ : m_game(variant)
+{
+ add("all_legal", &Engine::cmd_all_legal);
+ add("clear_board", &Engine::cmd_clear_board);
+ add("final_score", &Engine::cmd_final_score);
+ add("get_place", &Engine::cmd_get_place);
+ add("loadsgf", &Engine::cmd_loadsgf);
+ add("point_integers", &Engine::cmd_point_integers);
+ add("move_info", &Engine::cmd_move_info);
+ add("p", &Engine::cmd_p);
+ add("param_base", &Engine::cmd_param_base);
+ add("play", &Engine::cmd_play);
+ add("savesgf", &Engine::cmd_savesgf);
+ add("set_game", &Engine::cmd_set_game);
+ add("showboard", &Engine::cmd_showboard);
+ add("undo", &Engine::cmd_undo);
+}
+
+void Engine::board_changed()
+{
+ if (m_show_board)
+ LIBBOARDGAME_LOG(get_board());
+}
+
+void Engine::cmd_all_legal(const Arguments& args, Response& response)
+{
+ auto& bd = get_board();
+ unique_ptr<MoveList> moves(new MoveList);
+ unique_ptr<MoveMarker> marker(new MoveMarker);
+ bd.gen_moves(get_color_arg(args), *marker, *moves);
+ for (Move mv : *moves)
+ response << bd.to_string(mv, false) << '\n';
+}
+
+void Engine::cmd_clear_board()
+{
+ m_game.init();
+ board_changed();
+}
+
+void Engine::cmd_final_score(Response& response)
+{
+ auto& bd = get_board();
+ if (get_nu_players(bd.get_variant()) > 2)
+ {
+ for (Color c : bd.get_colors())
+ response << bd.get_points(c) << ' ';
+ }
+ else
+ {
+ auto score = bd.get_score_twoplayer(Color(0));
+ if (score > 0)
+ response << "B+" << score;
+ else if (score < 0)
+ response << "W+" << (-score);
+ else
+ response << "0";
+ }
+}
+
+void Engine::cmd_g(Response& response)
+{
+ genmove(get_board().get_effective_to_play(), response);
+}
+
+void Engine::cmd_genmove(const Arguments& args, Response& response)
+{
+ genmove(get_color_arg(args), response);
+}
+
+void Engine::cmd_get_place(const Arguments& args, Response& response)
+{
+ auto& bd = get_board();
+ unsigned place;
+ bool isPlaceShared;
+ bd.get_place(get_color_arg(args), place, isPlaceShared);
+ response << place;
+ if (isPlaceShared)
+ response << " shared";
+}
+
+void Engine::cmd_loadsgf(const Arguments& args)
+{
+ args.check_size_less_equal(2);
+ string file = args.get(0);
+ int move_number = -1;
+ if (args.get_size() == 2)
+ move_number = args.parse_min<int>(1, 1) - 1;
+ try
+ {
+ TreeReader reader;
+ reader.read(file);
+ auto tree = reader.get_tree_transfer_ownership();
+ m_game.init(tree);
+ const SgfNode* node = nullptr;
+ if (move_number != -1)
+ node = m_game.get_tree().get_node_before_move_number(move_number);
+ if (! node)
+ node = &get_last_node(m_game.get_root());
+ m_game.goto_node(*node);
+ board_changed();
+ }
+ catch (const runtime_error& e)
+ {
+ throw Failure(e.what());
+ }
+}
+
+/** Return move info of a move given by its integer or string representation. */
+void Engine::cmd_move_info(const Arguments& args, Response& response)
+{
+ auto& bd = get_board();
+ Move mv;
+ try
+ {
+ mv = Move(args.parse<Move::IntType>());
+ }
+ catch (const Failure&)
+ {
+ try
+ {
+ mv = bd.from_string(args.get());
+ }
+ catch (const runtime_error&)
+ {
+ ostringstream msg;
+ msg << "invalid argument '" << args.get()
+ << "' (expected move or move ID)";
+ throw Failure(msg.str());
+ }
+ }
+ auto& geo = bd.get_geometry();
+ Piece piece = bd.get_move_piece(mv);
+ auto& info_ext_2 = bd.get_move_info_ext_2(mv);
+ response
+ << "\n"
+ << "ID: " << mv.to_int() << "\n"
+ << "Piece: " << static_cast<int>(piece.to_int())
+ << " (" << bd.get_piece_info(piece).get_name() << ")\n"
+ << "Points:";
+ for (Point p : bd.get_move_points(mv))
+ response << ' ' << geo.to_string(p);
+ response
+ << "\n"
+ << "BrkSym: " << info_ext_2.breaks_symmetry << "\n"
+ << "SymMv: " << bd.to_string(info_ext_2.symmetric_move);
+}
+
+void Engine::cmd_p(const Arguments& args)
+{
+ play(get_board().get_to_play(), args, 0);
+}
+
+void Engine::cmd_param_base(const Arguments& args, Response& response)
+{
+ if (args.get_size() == 0)
+ response
+ << "accept_illegal " << m_accept_illegal << '\n'
+ << "resign " << m_resign << '\n';
+ else
+ {
+ args.check_size(2);
+ string name = args.get(0);
+ if (name == "accept_illegal")
+ m_accept_illegal = args.parse<bool>(1);
+ else if (name == "resign")
+ m_resign = args.parse<bool>(1);
+ else
+ {
+ ostringstream msg;
+ msg << "unknown parameter '" << name << "'";
+ throw Failure(msg.str());
+ }
+ }
+}
+
+void Engine::cmd_play(const Arguments& args)
+{
+ play(get_color_arg(args, 0), args, 1);
+}
+
+void Engine::cmd_point_integers(Response& response)
+{
+ auto& geo = get_board().get_geometry();
+ Grid<int> grid;
+ for (Point p : geo)
+ grid[p] = p.to_int();
+ response << '\n' << grid.to_string(geo);
+}
+
+void Engine::cmd_reg_genmove(const Arguments& args, Response& response)
+{
+ RandomGenerator::set_global_seed_last();
+ Move move = get_player().genmove(get_board(), get_color_arg(args));
+ if (move.is_null())
+ throw Failure("player failed to generate a move");
+ response << get_board().to_string(move, false);
+}
+
+void Engine::cmd_savesgf(const Arguments& args)
+{
+ ofstream out(args.get());
+ PentobiTreeWriter writer(out, m_game.get_tree());
+ writer.set_indent(1);
+ writer.write();
+ if (! out)
+ throw Failure(strerror(errno));
+}
+
+/** Set the game variant.
+ Argument: game variant as in GM property of Pentobi SGF files
+ <br>
+ This command is similar to the command that is used by Quarry
+ (http://home.gna.org/quarry/) to set a game at GTP engines that support
+ multiple games. */
+void Engine::cmd_set_game(const Arguments& args)
+{
+ Variant variant;
+ if (! parse_variant(args.get_line(), variant))
+ throw Failure("invalid argument");
+ m_game.init(variant);
+ board_changed();
+}
+
+void Engine::cmd_showboard(Response& response)
+{
+ response << '\n' << get_board();
+}
+
+void Engine::cmd_undo()
+{
+ auto& bd = get_board();
+ if (bd.get_nu_moves() == 0)
+ throw Failure("cannot undo");
+ m_game.undo();
+ board_changed();
+}
+
+void Engine::genmove(Color c, Response& response)
+{
+ auto& bd = get_board();
+ auto& player = get_player();
+ auto mv = player.genmove(bd, c);
+ if (mv.is_null())
+ {
+ response << "pass";
+ return;
+ }
+ if (! bd.is_legal(c, mv))
+ {
+ ostringstream msg;
+ msg << "player generated illegal move: " << bd.to_string(mv);
+ throw Failure(msg.str());
+ }
+ if (m_resign && player.resign())
+ {
+ response << "resign";
+ return;
+ }
+ m_game.play(c, mv, true);
+ response << bd.to_string(mv, false);
+ board_changed();
+}
+
+Color Engine::get_color_arg(const Arguments& args) const
+{
+ if (args.get_size() > 1)
+ throw Failure("too many arguments");
+ return get_color_arg(args, 0);
+}
+
+Color Engine::get_color_arg(const Arguments& args, unsigned i) const
+{
+ string s = args.get_tolower(i);
+ auto& bd = get_board();
+ auto variant = bd.get_variant();
+ if (get_nu_colors(variant) == 2)
+ {
+ if (s == "blue" || s == "black" || s == "b")
+ return Color(0);
+ if (s == "green" || s == "white" || s == "w")
+ return Color(1);
+ }
+ else
+ {
+ if (s == "1" || s == "blue")
+ return Color(0);
+ if (s == "2" || s == "yellow")
+ return Color(1);
+ if (s == "3" || s == "red")
+ return Color(2);
+ if (s == "4" || s == "green")
+ return Color(3);
+ }
+ throw Failure("invalid color argument '" + s + "'");
+}
+
+PlayerBase& Engine::get_player() const
+{
+ if (! m_player)
+ throw Failure("no player set");
+ return *m_player;
+}
+
+void Engine::play(Color c, const Arguments& args, unsigned arg_move_begin)
+{
+ auto& bd = get_board();
+ if (bd.get_nu_moves() >= Board::max_game_moves)
+ throw Failure("too many moves");
+ Move mv;
+ try
+ {
+ if (arg_move_begin == 0)
+ mv = bd.from_string(args.get_line());
+ else
+ mv = bd.from_string(args.get_remaining_line(arg_move_begin - 1));
+ }
+ catch (const runtime_error& e)
+ {
+ throw Failure(e.what());
+ }
+ if (mv.is_null())
+ throw Failure("play pass not supported (anymore)");
+ if (! m_accept_illegal && ! bd.is_legal(c, mv))
+ throw Failure("illegal move");
+ m_game.play(c, mv, true);
+ board_changed();
+}
+
+void Engine::set_player(PlayerBase& player)
+{
+ m_player = &player;
+ add("genmove", &Engine::cmd_genmove);
+ add("g", &Engine::cmd_g);
+ add("reg_genmove", &Engine::cmd_reg_genmove);
+}
+
+void Engine::set_show_board(bool enable)
+{
+ if (enable && ! m_show_board)
+ LIBBOARDGAME_LOG(get_board());
+ m_show_board = enable;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Engine.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_ENGINE_H
+#define LIBPENTOBI_BASE_ENGINE_H
+
+#include "libpentobi_base/Game.h"
+#include "libpentobi_base/PlayerBase.h"
+#include "libboardgame_base/Engine.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_gtp::Arguments;
+using libboardgame_gtp::Response;
+
+//-----------------------------------------------------------------------------
+
+/** GTP Blokus engine. */
+class Engine
+ : public libboardgame_base::Engine
+{
+public:
+ explicit Engine(Variant variant);
+
+ void cmd_all_legal(const Arguments&, Response&);
+ void cmd_clear_board();
+ void cmd_final_score(Response&);
+ void cmd_g(Response&);
+ void cmd_genmove(const Arguments&, Response&);
+ void cmd_get_place(const Arguments& args, Response&);
+ void cmd_loadsgf(const Arguments&);
+ void cmd_move_info(const Arguments&, Response&);
+ void cmd_p(const Arguments&);
+ void cmd_param_base(const Arguments&, Response&);
+ void cmd_play(const Arguments&);
+ void cmd_point_integers(Response&);
+ void cmd_showboard(Response&);
+ void cmd_reg_genmove(const Arguments&, Response&);
+ void cmd_savesgf(const Arguments&);
+ void cmd_set_game(const Arguments&);
+ void cmd_undo();
+
+ /** Set the player.
+ @param player The player (@ref libboardgame_doc_storesref) */
+ void set_player(PlayerBase& player);
+
+ void set_accept_illegal(bool enable);
+
+ /** Enable or disable resigning. */
+ void set_resign(bool enable);
+
+ void set_show_board(bool enable);
+
+ const Board& get_board() const;
+
+protected:
+ Color get_color_arg(const Arguments& args, unsigned i) const;
+
+ Color get_color_arg(const Arguments& args) const;
+
+private:
+ bool m_accept_illegal = false;
+
+ bool m_show_board = false;
+
+ bool m_resign = true;
+
+ Game m_game;
+
+ PlayerBase* m_player = nullptr;
+
+ void board_changed();
+
+ void genmove(Color c, Response& response);
+
+ PlayerBase& get_player() const;
+
+ void play(Color c, const Arguments& args, unsigned arg_move_begin);
+};
+
+inline const Board& Engine::get_board() const
+{
+ return m_game.get_board();
+}
+
+inline void Engine::set_accept_illegal(bool enable)
+{
+ m_accept_illegal = enable;
+}
+
+inline void Engine::set_resign(bool enable)
+{
+ m_resign = enable;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_ENGINE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Game.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Game.h"
+
+#include "BoardUtil.h"
+#include "libboardgame_sgf/InvalidTree.h"
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::InvalidTree;
+using libboardgame_sgf::util::back_to_main_variation;
+using libboardgame_sgf::util::is_main_variation;
+using libpentobi_base::boardutil::get_current_position_as_setup;
+
+//-----------------------------------------------------------------------------
+
+Game::Game(Variant variant)
+ : m_bd(new Board(variant)),
+ m_tree(variant)
+{
+ init(variant);
+}
+
+Game::~Game() = default;
+
+void Game::add_setup(Color c, Move mv)
+{
+ auto& node = m_tree.add_setup(*m_current, c, mv);
+ goto_node(node);
+}
+
+void Game::delete_all_variations()
+{
+ goto_node(back_to_main_variation(*m_current));
+ m_tree.delete_all_variations();
+}
+
+Color Game::get_to_play_default(const Game& game)
+{
+ auto& tree = game.get_tree();
+ auto& bd = game.get_board();
+ auto node = &game.get_current();
+ Color next = Color(0);
+ while (node)
+ {
+ auto mv = tree.get_move(*node);
+ if (! mv.is_null())
+ {
+ next = bd.get_next(mv.color);
+ break;
+ }
+ Color c;
+ if (libpentobi_base::node_util::get_player(*node, c))
+ return c;
+ node = node->get_parent_or_null();
+ }
+ return bd.get_effective_to_play(next);
+}
+
+void Game::goto_node(const SgfNode& node)
+{
+ auto old = m_current;
+ try
+ {
+ update(node);
+ }
+ catch (const InvalidTree&)
+ {
+ // Try to restore the old state.
+ if (! old)
+ m_current = &node;
+ else
+ {
+ try
+ {
+ update(*old);
+ }
+ catch (const InvalidTree&)
+ {
+ }
+ }
+ throw;
+ }
+}
+
+void Game::init(Variant variant)
+{
+ m_bd->init(variant);
+ m_tree.init_variant(variant);
+ m_current = &m_tree.get_root();
+}
+
+void Game::init(unique_ptr<SgfNode>& root)
+{
+ m_tree.init(root);
+ m_bd->init(m_tree.get_variant());
+ m_current = nullptr;
+ goto_node(m_tree.get_root());
+}
+
+void Game::keep_only_position()
+{
+ m_tree.keep_only_subtree(*m_current);
+ m_tree.remove_children(m_tree.get_root());
+ m_current = nullptr;
+ goto_node(m_tree.get_root());
+}
+
+void Game::keep_only_subtree()
+{
+ m_tree.keep_only_subtree(*m_current);
+ m_current = nullptr;
+ goto_node(m_tree.get_root());
+}
+
+void Game::play(ColorMove mv, bool always_create_new_node)
+{
+ m_bd->play(mv);
+ const SgfNode* child = nullptr;
+ if (! always_create_new_node)
+ child = m_tree.find_child_with_move(*m_current, mv);
+ if (child)
+ m_current = child;
+ else
+ {
+ m_current = &m_tree.create_new_child(*m_current);
+ m_tree.set_move(*m_current, mv);
+ }
+ set_to_play(get_to_play_default(*this));
+}
+
+void Game::remove_player()
+{
+ m_tree.remove_player(*m_current);
+ update(*m_current);
+}
+
+void Game::remove_setup(Color c, Move mv)
+{
+ auto& node = m_tree.remove_setup(*m_current, c, mv);
+ goto_node(node);
+}
+
+void Game::set_player(Color c)
+{
+ m_tree.set_player(*m_current, c);
+ update(*m_current);
+}
+
+void Game::set_result(int score)
+{
+ if (is_main_variation(*m_current))
+ m_tree.set_result(m_tree.get_root(), score);
+}
+
+void Game::set_to_play(Color c)
+{
+ m_bd->set_to_play(c);
+}
+
+void Game::truncate()
+{
+ goto_node(m_tree.truncate(*m_current));
+}
+
+void Game::undo()
+{
+ LIBBOARDGAME_ASSERT(! m_tree.get_move(*m_current).is_null());
+ LIBBOARDGAME_ASSERT(m_current->has_parent());
+ truncate();
+}
+
+void Game::update(const SgfNode& node)
+{
+ m_updater.update(*m_bd, m_tree, node);
+ m_current = &node;
+ set_to_play(get_to_play_default(*this));
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Game.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_GAME_H
+#define LIBPENTOBI_BASE_GAME_H
+
+#include "Board.h"
+#include "BoardUpdater.h"
+#include "NodeUtil.h"
+#include "PentobiTree.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class Game
+{
+public:
+ /** Determine a sensible value for the color to play at the current node.
+ If the color was explicitely set with a setup property, it will be
+ used. Otherwise, the effective color to play will be used, starting
+ with the next color of the color of the last move (see
+ Board::get_effective_to_play(Color)) */
+ static Color get_to_play_default(const Game& game);
+
+
+ explicit Game(Variant variant);
+
+ ~Game();
+
+
+ void init(Variant variant);
+
+ void init();
+
+ /** Initialize game from a SGF tree.
+ @note If the tree contains invalid properties, future calls to
+ goto_node() might throw an exception.
+ @param root The root node of the SGF tree; the ownership is transferred
+ to this class.
+ @throws InvalidTree, if the root node contains invalid
+ properties */
+ void init(unique_ptr<SgfNode>& root);
+
+ const Board& get_board() const;
+
+ Variant get_variant() const;
+
+ const SgfNode& get_current() const;
+
+ const SgfNode& get_root() const;
+
+ const PentobiTree& get_tree() const;
+
+ /** Get the current color to play.
+ Initialized with get_to_play_default() but may be changed with
+ set_to_play(). */
+ Color get_to_play() const;
+
+ /** @param mv
+ @param always_create_new_node Always create a new child of the current
+ node even if a child with the move already exists. */
+ void play(ColorMove mv, bool always_create_new_node);
+
+ void play(Color c, Move mv, bool always_create_new_node);
+
+ /** Update game state to a node in the tree.
+ @throws InvalidTree, if the game was constructed with an
+ external SGF tree and the tree contained invalid property values
+ (syntactically or sematically, like moves on occupied points). If an
+ exception is thrown, the current node is not changed. */
+ void goto_node(const SgfNode& node);
+
+ /** Undo the current move and go to parent node.
+ @pre ! get_current().get_move().is_null()
+ @pre get_current()->has_parent()
+ @note Even if the implementation of this function calls goto_node(),
+ it cannot throw an InvalidPropertyValue because the class Game ensures
+ that the current node is always reachable via a path of nodes with
+ valid move properties. */
+ void undo();
+
+ /** Set the current color to play.
+ Does not store a player property in the tree or affect what color is to
+ play when navigating away from and back to the current node. */
+ void set_to_play(Color c);
+
+ ColorMove get_move() const;
+
+ /** See libpentobi_base::Tree::get_move_ignore_invalid() */
+ ColorMove get_move_ignore_invalid() const;
+
+ /** Add final score to root node if the current node is in the main
+ variation. */
+ void set_result(int score);
+
+ void set_charset(const string& charset);
+
+ void remove_move_annotation();
+
+ double get_bad_move() const;
+
+ double get_good_move() const;
+
+ bool is_doubtful_move() const;
+
+ bool is_interesting_move() const;
+
+ void set_bad_move(double value = 1);
+
+ void set_good_move(double value = 1);
+
+ void set_doubtful_move();
+
+ void set_interesting_move();
+
+ string get_comment() const;
+
+ void set_comment(const string& s);
+
+ /** Delete the current node and its subtree and go to the parent node.
+ @pre get_current().has_parent() */
+ void truncate();
+
+ void truncate_children();
+
+ /** Replace the game tree by a new one that has the current position
+ as a setup in its root node. */
+ void keep_only_position();
+
+ /** Like keep_only_position() but does not delete the children of the
+ current node. */
+ void keep_only_subtree();
+
+ void make_main_variation();
+
+ void move_up_variation();
+
+ void move_down_variation();
+
+ /** Delete all variations but the main variation.
+ If the current node is not in the main variation it will be changed
+ to the node as in libboardgame_sgf::util::back_to_main_variation() */
+ void delete_all_variations();
+
+ /** Make the current node the first child of its parent. */
+ void make_first_child();
+
+ void set_modified();
+
+ void clear_modified();
+
+ bool is_modified() const;
+
+ /** Set the AP property at the root node. */
+ void set_application(const string& name, const string& version = "");
+
+ string get_player_name(Color c) const;
+
+ void set_player_name(Color c, const string& name);
+
+ string get_date() const;
+
+ void set_date(const string& date);
+
+ void set_date_today();
+
+ /** Get event info (standard property EV) from root node. */
+ string get_event() const;
+
+ void set_event(const string& event);
+
+ /** Get round info (standard property RO) from root node. */
+ string get_round() const;
+
+ void set_round(const string& round);
+
+ /** Get time info (standard property TM) from root node. */
+ string get_time() const;
+
+ void set_time(const string& time);
+
+ bool has_setup() const;
+
+ void add_setup(Color c, Move mv);
+
+ void remove_setup(Color c, Move mv);
+
+ /** See libpentobi_base::Tree::set_player() */
+ void set_player(Color c);
+
+ /** See libpentobi_base::Tree::remove_player() */
+ void remove_player();
+
+private:
+ const SgfNode* m_current;
+
+ unique_ptr<Board> m_bd;
+
+ PentobiTree m_tree;
+
+ BoardUpdater m_updater;
+
+ void update(const SgfNode& node);
+};
+
+inline void Game::clear_modified()
+{
+ m_tree.clear_modified();
+}
+
+inline double Game::get_bad_move() const
+{
+ return m_tree.get_bad_move(*m_current);
+}
+
+inline const Board& Game::get_board() const
+{
+ return *m_bd;
+}
+
+inline string Game::get_comment() const
+{
+ return m_tree.get_comment(*m_current);
+}
+
+inline string Game::get_date() const
+{
+ return m_tree.get_date();
+}
+
+inline string Game::get_event() const
+{
+ return m_tree.get_event();
+}
+
+inline const SgfNode& Game::get_current() const
+{
+ return *m_current;
+}
+
+inline double Game::get_good_move() const
+{
+ return m_tree.get_good_move(*m_current);
+}
+
+inline ColorMove Game::get_move() const
+{
+ return m_tree.get_move(*m_current);
+}
+
+inline ColorMove Game::get_move_ignore_invalid() const
+{
+ return m_tree.get_move_ignore_invalid(*m_current);
+}
+
+inline string Game::get_player_name(Color c) const
+{
+ return m_tree.get_player_name(c);
+}
+
+inline Color Game::get_to_play() const
+{
+ return m_bd->get_to_play();
+}
+
+inline string Game::get_round() const
+{
+ return m_tree.get_round();
+}
+
+inline const SgfNode& Game::get_root() const
+{
+ return m_tree.get_root();
+}
+
+inline string Game::get_time() const
+{
+ return m_tree.get_time();
+}
+
+inline const PentobiTree& Game::get_tree() const
+{
+ return m_tree;
+}
+
+inline bool Game::has_setup() const
+{
+ return libpentobi_base::node_util::has_setup(*m_current);
+}
+
+inline Variant Game::get_variant() const
+{
+ return m_bd->get_variant();
+}
+
+inline void Game::init()
+{
+ init(m_bd->get_variant());
+}
+
+inline bool Game::is_doubtful_move() const
+{
+ return m_tree.is_doubtful_move(*m_current);
+}
+
+inline bool Game::is_interesting_move() const
+{
+ return m_tree.is_interesting_move(*m_current);
+}
+
+inline bool Game::is_modified() const
+{
+ return m_tree.is_modified();
+}
+
+inline void Game::make_first_child()
+{
+ m_tree.make_first_child(*m_current);
+}
+
+inline void Game::make_main_variation()
+{
+ m_tree.make_main_variation(*m_current);
+}
+
+inline void Game::move_down_variation()
+{
+ m_tree.move_down(*m_current);
+}
+
+inline void Game::move_up_variation()
+{
+ m_tree.move_up(*m_current);
+}
+
+inline void Game::play(Color c, Move mv, bool always_create_new_node)
+{
+ play(ColorMove(c, mv), always_create_new_node);
+}
+
+inline void Game::remove_move_annotation()
+{
+ m_tree.remove_move_annotation(*m_current);
+}
+
+inline void Game::set_application(const string& name, const string& version)
+{
+ m_tree.set_application(name, version);
+}
+
+inline void Game::set_bad_move(double value)
+{
+ m_tree.set_bad_move(*m_current, value);
+}
+
+inline void Game::set_charset(const string& charset)
+{
+ m_tree.set_charset(charset);
+}
+
+inline void Game::set_comment(const string& s)
+{
+ m_tree.set_comment(*m_current, s);
+}
+
+inline void Game::set_date(const string& date)
+{
+ m_tree.set_date(date);
+}
+
+inline void Game::set_event(const string& event)
+{
+ m_tree.set_event(event);
+}
+
+inline void Game::set_date_today()
+{
+ m_tree.set_date_today();
+}
+
+inline void Game::set_doubtful_move()
+{
+ m_tree.set_doubtful_move(*m_current);
+}
+
+inline void Game::set_good_move(double value)
+{
+ m_tree.set_good_move(*m_current, value);
+}
+
+inline void Game::set_interesting_move()
+{
+ m_tree.set_interesting_move(*m_current);
+}
+
+inline void Game::set_modified()
+{
+ m_tree.set_modified();
+}
+
+inline void Game::set_player_name(Color c, const string& name)
+{
+ m_tree.set_player_name(c, name);
+}
+
+inline void Game::set_round(const string& round)
+{
+ m_tree.set_round(round);
+}
+
+inline void Game::set_time(const string& time)
+{
+ m_tree.set_time(time);
+}
+
+inline void Game::truncate_children()
+{
+ m_tree.remove_children(*m_current);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_GAME_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Geometry.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_GEOMETRY_H
+#define LIBPENTOBI_BASE_GEOMETRY_H
+
+#include "Point.h"
+#include "libboardgame_base/Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+typedef libboardgame_base::Geometry<Point> Geometry;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_GEOMETRY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Grid.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_GRID_H
+#define LIBPENTOBI_BASE_GRID_H
+
+#include "Point.h"
+#include "libboardgame_base/Grid.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+template<typename T>
+using Grid = libboardgame_base::Grid<Point, T>;
+
+template<typename T>
+using GridExt = libboardgame_base::GridExt<Point, T>;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_GRID_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Marker.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_MARKER_H
+#define LIBPENTOBI_BASE_MARKER_H
+
+#include "Point.h"
+#include "libboardgame_base/Marker.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+typedef libboardgame_base::Marker<Point> Marker;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_MARKER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Move.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_MOVE_H
+#define LIBPENTOBI_BASE_MOVE_H
+
+#include <cstdint>
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class Move
+{
+public:
+ /** Integer type used internally in this class to store a move.
+ This class is optimized for size not for speed because there are
+ large precomputed data structures that store moves and move lists.
+ Therefore it uses uint_least16_t, not uint_fast16_t. */
+ typedef uint_least16_t IntType;
+
+ static const IntType onboard_moves_classic = 30433;
+
+ static const IntType onboard_moves_trigon = 32131;
+
+ static const IntType onboard_moves_trigon_3 = 24859;
+
+ static const IntType onboard_moves_duo = 13729;
+
+ static const IntType onboard_moves_junior = 7217;
+
+ static const IntType onboard_moves_nexos = 15157;
+
+ static const IntType onboard_moves_callisto = 9433;
+
+ static const IntType onboard_moves_callisto_2 = 4265;
+
+ static const IntType onboard_moves_callisto_3 = 6885;
+
+ /** Integer range of moves.
+ The maximum is given by the number of on-board moves in game variant
+ Trigon, plus a null move. */
+ static const IntType range = onboard_moves_trigon + 1;
+
+ static Move null();
+
+ Move();
+
+ explicit Move(IntType i);
+
+ bool operator==(const Move& mv) const;
+
+ bool operator!=(const Move& mv) const;
+
+ bool operator<(const Move& mv) const;
+
+ bool is_null() const;
+
+ /** Return move as an integer between 0 and Move::range */
+ IntType to_int() const;
+
+private:
+ static const IntType value_uninitialized = range;
+
+ IntType m_i;
+
+ bool is_initialized() const;
+};
+
+inline Move::Move()
+{
+#if LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+inline Move::Move(IntType i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = i;
+}
+
+inline bool Move::operator==(const Move& mv) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(mv.is_initialized());
+ return m_i == mv.m_i;
+}
+
+inline bool Move::operator!=(const Move& mv) const
+{
+ return ! operator==(mv);
+}
+
+inline bool Move::operator<(const Move& mv) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(mv.is_initialized());
+ return m_i < mv.m_i;
+}
+
+inline bool Move::is_initialized() const
+{
+ return m_i < value_uninitialized;
+}
+
+inline bool Move::is_null() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i == 0;
+}
+
+inline Move Move::null()
+{
+ return Move(0);
+}
+
+inline Move::IntType Move::to_int() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_BASE_MOVE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/MoveInfo.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_MOVE_INFO_H
+#define LIBPENTOBI_BASE_MOVE_INFO_H
+
+#include "Move.h"
+#include "MovePoints.h"
+#include "Piece.h"
+#include "PieceInfo.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Most frequently accessed move info.
+ Contains the points and the piece of the move. If the point list is smaller
+ than MAX_SIZE, values above end() up to MAX_SIZE may be accessed and
+ contain Point::null() to allow loop unrolling. The points correspond to
+ PieceInfo::get_points(), which includes certain junction points in Nexos,
+ see comment there.
+ Since this is the most performance-critical data structure, it takes
+ a template argument to make the space for move points not larger than
+ needed in the current game variant. */
+template<unsigned MAX_SIZE>
+class MoveInfo
+{
+public:
+ MoveInfo() = default;
+
+ MoveInfo(Piece piece, const MovePoints& points)
+ {
+ m_piece = static_cast<uint_least8_t>(piece.to_int());
+ m_size = static_cast<uint_least8_t>(points.size());
+ for (MovePoints::IntType i = 0; i < MAX_SIZE; ++i)
+ m_points[i] = points.get_unchecked(i);
+ }
+
+ const Point* begin() const { return m_points; }
+
+ const Point* end() const { return m_points + m_size; }
+
+ Piece get_piece() const { return Piece(m_piece); }
+
+ unsigned get_size() const { return m_size; }
+
+private:
+ uint_least8_t m_piece;
+
+ uint_least8_t m_size;
+
+ Point m_points[MAX_SIZE];
+};
+
+//-----------------------------------------------------------------------------
+
+/** Less frequently accessed move info.
+ Stored separately from move points and move piece to improve CPU cache
+ performance.
+ Since this is a performance-critical data structure, it takes
+ a template argument to make the space for move points not larger than
+ needed in the current game variant.
+ @tparam MAX_ADJ_ATTACH Maximum total number of attach points and adjacent
+ points of a piece in the corresponding game variant. */
+template<unsigned MAX_ADJ_ATTACH>
+struct MoveInfoExt
+{
+ /** Concatenated list of adjacent and attach points. */
+ Point points[MAX_ADJ_ATTACH];
+
+ uint_least8_t size_attach_points;
+
+ uint_least8_t size_adj_points;
+
+ const Point* begin_adj() const { return points; }
+
+ const Point* end_adj() const { return points + size_adj_points; }
+
+ const Point* begin_attach() const { return end_adj(); }
+
+ const Point* end_attach() const
+ {
+ return begin_attach() + size_attach_points;
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+/** Least frequently accessed move info.
+ Stored separately from move points and move piece to improve CPU cache
+ performance. */
+struct MoveInfoExt2
+{
+ /** Whether the move breaks rotational symmetry of the board.
+ Currently not initialized for classic and trigon_3 board types because
+ enforced rotational-symmetric draws are not used in the MCTS search on
+ these boards (trigon_3 has no 2-player game variant and classic_2
+ currently only supports colored starting points, which makes rotational
+ draws impossible. */
+ bool breaks_symmetry;
+
+ uint_least8_t scored_points_size;
+
+ /** The rotational-symmetric counterpart to this move.
+ Only initalized for game variants that have rotational-symmetric boards
+ and starting points. */
+ Move symmetric_move;
+
+ Point label_pos;
+
+ /** The points of a move that contribute to the score, which excludes
+ junction points in Nexos. */
+ Point scored_points[PieceInfo::max_scored_size];
+
+
+ const Point* begin_scored_points() const { return scored_points; }
+
+ const Point* end_scored_points() const
+ {
+ return scored_points + scored_points_size;
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_BASE_MOVE_INFO_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/MoveList.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_MOVE_LIST_H
+#define LIBPENTOBI_BASE_MOVE_LIST_H
+
+#include "Move.h"
+#include "libboardgame_util/ArrayList.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** List that can hold all possible moves, not including Move::null() */
+typedef libboardgame_util::ArrayList<Move, Move::range - 1> MoveList;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_MOVE_LIST_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/MoveMarker.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_MOVE_MARKER_H
+#define LIBPENTOBI_BASE_MOVE_MARKER_H
+
+#include <array>
+#include "Move.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class MoveMarker
+{
+public:
+ MoveMarker()
+ {
+ clear();
+ }
+
+ bool operator[](Move mv) const
+ {
+ return m_a[mv.to_int()];
+ }
+
+ void set(Move mv)
+ {
+ m_a[mv.to_int()] = true;
+ }
+
+ void clear(Move mv)
+ {
+ m_a[mv.to_int()] = false;
+ }
+
+ template<class T>
+ void set(const T& t)
+ {
+ for (Move mv : t)
+ set(mv);
+ }
+
+ template<class T>
+ void clear(const T& t)
+ {
+ for (Move mv : t)
+ clear(mv);
+ }
+
+ void set()
+ {
+ m_a.fill(true);
+ }
+
+ void clear()
+ {
+ m_a.fill(false);
+ }
+
+private:
+ array<bool, Move::range> m_a;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_MOVE_MARKER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/MovePoints.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_MOVE_POINTS_H
+#define LIBPENTOBI_BASE_MOVE_POINTS_H
+
+#include "PieceInfo.h"
+#include "Point.h"
+#include "libboardgame_util/ArrayList.h"
+
+namespace libpentobi_base {
+
+using libboardgame_util::ArrayList;
+
+//-----------------------------------------------------------------------------
+
+typedef ArrayList<Point, PieceInfo::max_size, unsigned short> MovePoints;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_BASE_MOVE_POINTS_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/NexosGeometry.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "NexosGeometry.h"
+
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+map<unsigned, shared_ptr<NexosGeometry>> NexosGeometry::s_geometry;
+
+NexosGeometry::NexosGeometry(unsigned sz)
+{
+ Geometry::init(sz * 2 - 1, sz * 2 - 1);
+}
+
+const NexosGeometry& NexosGeometry::get(unsigned sz)
+{
+ auto pos = s_geometry.find(sz);
+ if (pos != s_geometry.end())
+ return *pos->second;
+ shared_ptr<NexosGeometry> geometry(new NexosGeometry(sz));
+ return *s_geometry.insert(make_pair(sz, geometry)).first->second;
+}
+
+auto NexosGeometry::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+ LIBBOARDGAME_UNUSED(x);
+ LIBBOARDGAME_UNUSED(y);
+ return AdjCoordList();
+}
+
+auto NexosGeometry::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+ DiagCoordList l;
+ if (get_point_type(x, y) == 1)
+ {
+ l.push_back(CoordPoint(x - 2, y));
+ l.push_back(CoordPoint(x + 2, y));
+ l.push_back(CoordPoint(x - 1, y - 1));
+ l.push_back(CoordPoint(x + 1, y + 1));
+ l.push_back(CoordPoint(x - 1, y + 1));
+ l.push_back(CoordPoint(x + 1, y - 1));
+ }
+ else if (get_point_type(x, y) == 2)
+ {
+ l.push_back(CoordPoint(x, y - 2));
+ l.push_back(CoordPoint(x, y + 2));
+ l.push_back(CoordPoint(x - 1, y - 1));
+ l.push_back(CoordPoint(x + 1, y + 1));
+ l.push_back(CoordPoint(x - 1, y + 1));
+ l.push_back(CoordPoint(x + 1, y - 1));
+ }
+ return l;
+}
+
+unsigned NexosGeometry::get_period_x() const
+{
+ return 2;
+}
+
+unsigned NexosGeometry::get_period_y() const
+{
+ return 2;
+}
+
+unsigned NexosGeometry::get_point_type(int x, int y) const
+{
+ if (x % 2 == 0)
+ return y % 2 == 0 ? 0 : 2;
+ else
+ return y % 2 == 0 ? 1 : 3;
+}
+
+bool NexosGeometry::init_is_onboard(unsigned x, unsigned y) const
+{
+ return x < get_width() && y < get_height() && get_point_type(x, y) != 3;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/NexosGeometry.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_NEXOS_GEOMETRY_H
+#define LIBPENTOBI_BASE_NEXOS_GEOMETRY_H
+
+#include <map>
+#include <memory>
+#include "Geometry.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Geometry as used in the game Nexos.
+ The points of the board are horizontal or vertical segments and junctions.
+ Junctions only need to be included in piece definitions if they are
+ necessary to indicate that the opponent cannot cross the junction
+ (i.e. if exactly two segments of the piece with the same orientation
+ connect to the junction).
+ The coordinates are like:
+ <tt>
+ 0 1 2 3 4 5 6 ...
+ 0 + - + - + - +
+ 1 | | | |
+ 2 + - + - + - +
+ 3 | | | |
+ 4 + - + - + - +
+ </tt>
+ There are four point types: 0=junction, 1=horizontal segment, 2=vertical
+ segment, 3=hole surrounded by segments.
+ To fit with the generalizations used in the Blokus engine, points have no
+ adjacent points, and points are diagonal to each other if they are segments
+ that connect to the same junction. */
+class NexosGeometry final
+ : public Geometry
+{
+public:
+ /** Create or reuse an already created geometry with a given size.
+ @param sz The number of segments in a row or column. */
+ static const NexosGeometry& get(unsigned sz);
+
+
+ AdjCoordList get_adj_coord(int x, int y) const override;
+
+ DiagCoordList get_diag_coord(int x, int y) const override;
+
+ unsigned get_point_type(int x, int y) const override;
+
+ unsigned get_period_x() const override;
+
+ unsigned get_period_y() const override;
+
+protected:
+ bool init_is_onboard(unsigned x, unsigned y) const override;
+
+private:
+ /** Stores already created geometries by size. */
+ static map<unsigned, shared_ptr<NexosGeometry>> s_geometry;
+
+
+ explicit NexosGeometry(unsigned sz);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_NEXOS_GEOMETRY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/NodeUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "NodeUtil.h"
+
+#include "libboardgame_util/StringUtil.h"
+
+namespace libpentobi_base {
+namespace node_util {
+
+using libboardgame_sgf::InvalidPropertyValue;
+using libboardgame_sgf::InvalidTree;
+using libboardgame_util::split;
+using libboardgame_util::trim;
+
+//-----------------------------------------------------------------------------
+
+bool get_move(const SgfNode& node, Variant variant, Color& c,
+ MovePoints& points)
+{
+ string id;
+ // Pentobi 0.1 used BLUE/YELLOW/RED/GREEN instead of 1/2/3/4 as suggested
+ // by SGF FF[5]. Pentobi 12.0 erroneosly used 1/2 for two-player Callisto
+ // instead of B/W. We still want to be able to read files written by older
+ // versions. They will be converted to the current format by
+ // PentobiTreeWriter.
+ if (get_nu_colors(variant) == 2)
+ {
+ if (node.has_property("B"))
+ {
+ id = "B";
+ c = Color(0);
+ }
+ else if (node.has_property("W"))
+ {
+ id = "W";
+ c = Color(1);
+ }
+ else if (node.has_property("1"))
+ {
+ id = "1";
+ c = Color(0);
+ }
+ else if (node.has_property("2"))
+ {
+ id = "2";
+ c = Color(1);
+ }
+ else if (node.has_property("BLUE"))
+ {
+ id = "BLUE";
+ c = Color(0);
+ }
+ else if (node.has_property("GREEN"))
+ {
+ id = "GREEN";
+ c = Color(1);
+ }
+ }
+ else
+ {
+ if (node.has_property("1"))
+ {
+ id = "1";
+ c = Color(0);
+ }
+ else if (node.has_property("2"))
+ {
+ id = "2";
+ c = Color(1);
+ }
+ else if (node.has_property("3"))
+ {
+ id = "3";
+ c = Color(2);
+ }
+ else if (node.has_property("4"))
+ {
+ id = "4";
+ c = Color(3);
+ }
+ else if (node.has_property("BLUE"))
+ {
+ id = "BLUE";
+ c = Color(0);
+ }
+ else if (node.has_property("YELLOW"))
+ {
+ id = "YELLOW";
+ c = Color(1);
+ }
+ else if (node.has_property("RED"))
+ {
+ id = "RED";
+ c = Color(2);
+ }
+ else if (node.has_property("GREEN"))
+ {
+ id = "GREEN";
+ c = Color(3);
+ }
+ }
+ if (id.empty())
+ return false;
+ vector<string> values;
+ values = node.get_multi_property(id);
+ // Note: we still support having the points of a move in a list of point
+ // values instead of a single value as used by Pentobi <= 0.2, but it
+ // is deprecated
+ points.clear();
+ auto& geo = get_geometry(variant);
+ bool is_nexos = (get_board_type(variant) == BoardType::nexos);
+ for (const auto& s : values)
+ {
+ if (trim(s).empty())
+ continue;
+ vector<string> v = split(s, ',');
+ for (const auto& p_str : v)
+ {
+ Point p;
+ if (! geo.from_string(p_str, p))
+ throw InvalidPropertyValue(id, p_str);
+ if (is_nexos)
+ {
+ auto point_type = geo.get_point_type(p);
+ if (point_type != 1 && point_type != 2)
+ // Silently discard points that are not line segments, such
+ // files were written by some (unreleased) versions of
+ // Pentobi.
+ continue;
+ }
+ points.push_back(p);
+ }
+ }
+ return true;
+}
+
+bool get_player(const SgfNode& node, Color& c)
+{
+ if (! node.has_property("PL"))
+ return false;
+ string value = node.get_property("PL");
+ if (value == "B" || value == "1")
+ c = Color(0);
+ else if (value == "W" || value == "2")
+ c = Color(1);
+ else if (value == "3")
+ c = Color(2);
+ else if (value == "4")
+ c = Color(3);
+ else
+ throw InvalidTree("invalid value for PL property");
+ return true;
+}
+
+bool has_setup(const SgfNode& node)
+{
+ for (auto& i : node.get_properties())
+ if (i.id == "AB" || i.id == "AW" || i.id == "A1" || i.id == "A2"
+ || i.id == "A3" || i.id == "A4" || i.id == "AE")
+ return true;
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace node_util
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/NodeUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_NODE_UTIL_H
+#define LIBPENTOBI_BASE_NODE_UTIL_H
+
+#include "Color.h"
+#include "MovePoints.h"
+#include "Variant.h"
+#include "libboardgame_sgf/SgfNode.h"
+
+namespace libpentobi_base {
+namespace node_util {
+
+using libboardgame_sgf::SgfNode;
+
+//-----------------------------------------------------------------------------
+
+/** Get move points.
+ @param node
+ @param variant
+ @param[out] c The move color (only defined if return value is true)
+ @param[out] points The move points (only defined if return value is
+ true)
+ @return true if the node has a move property. */
+bool get_move(const SgfNode& node, Variant variant, Color& c,
+ MovePoints& points);
+
+/** Check if a node has setup properties (not including the PL property). */
+bool has_setup(const SgfNode& node);
+
+/** Get the color to play in a setup position (PL property). */
+bool get_player(const SgfNode& node, Color& c);
+
+//-----------------------------------------------------------------------------
+
+} // namespace node_util
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_NODE_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiSgfUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PentobiSgfUtil.h"
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+namespace sgf_util {
+
+//-----------------------------------------------------------------------------
+
+const char* get_color_id(Variant variant, Color c)
+{
+ static_assert(Color::range == 4, "");
+ if (get_nu_colors(variant) == 2)
+ return c == Color(0) ? "B" : "W";
+ if (c == Color(0))
+ return "1";
+ if (c == Color(1))
+ return "2";
+ if (c == Color(2))
+ return "3";
+ LIBBOARDGAME_ASSERT(c == Color(3));
+ return "4";
+}
+
+const char* get_setup_id(Variant variant, Color c)
+{
+ static_assert(Color::range == 4, "");
+ if (get_nu_colors(variant) == 2)
+ return c == Color(0) ? "AB" : "AW";
+ if (c == Color(0))
+ return "A1";
+ if (c == Color(1))
+ return "A2";
+ if (c == Color(2))
+ return "A3";
+ LIBBOARDGAME_ASSERT(c == Color(3));
+ return "A4";
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace sgf_util
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiSgfUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H
+#define LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H
+
+#include "Color.h"
+#include "Variant.h"
+
+namespace libpentobi_base {
+namespace sgf_util {
+
+//-----------------------------------------------------------------------------
+
+/** Get SGF move property ID for a color in a game variant. */
+const char* get_color_id(Variant variant, Color c);
+
+/** Get SGF setup property ID for a color in a game variant. */
+const char* get_setup_id(Variant variant, Color c);
+
+//-----------------------------------------------------------------------------
+
+} // namespace sgf_util
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PENTOBI_SGF_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiTree.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PentobiTree.h"
+
+#include "BoardUpdater.h"
+#include "BoardUtil.h"
+#include "NodeUtil.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::InvalidPropertyValue;
+using libboardgame_sgf::InvalidTree;
+using libboardgame_util::to_string;
+using libpentobi_base::boardutil::get_current_position_as_setup;
+
+//-----------------------------------------------------------------------------
+
+PentobiTree::PentobiTree(Variant variant)
+{
+ init_variant(variant);
+}
+
+PentobiTree::PentobiTree(unique_ptr<SgfNode>& root)
+{
+ init(root);
+}
+
+const SgfNode& PentobiTree::add_setup(const SgfNode& node, Color c, Move mv)
+{
+ const SgfNode* result;
+ if (has_move(node))
+ result = &create_new_child(node);
+ else
+ result = &node;
+ Setup::PlacementList add_empty = get_setup_property(*result, "AE");
+ if (add_empty.remove(mv))
+ set_setup_property(*result, "AE", add_empty);
+ auto id = get_setup_prop_id(c);
+ Setup::PlacementList add_color = get_setup_property(*result, id);
+ if (add_color.include(mv))
+ set_setup_property(*result, id, add_color);
+ return *result;
+}
+
+const SgfNode* PentobiTree::find_child_with_move(const SgfNode& node,
+ ColorMove mv) const
+{
+ for (auto& i : node.get_children())
+ if (get_move(i) == mv)
+ return &i;
+ return nullptr;
+}
+
+ColorMove PentobiTree::get_move(const SgfNode& node) const
+{
+ Color c;
+ MovePoints points;
+ if (! libpentobi_base::node_util::get_move(node, m_variant, c, points))
+ return ColorMove::null();
+ if (points.size() == 0)
+ // Older (unreleased?) versions of Pentobi used empty move values
+ // to encode pass moves in search tree dumps but we don't support
+ // pass moves Board anymore.
+ return ColorMove::null();
+ Move mv;
+ if (! m_bc->find_move(points, mv))
+ throw InvalidTree("Tree contains illegal move");
+ return ColorMove(c, mv);
+}
+
+ColorMove PentobiTree::get_move_ignore_invalid(const SgfNode& node) const
+{
+ try
+ {
+ return get_move(node);
+ }
+ catch (const InvalidTree&)
+ {
+ return ColorMove::null();
+ }
+}
+
+const SgfNode* PentobiTree::get_node_before_move_number(
+ unsigned move_number) const
+{
+ auto node = &get_root();
+ unsigned n = 0;
+ while (node->has_children())
+ {
+ auto& child = node->get_first_child();
+ if (! get_move(child).is_null() && n++ == move_number)
+ return node;
+ node = &child;
+ }
+ return nullptr;
+}
+
+string PentobiTree::get_player_name(Color c) const
+{
+ string name;
+ auto& root = get_root();
+ if (get_nu_players(m_variant) == 2)
+ {
+ if (c == Color(0) || c == Color(2))
+ name = root.get_property("PB", "");
+ else if (c == Color(1) || c == Color(2))
+ name = root.get_property("PW", "");
+ }
+ else
+ {
+ if (c == Color(0))
+ name = root.get_property("P1", "");
+ else if (c == Color(1))
+ name = root.get_property("P2", "");
+ else if (c == Color(2))
+ name = root.get_property("P3", "");
+ else if (c == Color(3))
+ name = root.get_property("P4", "");
+ }
+ return name;
+}
+
+Setup::PlacementList PentobiTree::get_setup_property(const SgfNode& node,
+ const char* id) const
+{
+ vector<string> values = node.get_multi_property(id);
+ Setup::PlacementList result;
+ for (const string& s : values)
+ result.push_back(m_bc->from_string(s));
+ return result;
+}
+
+Variant PentobiTree::get_variant(const SgfNode& root)
+{
+ string game = root.get_property("GM");
+ Variant variant;
+ if (! parse_variant(game, variant))
+ throw InvalidPropertyValue("GM", game);
+ return variant;
+}
+
+bool PentobiTree::has_main_variation_moves() const
+{
+ auto node = &get_root();
+ while (node)
+ {
+ if (has_move_ignore_invalid(*node))
+ return true;
+ node = node->get_first_child_or_null();
+ }
+ return false;
+}
+
+void PentobiTree::init(unique_ptr<SgfNode>& root)
+{
+ Variant variant = get_variant(*root);
+ SgfTree::init(root);
+ m_variant = variant;
+ init_board_const(variant);
+}
+
+void PentobiTree::init_board_const(Variant variant)
+{
+ m_bc = &BoardConst::get(variant);
+}
+
+void PentobiTree::init_variant(Variant variant)
+{
+ SgfTree::init();
+ m_variant = variant;
+ set_game_property();
+ init_board_const(variant);
+ clear_modified();
+}
+
+void PentobiTree::keep_only_subtree(const SgfNode& node)
+{
+ LIBBOARDGAME_ASSERT(contains(node));
+ if (&node == &get_root())
+ return;
+ string charset = get_root().get_property("CA", "");
+ string application = get_root().get_property("AP", "");
+ bool create_new_setup = has_move(node);
+ if (! create_new_setup)
+ {
+ auto current = node.get_parent_or_null();
+ while (current)
+ {
+ if (has_move(*current) || node_util::has_setup(*current))
+ {
+ create_new_setup = true;
+ break;
+ }
+ current = current->get_parent_or_null();
+ }
+ }
+ if (create_new_setup)
+ {
+ unique_ptr<Board> bd(new Board(m_variant));
+ BoardUpdater updater;
+ updater.update(*bd, *this, node);
+ Setup setup;
+ get_current_position_as_setup(*bd, setup);
+ LIBBOARDGAME_ASSERT(! node_util::has_setup(node));
+ set_setup(node, setup);
+ }
+ make_root(node);
+ if (! application.empty())
+ {
+ set_property(node, "AP", application);
+ move_property_to_front(node, "AP");
+ }
+ if (! charset.empty())
+ {
+ set_property(node, "CA", charset);
+ move_property_to_front(node, "CA");
+ }
+ set_game_property();
+}
+
+void PentobiTree::remove_player(const SgfNode& node)
+{
+ remove_property(node, "PL");
+}
+
+const SgfNode& PentobiTree::remove_setup(const SgfNode& node, Color c,
+ Move mv)
+{
+ const SgfNode* result;
+ if (has_move(node))
+ result = &create_new_child(node);
+ else
+ result = &node;
+ auto id = get_setup_prop_id(c);
+ auto add_color = get_setup_property(*result, id);
+ if (add_color.remove(mv))
+ set_setup_property(*result, id, add_color);
+ else
+ {
+ Setup::PlacementList add_empty = get_setup_property(*result, "AE");
+ if (add_empty.include(mv))
+ set_setup_property(*result, "AE", add_empty);
+ }
+ return *result;
+}
+
+void PentobiTree::set_game_property()
+{
+ auto& root = get_root();
+ set_property(root, "GM", to_string(m_variant));
+ move_property_to_front(root, "GM");
+}
+
+void PentobiTree::set_move(const SgfNode& node, Color c, Move mv)
+{
+ LIBBOARDGAME_ASSERT(! mv.is_null());
+ auto id = get_color(c);
+ set_property(node, id, m_bc->to_string(mv, false));
+}
+
+void PentobiTree::set_player(const SgfNode& node, Color c)
+{
+ set_property(node, "PL", get_color(c));
+}
+
+void PentobiTree::set_player_name(Color c, const string& name)
+{
+ auto& root = get_root();
+ if (get_nu_players(m_variant) == 2)
+ {
+ if (c == Color(0) || c == Color(2))
+ set_property_remove_empty(root, "PB", name);
+ else if (c == Color(1) || c == Color(3))
+ set_property_remove_empty(root, "PW", name);
+ }
+ else
+ {
+ if (c == Color(0))
+ set_property_remove_empty(root, "P1", name);
+ else if (c == Color(1))
+ set_property_remove_empty(root, "P2", name);
+ else if (c == Color(2))
+ set_property_remove_empty(root, "P3", name);
+ else if (c == Color(3))
+ set_property_remove_empty(root, "P4", name);
+ }
+}
+
+void PentobiTree::set_result(const SgfNode& node, int score)
+{
+ if (score > 0)
+ {
+ ostringstream s;
+ s << "B+" << score;
+ set_property(node, "RE", s.str());
+ }
+ else if (score < 0)
+ {
+ ostringstream s;
+ s << "W+" << (-score);
+ set_property(node, "RE", s.str());
+ }
+ else
+ set_property(node, "RE", "0");
+}
+
+void PentobiTree::set_setup(const SgfNode& node, const Setup& setup)
+{
+ auto nu_colors = get_nu_colors(m_variant);
+ LIBBOARDGAME_ASSERT(nu_colors >= 2 && nu_colors <= 4);
+ remove_property(node, "B");
+ remove_property(node, "W");
+ remove_property(node, "1");
+ remove_property(node, "2");
+ remove_property(node, "3");
+ remove_property(node, "4");
+ remove_property(node, "AB");
+ remove_property(node, "AW");
+ remove_property(node, "A1");
+ remove_property(node, "A2");
+ remove_property(node, "A3");
+ remove_property(node, "A4");
+ remove_property(node, "AE");
+ if (nu_colors == 2)
+ {
+ set_setup_property(node, "AB", setup.placements[Color(0)]);
+ set_setup_property(node, "AW", setup.placements[Color(1)]);
+ }
+ else
+ {
+ set_setup_property(node, "A1", setup.placements[Color(0)]);
+ set_setup_property(node, "A2", setup.placements[Color(1)]);
+ set_setup_property(node, "A3", setup.placements[Color(2)]);
+ if (nu_colors > 3)
+ set_setup_property(node, "A4", setup.placements[Color(3)]);
+ }
+ set_player(node, setup.to_play);
+}
+
+void PentobiTree::set_setup_property(const SgfNode& node, const char* id,
+ const Setup::PlacementList& placements)
+{
+ if (placements.empty())
+ {
+ remove_property(node, id);
+ return;
+ }
+ vector<string> values;
+ for (Move mv : placements)
+ values.push_back(m_bc->to_string(mv, false));
+ set_property(node, id, values);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiTree.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PENTOBI_TREE_H
+#define LIBPENTOBI_BASE_PENTOBI_TREE_H
+
+#include "ColorMove.h"
+#include "BoardConst.h"
+#include "Variant.h"
+#include "Setup.h"
+#include "PentobiSgfUtil.h"
+#include "libboardgame_sgf/SgfTree.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libboardgame_sgf::SgfTree;
+
+//-----------------------------------------------------------------------------
+
+/** Blokus SGF tree.
+ See also doc/blksgf/Pentobi-SGF.html in the Pentobi distribution for
+ a description of the properties used. */
+class PentobiTree
+ : public SgfTree
+{
+public:
+ /** Parse the GM property of a root node.
+ @throws MissingProperty
+ @throws InvalidPropertyValue */
+ static Variant get_variant(const SgfNode& root);
+
+
+ explicit PentobiTree(Variant variant);
+
+ explicit PentobiTree(unique_ptr<SgfNode>& root);
+
+ void init(unique_ptr<SgfNode>& root) override;
+
+ void init_variant(Variant variant);
+
+ void set_move(const SgfNode& node, ColorMove mv);
+
+ void set_move(const SgfNode& node, Color c, Move mv);
+
+ /** Return move or ColorMove::null() if node has no move property.
+ @throws InvalidTree if the node has a move property with an invalid
+ value. */
+ ColorMove get_move(const SgfNode& node) const;
+
+ /** Like get_move() but returns ColorMove::null() on invalid property
+ value. */
+ ColorMove get_move_ignore_invalid(const SgfNode& node) const;
+
+ /** Same as ! get_move.is_null() */
+ bool has_move(const SgfNode& node) const;
+
+ /** Same as ! get_move_ignore_invalid.is_null() */
+ bool has_move_ignore_invalid(const SgfNode& node) const;
+
+ const SgfNode* find_child_with_move(const SgfNode& node,
+ ColorMove mv) const;
+
+ void set_result(const SgfNode& node, int score);
+
+ const SgfNode* get_node_before_move_number(unsigned move_number) const;
+
+ Variant get_variant() const;
+
+ string get_player_name(Color c) const;
+
+ void set_player_name(Color c, const string& name);
+
+ const BoardConst& get_board_const() const;
+
+ /** Check if any node in the main variation has a move.
+ Invalid move properties are ignored. */
+ bool has_main_variation_moves() const;
+
+ void keep_only_subtree(const SgfNode& node);
+
+ /** Add a piece as setup.
+ @pre ! mv.is_null()
+ If the node already contains a move, a new child will be created.
+ @pre The piece points must be empty on the board
+ @return The node or the new child if one was created. */
+ const SgfNode& add_setup(const SgfNode& node, Color c, Move mv);
+
+ /** Remove a piece using setup properties.
+ @pre ! mv.is_null()
+ If the node already contains a move, a new child will be created.
+ @pre The move must exist on the board
+ @return The node or the new child if one was created. */
+ const SgfNode& remove_setup(const SgfNode& node, Color c, Move mv);
+
+ /** Set the color to play in a setup position (PL property). */
+ void set_player(const SgfNode& node, Color c);
+
+ /** Remove the PL property.
+ @see set_player() */
+ void remove_player(const SgfNode& node);
+
+private:
+ Variant m_variant;
+
+ const BoardConst* m_bc;
+
+ const char* get_color(Color c) const;
+
+ Setup::PlacementList get_setup_property(const SgfNode& node,
+ const char* id) const;
+
+ const char* get_setup_prop_id(Color c) const;
+
+ void set_setup(const SgfNode& node, const Setup& setup);
+
+ void init_board_const(Variant variant);
+
+ void set_game_property();
+
+ void set_setup_property(const SgfNode& node, const char* id,
+ const Setup::PlacementList& placements);
+};
+
+inline const BoardConst& PentobiTree::get_board_const() const
+{
+ return *m_bc;
+}
+
+inline const char* PentobiTree::get_color(Color c) const
+{
+ return sgf_util::get_color_id(m_variant, c);
+}
+
+inline const char* PentobiTree::get_setup_prop_id(Color c) const
+{
+ return sgf_util::get_setup_id(m_variant, c);
+}
+
+inline Variant PentobiTree::get_variant() const
+{
+ return m_variant;
+}
+
+inline bool PentobiTree::has_move(const SgfNode& node) const
+{
+ return ! get_move(node).is_null();
+}
+
+inline bool PentobiTree::has_move_ignore_invalid(const SgfNode& node) const
+{
+ return ! get_move_ignore_invalid(node).is_null();
+}
+
+inline void PentobiTree::set_move(const SgfNode& node, ColorMove mv)
+{
+ set_move(node, mv.color, mv.move);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PENTOBI_SGF_TREE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiTreeWriter.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PentobiTreeWriter.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+PentobiTreeWriter::PentobiTreeWriter(ostream& out, const PentobiTree& tree)
+ : libboardgame_sgf::TreeWriter(out, tree.get_root()),
+ m_variant(tree.get_variant())
+{
+}
+
+PentobiTreeWriter::~PentobiTreeWriter()
+{
+}
+
+void PentobiTreeWriter::write_property(const string& id,
+ const vector<string>& values)
+{
+ auto nu_colors = get_nu_colors(m_variant);
+ // Replace obsolete move property IDs or multi-valued move properties
+ // as used by early versions of Pentobi
+ if (id == "BLUE" || id == "YELLOW" || id == "GREEN" || id == "RED"
+ || ((id == "1" || id == "2" || id == "3" || id == "4" || id == "B"
+ || id == "W")
+ && values.size() > 1))
+ {
+ string new_id;
+ if (id == "BLUE")
+ new_id = (nu_colors == 2 ? "B" : "1");
+ else if (id == "YELLOW")
+ new_id = "2";
+ else if (id == "GREEN")
+ new_id = (nu_colors == 2 ? "W" : "4");
+ else if (id == "RED")
+ new_id = "3";
+ else
+ new_id = id;
+ if (values.size() < 2)
+ libboardgame_sgf::TreeWriter::write_property(new_id, values);
+ else
+ {
+ string val = values[0];
+ for (size_t i = 1; i < values.size(); ++i)
+ val += "," + values[i];
+ vector<string> new_values;
+ new_values.push_back(val);
+ libboardgame_sgf::TreeWriter::write_property(new_id, new_values);
+ }
+ return;
+ }
+ // Pentobi 12.0 versions erroneously used multi-player properties for
+ // two-player Callisto.
+ if (nu_colors == 2)
+ {
+ if (id == "1")
+ {
+ libboardgame_sgf::TreeWriter::write_property("B", values);
+ return;
+ }
+ if (id == "2")
+ {
+ libboardgame_sgf::TreeWriter::write_property("W", values);
+ return;
+ }
+ }
+ libboardgame_sgf::TreeWriter::write_property(id, values);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiTreeWriter.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H
+#define LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H
+
+#include "PentobiTree.h"
+#include "libboardgame_sgf/TreeWriter.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Blokus-specific tree writer.
+ Automatically replaces obsolete move properties as used by early versions
+ of Pentobi. */
+class PentobiTreeWriter
+ : public libboardgame_sgf::TreeWriter
+{
+public:
+ PentobiTreeWriter(ostream& out, const PentobiTree& tree);
+
+ virtual ~PentobiTreeWriter();
+
+ void write_property(const string& id,
+ const vector<string>& values) override;
+
+private:
+ Variant m_variant;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Piece.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PIECE_H
+#define LIBPENTOBI_BASE_PIECE_H
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Wrapper around an integer representing a piece type in a certain
+ game variant. */
+class Piece
+{
+public:
+ typedef uint_fast8_t IntType;
+
+ /** Maximum number of unique pieces per color. */
+ static const IntType max_pieces = 24;
+
+ /** Integer range used for unique pieces without the null piece. */
+ static const IntType range_not_null = max_pieces;
+
+ /** Integer range used for unique pieces including the null piece */
+ static const IntType range = max_pieces + 1;
+
+ static Piece null();
+
+ Piece();
+
+ explicit Piece(IntType i);
+
+ bool operator==(const Piece& piece) const;
+
+ bool operator!=(const Piece& piece) const;
+
+ bool is_null() const;
+
+ /** Return move as an integer between 0 and Piece::range */
+ IntType to_int() const;
+
+private:
+ static const IntType value_null = range - 1;
+
+ static const IntType value_uninitialized = range;
+
+ IntType m_i;
+
+ bool is_initialized() const;
+};
+
+inline Piece::Piece()
+{
+#if LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+inline Piece::Piece(IntType i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = i;
+}
+
+inline bool Piece::operator==(const Piece& piece) const
+{
+ return m_i == piece.m_i;
+}
+
+inline bool Piece::operator!=(const Piece& piece) const
+{
+ return ! operator==(piece);
+}
+
+inline bool Piece::is_initialized() const
+{
+ return m_i < value_uninitialized;
+}
+
+inline bool Piece::is_null() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i == value_null;
+}
+
+inline Piece Piece::null()
+{
+ return Piece(value_null);
+}
+
+inline auto Piece::to_int() const -> IntType
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_BASE_PIECE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceInfo.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PieceInfo.h"
+
+#include <algorithm>
+#include "libboardgame_base/GeometryUtil.h"
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_util/Log.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::geometry_util::normalize_offset;
+using libboardgame_base::geometry_util::type_match_shift;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const bool log_piece_creation = false;
+
+struct NormalizedPoints
+{
+ /** The normalized points of the transformed piece.
+ The points were shifted using GeometryUtil::normalize_offset(). */
+ PiecePoints points;
+
+ /** The point type of (0,0) in the normalized points. */
+ unsigned point_type;
+
+ bool operator==(const NormalizedPoints& n) const
+ {
+ return points == n.points && point_type == n.point_type;
+ }
+};
+
+#if LIBBOARDGAME_DEBUG
+/** Check consistency of transformations.
+ Checks that the point list (which must be already sorted) has no
+ duplicates. */
+bool check_consistency(const PiecePoints& points)
+{
+ for (unsigned i = 0; i < points.size(); ++i)
+ if (i > 0 && points[i] == points[i - 1])
+ return false;
+ return true;
+}
+#endif // LIBBOARDGAME_DEBUG
+
+/** Bring piece points into a normal form that is constant under translation. */
+NormalizedPoints normalize(const PiecePoints& points, unsigned point_type,
+ const Geometry& geo)
+{
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("Points ", points);
+ NormalizedPoints normalized;
+ normalized.points = points;
+ type_match_shift(geo, normalized.points.begin(),
+ normalized.points.end(), point_type);
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("Point type ", point_type, ", type match shift ",
+ normalized.points);
+ // Make the coordinates positive and minimal
+ unsigned width; // unused
+ unsigned height; // unused
+ CoordPoint offset;
+ normalize_offset(normalized.points.begin(), normalized.points.end(),
+ width, height, offset);
+ normalized.point_type = geo.get_point_type(offset);
+ // Sort the coordinates
+ sort(normalized.points.begin(), normalized.points.end());
+ return normalized;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+PieceInfo::PieceInfo(const string& name, const PiecePoints& points,
+ const Geometry& geo, const PieceTransforms& transforms,
+ PieceSet piece_set, const CoordPoint& label_pos,
+ unsigned nu_instances)
+ : m_nu_instances(nu_instances),
+ m_points(points),
+ m_label_pos(label_pos),
+ m_transforms(&transforms),
+ m_name(name)
+{
+ LIBBOARDGAME_ASSERT(nu_instances > 0);
+ LIBBOARDGAME_ASSERT(nu_instances <= PieceInfo::max_instances);
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("Creating transformations for piece ", name, ' ',
+ points);
+ vector<NormalizedPoints> all_transformed_points;
+ PiecePoints transformed_points;
+ for (const Transform* transform : transforms.get_all())
+ {
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("Transformation ", typeid(*transform).name());
+ transformed_points = points;
+ transform->transform(transformed_points.begin(),
+ transformed_points.end());
+ NormalizedPoints normalized = normalize(transformed_points,
+ transform->get_new_point_type(),
+ geo);
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("Normalized ", normalized.points, " point type ",
+ normalized.point_type);
+ LIBBOARDGAME_ASSERT(check_consistency(normalized.points));
+ auto begin = all_transformed_points.begin();
+ auto end = all_transformed_points.end();
+ auto pos = find(begin, end, normalized);
+ if (pos != end)
+ {
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("Equivalent to ", pos - begin);
+ m_equivalent_transform[transform]
+ = transforms.get_all()[pos - begin];
+ }
+ else
+ {
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("New (", m_uniq_transforms.size(), ")");
+ m_equivalent_transform[transform] = transform;
+ m_uniq_transforms.push_back(transform);
+ }
+ all_transformed_points.push_back(normalized);
+ };
+ if (piece_set == PieceSet::nexos)
+ {
+ m_score_points = 0;
+ for (auto& p : points)
+ {
+ auto point_type = geo.get_point_type(p);
+ LIBBOARDGAME_ASSERT(point_type <= 2);
+ if (point_type == 1 || point_type == 2) // Line segment
+ ++m_score_points;
+ }
+ }
+ else if (points.size() == 1 && piece_set == PieceSet::callisto)
+ m_score_points = 0;
+ else
+ m_score_points = static_cast<ScoreType>(points.size());
+}
+
+bool PieceInfo::can_flip_horizontally(const Transform* transform) const
+{
+ transform = get_equivalent_transform(transform);
+ auto flip = get_equivalent_transform(
+ m_transforms->get_mirrored_horizontally(transform));
+ return flip != transform;
+}
+
+bool PieceInfo::can_flip_vertically(const Transform* transform) const
+{
+ transform = get_equivalent_transform(transform);
+ auto flip = get_equivalent_transform(
+ m_transforms->get_mirrored_vertically(transform));
+ return flip != transform;
+}
+
+bool PieceInfo::can_rotate() const
+{
+ auto transform = m_uniq_transforms[0];
+ auto rotate = get_equivalent_transform(
+ m_transforms->get_rotated_clockwise(transform));
+ return rotate != transform;
+}
+
+const Transform* PieceInfo::find_transform(const Geometry& geo,
+ const Points& points) const
+{
+ NormalizedPoints normalized =
+ normalize(points, geo.get_point_type(0, 0), geo);
+ for (const Transform* transform : get_transforms())
+ {
+ Points piece_points = get_points();
+ transform->transform(piece_points.begin(), piece_points.end());
+ NormalizedPoints normalized_piece =
+ normalize(piece_points, transform->get_new_point_type(), geo);
+ if (normalized_piece == normalized)
+ return transform;
+ }
+ return nullptr;
+}
+
+const Transform* PieceInfo::get_equivalent_transform(
+ const Transform* transform) const
+{
+ auto pos = m_equivalent_transform.find(transform);
+ LIBBOARDGAME_ASSERT(pos != m_equivalent_transform.end());
+ return pos->second;
+}
+
+const Transform* PieceInfo::get_next_transform(const Transform* transform) const
+{
+ transform = get_equivalent_transform(transform);
+ auto begin = m_uniq_transforms.begin();
+ auto end = m_uniq_transforms.end();
+ auto pos = find(begin, end, transform);
+ LIBBOARDGAME_ASSERT(pos != end);
+ if (pos + 1 == end)
+ return *begin;
+ else
+ return *(pos + 1);
+}
+
+const Transform* PieceInfo::get_previous_transform(
+ const Transform* transform) const
+{
+ transform = get_equivalent_transform(transform);
+ auto begin = m_uniq_transforms.begin();
+ auto end = m_uniq_transforms.end();
+ auto pos = find(begin, end, transform);
+ LIBBOARDGAME_ASSERT(pos != end);
+ if (pos == begin)
+ return *(end - 1);
+ else
+ return *(pos - 1);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceInfo.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PIECE_INFO_H
+#define LIBPENTOBI_BASE_PIECE_INFO_H
+
+#include <map>
+#include <string>
+#include <vector>
+#include "Geometry.h"
+#include "PieceTransforms.h"
+#include "Variant.h"
+#include "libboardgame_base/CoordPoint.h"
+#include "libboardgame_base/Transform.h"
+#include "libboardgame_util/ArrayList.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::CoordPoint;
+using libboardgame_base::Transform;
+using libboardgame_util::ArrayList;
+
+//-----------------------------------------------------------------------------
+
+typedef float ScoreType;
+
+//-----------------------------------------------------------------------------
+
+class PieceInfo
+{
+public:
+ /** Maximum number of points in a piece.
+ The maximum piece size occurs with the I4 piece in Nexos (4 real points
+ and 3 junction points, see get_points()). */
+ static const unsigned max_size = 7;
+
+ /** Maximum number of scored points in a piece.
+ This excludes junction points in Nexos. The maximum number of scored
+ points occurs in Trigon. */
+ static const unsigned max_scored_size = 6;
+
+ /** Maximum number of instances of a piece per player. */
+ static const unsigned max_instances = 3;
+
+ typedef ArrayList<CoordPoint, max_size> Points;
+
+
+ /** Constructor.
+ @param name A short unique name for the piece.
+ @param points The coordinates of the piece elements.
+ @param geo
+ @param transforms
+ @param piece_set
+ @param label_pos The coordinates for drawing a label on the piece.
+ @param nu_instances The number of instances of the piece per player. */
+ PieceInfo(const string& name, const Points& points,
+ const Geometry& geo, const PieceTransforms& transforms,
+ PieceSet piece_set, const CoordPoint& label_pos,
+ unsigned nu_instances = 1);
+
+ const string& get_name() const { return m_name; }
+
+ /** The points of the piece.
+ In Nexos, the points of a piece contain the coordinates of line
+ segments and of junctions that are essentially needed to mark the
+ intersection as non-crossable (i.e. junctions that touch exactly two
+ line segments of the piece with identical orientation. */
+ const Points& get_points() const { return m_points; }
+
+ const CoordPoint& get_label_pos() const { return m_label_pos; }
+
+ /** Return the number of points of the piece that contribute to the score.
+ This excludes any junction points included in the piece definition in
+ Nexos.*/
+ ScoreType get_score_points() const { return m_score_points; }
+
+ unsigned get_nu_instances() const { return m_nu_instances; }
+
+ /** Get a list with unique transformations.
+ The list has the same order as PieceTransforms::get_all() but
+ transformations that are equivalent to a previous transformation
+ (because of a symmetry of the piece) are omitted. */
+ const vector<const Transform*>& get_transforms() const
+ {
+ return m_uniq_transforms;
+ }
+
+ /** Get next transform from the list of unique transforms. */
+ const Transform* get_next_transform(const Transform* transform) const;
+
+ /** Get previous transform from the list of unique transforms. */
+ const Transform* get_previous_transform(const Transform* transform) const;
+
+ /** Get the transform from the list of unique transforms that is equivalent
+ to a given transform. */
+ const Transform* get_equivalent_transform(const Transform* transform) const;
+
+ bool can_rotate() const;
+
+ bool can_flip_horizontally(const Transform* transform) const;
+
+ bool can_flip_vertically(const Transform* transform) const;
+
+ const Transform* find_transform(const Geometry& geo,
+ const Points& points) const;
+
+private:
+ unsigned m_nu_instances;
+
+ Points m_points;
+
+ CoordPoint m_label_pos;
+
+ ScoreType m_score_points;
+
+ const PieceTransforms* m_transforms;
+
+ string m_name;
+
+ vector<const Transform*> m_uniq_transforms;
+
+ map<const Transform*,const Transform*> m_equivalent_transform;
+};
+
+//-----------------------------------------------------------------------------
+
+typedef PieceInfo::Points PiecePoints;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PIECE_INFO_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceMap.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PIECE_MAP_H
+#define LIBPENTOBI_BASE_PIECE_MAP_H
+
+#include <array>
+#include <algorithm>
+#include "Piece.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Container mapping a unique piece to another element type.
+ The elements must be default-constructible. */
+template<typename T>
+class PieceMap
+{
+public:
+ PieceMap() = default;
+
+ explicit PieceMap(const T& val);
+
+ PieceMap& operator=(const PieceMap& piece_map);
+
+ bool operator==(const PieceMap& piece_map) const;
+
+ T& operator[](Piece piece);
+
+ const T& operator[](Piece piece) const;
+
+ void fill(const T& val);
+
+private:
+ array<T, Piece::range_not_null> m_a;
+};
+
+template<typename T>
+inline PieceMap<T>::PieceMap(const T& val)
+{
+ fill(val);
+}
+
+template<typename T>
+PieceMap<T>& PieceMap<T>::operator=(const PieceMap& piece_map)
+{
+ copy(piece_map.m_a.begin(), piece_map.m_a.end(), m_a.begin());
+ return *this;
+}
+
+template<typename T>
+bool PieceMap<T>::operator==(const PieceMap& piece_map) const
+{
+ return equal(m_a.begin(), m_a.end(), piece_map.m_a.begin());
+}
+
+template<typename T>
+inline T& PieceMap<T>::operator[](Piece piece)
+{
+ LIBBOARDGAME_ASSERT(! piece.is_null());
+ return m_a[piece.to_int()];
+}
+
+template<typename T>
+inline const T& PieceMap<T>::operator[](Piece piece) const
+{
+ LIBBOARDGAME_ASSERT(! piece.is_null());
+ return m_a[piece.to_int()];
+}
+
+template<typename T>
+void PieceMap<T>::fill(const T& val)
+{
+ m_a.fill(val);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PIECE_MAP_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransforms.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PieceTransforms.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+PieceTransforms::~PieceTransforms() = default;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransforms.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_PIECE_TRANSFORMS_H
+#define LIBPENTOBI_PIECE_TRANSFORMS_H
+
+#include <vector>
+#include "libboardgame_base/Transform.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::Transform;
+
+//-----------------------------------------------------------------------------
+
+class PieceTransforms
+{
+public:
+ virtual ~PieceTransforms();
+
+ virtual const Transform* get_mirrored_horizontally(
+ const Transform* transf) const = 0;
+
+ virtual const Transform* get_mirrored_vertically(
+ const Transform* transf) const = 0;
+
+ virtual const Transform* get_rotated_anticlockwise(
+ const Transform* transf) const = 0;
+
+ virtual const Transform* get_rotated_clockwise(
+ const Transform* transf) const = 0;
+
+ virtual const Transform* get_default() const;
+
+ const vector<const Transform*>& get_all() const;
+
+ /** Find the transform by its class.
+ @tparam T The class of the transform.
+ @return The pointer to the transform or null if the transforms do not
+ contain the instance of the given class. */
+ template<class T>
+ const Transform* find() const;
+
+protected:
+ /** All piece transformations.
+ Must be initialized in constructor of subclass. */
+ vector<const Transform*> m_all;
+};
+
+template<class T>
+const Transform* PieceTransforms::find() const
+{
+ for (auto t : m_all)
+ if (dynamic_cast<const T*>(t))
+ return t;
+ return nullptr;
+}
+
+inline const Transform* PieceTransforms::get_default() const
+{
+ return m_all[0];
+}
+
+inline const vector<const Transform*>& PieceTransforms::get_all() const
+{
+ return m_all;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_PIECE_TRANSFORMS_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsClassic.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PieceTransformsClassic.h"
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+PieceTransformsClassic::PieceTransformsClassic()
+{
+ m_all.reserve(8);
+ m_all.push_back(&m_identity);
+ m_all.push_back(&m_rot90);
+ m_all.push_back(&m_rot180);
+ m_all.push_back(&m_rot270);
+ m_all.push_back(&m_refl);
+ m_all.push_back(&m_rot90refl);
+ m_all.push_back(&m_rot180refl);
+ m_all.push_back(&m_rot270refl);
+}
+
+const Transform* PieceTransformsClassic::get_mirrored_horizontally(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_refl;
+ else if (transf == &m_rot90)
+ result = &m_rot270refl;
+ else if (transf == &m_rot180)
+ result = &m_rot180refl;
+ else if (transf == &m_rot270)
+ result = &m_rot90refl;
+ else if (transf == &m_refl)
+ result = &m_identity;
+ else if (transf == &m_rot90refl)
+ result = &m_rot270;
+ else if (transf == &m_rot180refl)
+ result = &m_rot180;
+ else if (transf == &m_rot270refl)
+ result = &m_rot90;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+const Transform* PieceTransformsClassic::get_mirrored_vertically(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_rot180refl;
+ else if (transf == &m_rot90)
+ result = &m_rot90refl;
+ else if (transf == &m_rot180)
+ result = &m_refl;
+ else if (transf == &m_rot270)
+ result = &m_rot270refl;
+ else if (transf == &m_refl)
+ result = &m_rot180;
+ else if (transf == &m_rot90refl)
+ result = &m_rot90;
+ else if (transf == &m_rot180refl)
+ result = &m_identity;
+ else if (transf == &m_rot270refl)
+ result = &m_rot270;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+const Transform* PieceTransformsClassic::get_rotated_anticlockwise(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_rot270;
+ else if (transf == &m_rot90)
+ result = &m_identity;
+ else if (transf == &m_rot180)
+ result = &m_rot90;
+ else if (transf == &m_rot270)
+ result = &m_rot180;
+ else if (transf == &m_refl)
+ result = &m_rot270refl;
+ else if (transf == &m_rot90refl)
+ result = &m_refl;
+ else if (transf == &m_rot180refl)
+ result = &m_rot90refl;
+ else if (transf == &m_rot270refl)
+ result = &m_rot180refl;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+const Transform* PieceTransformsClassic::get_rotated_clockwise(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_rot90;
+ else if (transf == &m_rot90)
+ result = &m_rot180;
+ else if (transf == &m_rot180)
+ result = &m_rot270;
+ else if (transf == &m_rot270)
+ result = &m_identity;
+ else if (transf == &m_refl)
+ result = &m_rot90refl;
+ else if (transf == &m_rot90refl)
+ result = &m_rot180refl;
+ else if (transf == &m_rot180refl)
+ result = &m_rot270refl;
+ else if (transf == &m_rot270refl)
+ result = &m_refl;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsClassic.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H
+#define LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H
+
+#include "PieceTransforms.h"
+#include "libboardgame_base/RectTransform.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::TransfIdentity;
+using libboardgame_base::TransfRectRot90;
+using libboardgame_base::TransfRectRot180;
+using libboardgame_base::TransfRectRot270;
+using libboardgame_base::TransfRectRefl;
+using libboardgame_base::TransfRectRot90Refl;
+using libboardgame_base::TransfRectRot180Refl;
+using libboardgame_base::TransfRectRot270Refl;
+
+//-----------------------------------------------------------------------------
+
+class PieceTransformsClassic
+ : public PieceTransforms
+{
+public:
+ PieceTransformsClassic();
+
+ const Transform* get_mirrored_horizontally(
+ const Transform* transf) const override;
+
+ const Transform* get_mirrored_vertically(
+ const Transform* transf) const override;
+
+ const Transform* get_rotated_anticlockwise(
+ const Transform* transf) const override;
+
+ const Transform* get_rotated_clockwise(
+ const Transform* transf) const override;
+
+private:
+ TransfIdentity m_identity;
+
+ TransfRectRot90 m_rot90;
+
+ TransfRectRot180 m_rot180;
+
+ TransfRectRot270 m_rot270;
+
+ TransfRectRefl m_refl;
+
+ TransfRectRot90Refl m_rot90refl;
+
+ TransfRectRot180Refl m_rot180refl;
+
+ TransfRectRot270Refl m_rot270refl;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_PIECE_TRANSFORMS_CLASSIC_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsTrigon.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PieceTransformsTrigon.h"
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+PieceTransformsTrigon::PieceTransformsTrigon()
+{
+ m_all.reserve(12);
+ m_all.push_back(&m_identity);
+ m_all.push_back(&m_rot60);
+ m_all.push_back(&m_rot120);
+ m_all.push_back(&m_rot180);
+ m_all.push_back(&m_rot240);
+ m_all.push_back(&m_rot300);
+ m_all.push_back(&m_refl);
+ m_all.push_back(&m_refl_rot60);
+ m_all.push_back(&m_refl_rot120);
+ m_all.push_back(&m_refl_rot180);
+ m_all.push_back(&m_refl_rot240);
+ m_all.push_back(&m_refl_rot300);
+}
+
+const Transform* PieceTransformsTrigon::get_default() const
+{
+ return &m_identity;
+}
+
+const Transform* PieceTransformsTrigon::get_mirrored_horizontally(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_refl;
+ else if (transf == &m_rot60)
+ result = &m_refl_rot300;
+ else if (transf == &m_rot120)
+ result = &m_refl_rot240;
+ else if (transf == &m_rot180)
+ result = &m_refl_rot180;
+ else if (transf == &m_rot240)
+ result = &m_refl_rot120;
+ else if (transf == &m_rot300)
+ result = &m_refl_rot60;
+ else if (transf == &m_refl)
+ result = &m_identity;
+ else if (transf == &m_refl_rot60)
+ result = &m_rot300;
+ else if (transf == &m_refl_rot120)
+ result = &m_rot240;
+ else if (transf == &m_refl_rot180)
+ result = &m_rot180;
+ else if (transf == &m_refl_rot240)
+ result = &m_rot120;
+ else if (transf == &m_refl_rot300)
+ result = &m_rot60;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+const Transform* PieceTransformsTrigon::get_mirrored_vertically(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_refl_rot180;
+ else if (transf == &m_rot60)
+ result = &m_refl_rot120;
+ else if (transf == &m_rot120)
+ result = &m_refl_rot60;
+ else if (transf == &m_rot180)
+ result = &m_refl;
+ else if (transf == &m_rot240)
+ result = &m_refl_rot300;
+ else if (transf == &m_rot300)
+ result = &m_refl_rot240;
+ else if (transf == &m_refl)
+ result = &m_rot180;
+ else if (transf == &m_refl_rot60)
+ result = &m_rot120;
+ else if (transf == &m_refl_rot120)
+ result = &m_rot60;
+ else if (transf == &m_refl_rot180)
+ result = &m_identity;
+ else if (transf == &m_refl_rot240)
+ result = &m_rot300;
+ else if (transf == &m_refl_rot300)
+ result = &m_rot240;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+const Transform* PieceTransformsTrigon::get_rotated_anticlockwise(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_rot300;
+ else if (transf == &m_rot60)
+ result = &m_identity;
+ else if (transf == &m_rot120)
+ result = &m_rot60;
+ else if (transf == &m_rot180)
+ result = &m_rot120;
+ else if (transf == &m_rot240)
+ result = &m_rot180;
+ else if (transf == &m_rot300)
+ result = &m_rot240;
+ else if (transf == &m_refl)
+ result = &m_refl_rot300;
+ else if (transf == &m_refl_rot60)
+ result = &m_refl;
+ else if (transf == &m_refl_rot120)
+ result = &m_refl_rot60;
+ else if (transf == &m_refl_rot180)
+ result = &m_refl_rot120;
+ else if (transf == &m_refl_rot240)
+ result = &m_refl_rot180;
+ else if (transf == &m_refl_rot300)
+ result = &m_refl_rot240;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+const Transform* PieceTransformsTrigon::get_rotated_clockwise(
+ const Transform* transf) const
+{
+ const Transform* result;
+ if (transf == &m_identity)
+ result = &m_rot60;
+ else if (transf == &m_rot60)
+ result = &m_rot120;
+ else if (transf == &m_rot120)
+ result = &m_rot180;
+ else if (transf == &m_rot180)
+ result = &m_rot240;
+ else if (transf == &m_rot240)
+ result = &m_rot300;
+ else if (transf == &m_rot300)
+ result = &m_identity;
+ else if (transf == &m_refl)
+ result = &m_refl_rot60;
+ else if (transf == &m_refl_rot60)
+ result = &m_refl_rot120;
+ else if (transf == &m_refl_rot120)
+ result = &m_refl_rot180;
+ else if (transf == &m_refl_rot180)
+ result = &m_refl_rot240;
+ else if (transf == &m_refl_rot240)
+ result = &m_refl_rot300;
+ else if (transf == &m_refl_rot300)
+ result = &m_refl;
+ else
+ {
+ LIBBOARDGAME_ASSERT(false);
+ result = nullptr;
+ }
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsTrigon.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H
+#define LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H
+
+#include "PieceTransforms.h"
+#include "TrigonTransform.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class PieceTransformsTrigon
+ : public PieceTransforms
+{
+public:
+ PieceTransformsTrigon();
+
+ const Transform* get_mirrored_horizontally(
+ const Transform* transf) const override;
+
+ const Transform* get_mirrored_vertically(
+ const Transform* transf) const override;
+
+ const Transform* get_rotated_anticlockwise(
+ const Transform* transf) const override;
+
+ const Transform* get_rotated_clockwise(
+ const Transform* transf) const override;
+
+ const Transform* get_default() const override;
+
+private:
+ TransfTrigonIdentity m_identity;
+
+ TransfTrigonRot60 m_rot60;
+
+ TransfTrigonRot120 m_rot120;
+
+ TransfTrigonRot180 m_rot180;
+
+ TransfTrigonRot240 m_rot240;
+
+ TransfTrigonRot300 m_rot300;
+
+ TransfTrigonRefl m_refl;
+
+ TransfTrigonReflRot60 m_refl_rot60;
+
+ TransfTrigonReflRot120 m_refl_rot120;
+
+ TransfTrigonReflRot180 m_refl_rot180;
+
+ TransfTrigonReflRot240 m_refl_rot240;
+
+ TransfTrigonReflRot300 m_refl_rot300;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_PIECE_TRANSFORMS_TRIGON_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PlayerBase.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PlayerBase.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+PlayerBase::~PlayerBase()
+{
+}
+
+bool PlayerBase::resign() const
+{
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PlayerBase.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PLAYER_BASE_H
+#define LIBPENTOBI_BASE_PLAYER_BASE_H
+
+#include "Board.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class PlayerBase
+{
+public:
+ virtual ~PlayerBase();
+
+ virtual Move genmove(const Board& bd, Color c) = 0;
+
+ /** Check if the player wants to resign.
+ This may only be called after a genmove() and returns true if the
+ player wants to resign in the position at the last genmove().
+ The default implementation returns false. */
+ virtual bool resign() const;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PLAYER_BASE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Point.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_POINT_H
+#define LIBPENTOBI_BASE_POINT_H
+
+#include "libboardgame_base/Point.h"
+
+//-----------------------------------------------------------------------------
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Point (coordinate of on-board field) for Blokus game variants.
+ Supports RectGeometry up to size 20, TrigonGeometry up to edge size 9,
+ and NexosGeometry up to size 13. */
+typedef libboardgame_base::Point<486, 35, 25, unsigned short> Point;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PointList.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_POINT_LIST_H
+#define LIBPENTOBI_BASE_POINT_LIST_H
+
+#include "Point.h"
+#include "libboardgame_util/ArrayList.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+typedef ArrayList<Point, Point::max_onboard> PointList;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINT_LIST_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PointState.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PointState.h"
+
+#include <iostream>
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, const PointState& s)
+{
+ if (s.is_color())
+ out << s.to_color();
+ else
+ out << 'E';
+ return out;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PointState.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_POINTSTATE_H
+#define LIBPENTOBI_BASE_POINTSTATE_H
+
+#include "Color.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** State of an on-board point, which can be a color or empty */
+class PointState
+{
+public:
+ typedef Color::IntType IntType;
+
+ static const IntType range = Color::range + 1;
+
+ static const IntType value_empty = range - 1;
+
+
+ PointState();
+
+ explicit PointState(Color c);
+
+ explicit PointState(IntType i);
+
+ bool operator==(const PointState& s) const;
+
+ bool operator!=(const PointState& s) const;
+
+ bool operator==(const Color& c) const;
+
+ bool operator!=(const Color& c) const;
+
+ IntType to_int() const;
+
+ static PointState empty();
+
+ bool is_empty() const;
+
+ bool is_color() const;
+
+ Color to_color() const;
+
+private:
+ static const IntType value_uninitialized = range;
+
+ IntType m_i;
+
+ bool is_initialized() const;
+};
+
+
+inline PointState::PointState()
+{
+#if LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+inline PointState::PointState(Color c)
+{
+ m_i = c.to_int();
+}
+
+inline PointState::PointState(IntType i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = i;
+}
+
+inline bool PointState::operator==(const PointState& p) const
+{
+ return m_i == p.m_i;
+}
+
+inline bool PointState::operator==(const Color& c) const
+{
+ return m_i == c.to_int();
+}
+
+inline bool PointState::operator!=(const PointState& s) const
+{
+ return ! operator==(s);
+}
+
+inline bool PointState::operator!=(const Color& c) const
+{
+ return ! operator==(c);
+}
+
+inline PointState PointState::empty()
+{
+ return PointState(value_empty);
+}
+
+inline bool PointState::is_initialized() const
+{
+ return m_i < value_uninitialized;
+}
+
+inline bool PointState::is_color() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i != value_empty;
+}
+
+inline bool PointState::is_empty() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i == value_empty;
+}
+
+inline Color PointState::to_color() const
+{
+ LIBBOARDGAME_ASSERT(is_color());
+ return Color(m_i);
+}
+
+inline PointState::IntType PointState::to_int() const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ return m_i;
+}
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, const PointState& s);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINTSTATE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PrecompMoves.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PRECOMP_MOVES_H
+#define LIBPENTOBI_BASE_PRECOMP_MOVES_H
+
+#include "Grid.h"
+#include "Move.h"
+#include "PieceMap.h"
+#include "Point.h"
+#include "libboardgame_util/Range.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Precomputed moves for fast move generation.
+ Compact storage of precomputed lists with local moves. Each list contains
+ all moves that include a given point constrained by the piece type and the
+ forbidden status of adjacant points. This drastically reduces the number of
+ moves that need to be checked for legality during move generation.
+ @see Board::get_adj_status() */
+class PrecompMoves
+{
+public:
+ /** The number of neighbors used for computing the adjacent status.
+ The adjacent status is a single number that encodes the forbidden
+ status of the first adj_status_nu_adj neighbors (from the list
+ Geometry::get_adj() concatenated with Geometry::get_diag()). It is used
+ for speeding up the matching of moves at a given point. Increasing this
+ number will make the precomputed lists shorter but exponentially
+ increase the number of lists and the total memory used for all lists.
+ Therefore, the optimal value for speeding up the matching depends on
+ the CPU cache size. */
+#if PENTOBI_LOW_RESOURCES
+ static const unsigned adj_status_nu_adj = 5;
+#else
+ static const unsigned adj_status_nu_adj = 6;
+#endif
+
+ /** The maximum sum of the sizes of all precomputed move lists in any
+ game variant. */
+ static const unsigned max_move_lists_sum_length =
+ adj_status_nu_adj == 4 ?
+ 832444 : adj_status_nu_adj == 5 ? 1425934 : 2769060;
+ static_assert(adj_status_nu_adj >= 4 && adj_status_nu_adj <= 6, "");
+
+ /** The range of values for the adjacent status. */
+ static const unsigned nu_adj_status = 1 << adj_status_nu_adj;
+
+ /** Begin/end range for lists with moves at a given point. */
+ typedef libboardgame_util::Range<const Move> Range;
+
+
+ /** Add a move to list during construction. */
+ void set_move(unsigned i, Move mv)
+ {
+ LIBBOARDGAME_ASSERT(i < max_move_lists_sum_length);
+ m_move_lists[i] = mv;
+ }
+
+ /** Store beginning and end of a local move list duing construction. */
+ void set_list_range(Point p, unsigned adj_status, Piece piece,
+ unsigned begin, unsigned size)
+ {
+ m_moves_range[p][adj_status][piece] = CompressedRange(begin, size);
+ }
+
+ /** Get all moves of a piece at a point constrained by the forbidden
+ status of adjacent points. */
+ Range get_moves(Piece piece, Point p, unsigned adj_status = 0) const
+ {
+ auto& range = m_moves_range[p][adj_status][piece];
+ auto begin = move_lists_begin() + range.begin();
+ auto end = begin + range.size();
+ return Range(begin, end);
+ }
+
+ bool has_moves(Piece piece, Point p, unsigned adj_status) const
+ {
+ return ! m_moves_range[p][adj_status][piece].empty();
+ }
+
+ /** Begin of storage for move lists.
+ Only needed for special use cases like during an in-place construction
+ of PrecompMoves for follow-up positions when we need to compare the
+ index of old iterators with the current get_size() to ensure that
+ we don't overwrite any old content that we still need to read
+ during the construction. */
+ const Move* move_lists_begin() const { return &(*m_move_lists.begin()); }
+
+private:
+ class CompressedRange
+ {
+ public:
+ CompressedRange() = default;
+
+ CompressedRange(unsigned begin, unsigned size)
+ {
+ LIBBOARDGAME_ASSERT(begin < max_move_lists_sum_length);
+ LIBBOARDGAME_ASSERT(begin + size <= max_move_lists_sum_length);
+ static_assert(max_move_lists_sum_length < (1 << 24), "");
+ LIBBOARDGAME_ASSERT(size < (1 << 8));
+ m_val = size;
+ if (size != 0)
+ m_val |= (begin << 8);
+ }
+
+ bool empty() const { return m_val == 0; }
+
+ unsigned begin() const { return m_val >> 8; }
+
+ unsigned size() const { return m_val & 0xff; }
+
+ private:
+ uint_least32_t m_val;
+ };
+
+ /** See m_move_lists. */
+ Grid<array<PieceMap<CompressedRange>, nu_adj_status>> m_moves_range;
+
+ /** Compact representation of lists of moves of a piece at a point
+ constrained by the forbidden status of adjacent points.
+ All lists are stored in a single array; m_moves_range contains
+ information about the actual begin/end indices. */
+ array<Move, max_move_lists_sum_length> m_move_lists;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PRECOMP_MOVES_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/ScoreUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_SCORE_UTIL_H
+#define LIBPENTOBI_BASE_SCORE_UTIL_H
+
+#include <algorithm>
+#include <array>
+#include "Color.h"
+#include "PieceInfo.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Convert the result of a multi-player game into a comparable number.
+ This generalizes the game result of a two-player game (0,0.5,1 for
+ loss/tie/win) for a game with n \> 2 players. The points are sorted in
+ ascending order. Each rank r_i (i in 0..n-1) is assigned a value of
+ r_i/(n-1). If multiple players have the same points, the result value is
+ the average of all ranks with these points. So being the single winner
+ still gives the result 1 and being the single loser the result 0. Being the
+ single winner is better than sharing the best rank, which is better than
+ getting the second rank, etc.
+ @return The game result for each player. */
+template<typename FLOAT>
+void get_multiplayer_result(unsigned nu_players,
+ const array<ScoreType, Color::range>& points,
+ array<FLOAT, Color::range>& result,
+ bool break_ties)
+{
+ array<ScoreType, Color::range> adjusted, sorted;
+ for (Color::IntType i = 0; i < nu_players; ++i)
+ {
+ adjusted[i] = points[i];
+ if (break_ties)
+ // Favor later player. The adjustment must be smaller than the
+ // smallest difference in points (0.5 for GembloQ).
+ adjusted[i] += 0.001f * i;
+ sorted[i] = adjusted[i];
+ }
+ sort(sorted.begin(), sorted.begin() + nu_players);
+ for (Color::IntType i = 0; i < nu_players; ++i)
+ {
+ FLOAT sum = 0;
+ FLOAT n = 0;
+ FLOAT float_j = 0;
+ FLOAT factor = 1 / FLOAT(nu_players - 1);
+ for (unsigned j = 0; j < nu_players; ++j, ++float_j)
+ if (sorted[j] == adjusted[i])
+ {
+ sum += factor * float_j;
+ ++n;
+ }
+ result[i] = sum / n;
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_SCORE_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Setup.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_SETUP_H
+#define LIBPENTOBI_BASE_SETUP_H
+
+#include "ColorMap.h"
+#include "Move.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Definition of a setup position.
+ A setup position consists of a number of pieces that are placed at once
+ (in no particular order) on the board and a color to play next. */
+struct Setup
+{
+ /** Maximum number of pieces on board per color. */
+ static const unsigned max_pieces = 24;
+
+ typedef ArrayList<Move,max_pieces> PlacementList;
+
+ Color to_play = Color(0);
+
+ ColorMap<PlacementList> placements;
+
+ void clear();
+};
+
+inline void Setup::clear()
+{
+ to_play = Color(0);
+ for_each_color([&](Color c) { placements[c].clear(); });
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_SETUP_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/StartingPoints.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "StartingPoints.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+void StartingPoints::add_colored_starting_point(const Geometry& geo,
+ unsigned x, unsigned y,
+ Color c)
+{
+ Point p = geo.get_point(x, y);
+ m_is_colored_starting_point[p] = true;
+ m_starting_point_color[p] = c;
+ m_starting_points[c].push_back(p);
+}
+
+void StartingPoints::add_colorless_starting_point(const Geometry& geo,
+ unsigned x, unsigned y)
+{
+ Point p = geo.get_point(x, y);
+ m_is_colorless_starting_point[p] = true;
+ for_each_color([&](Color c) {
+ m_starting_points[c].push_back(p);
+ });
+}
+
+void StartingPoints::init(Variant variant, const Geometry& geo)
+{
+ m_is_colored_starting_point.fill(false, geo);
+ m_is_colorless_starting_point.fill(false, geo);
+ for_each_color([&](Color c) {
+ m_starting_points[c].clear();
+ });
+ switch (get_board_type(variant))
+ {
+ case BoardType::classic:
+ add_colored_starting_point(geo, 0, 0, Color(0));
+ add_colored_starting_point(geo, 19, 0, Color(1));
+ add_colored_starting_point(geo, 19, 19, Color(2));
+ add_colored_starting_point(geo, 0, 19, Color(3));
+ break;
+ case BoardType::duo:
+ add_colored_starting_point(geo, 4, 4, Color(0));
+ add_colored_starting_point(geo, 9, 9, Color(1));
+ break;
+ case BoardType::trigon:
+ add_colorless_starting_point(geo, 17, 3);
+ add_colorless_starting_point(geo, 17, 14);
+ add_colorless_starting_point(geo, 9, 6);
+ add_colorless_starting_point(geo, 9, 11);
+ add_colorless_starting_point(geo, 25, 6);
+ add_colorless_starting_point(geo, 25, 11);
+ break;
+ case BoardType::trigon_3:
+ add_colorless_starting_point(geo, 15, 2);
+ add_colorless_starting_point(geo, 15, 13);
+ add_colorless_starting_point(geo, 7, 5);
+ add_colorless_starting_point(geo, 7, 10);
+ add_colorless_starting_point(geo, 23, 5);
+ add_colorless_starting_point(geo, 23, 10);
+ break;
+ case BoardType::nexos:
+ add_colored_starting_point(geo, 4, 3, Color(0));
+ add_colored_starting_point(geo, 3, 4, Color(0));
+ add_colored_starting_point(geo, 5, 4, Color(0));
+ add_colored_starting_point(geo, 4, 5, Color(0));
+ add_colored_starting_point(geo, 20, 3, Color(1));
+ add_colored_starting_point(geo, 19, 4, Color(1));
+ add_colored_starting_point(geo, 21, 4, Color(1));
+ add_colored_starting_point(geo, 20, 5, Color(1));
+ add_colored_starting_point(geo, 20, 19, Color(2));
+ add_colored_starting_point(geo, 19, 20, Color(2));
+ add_colored_starting_point(geo, 21, 20, Color(2));
+ add_colored_starting_point(geo, 20, 21, Color(2));
+ add_colored_starting_point(geo, 4, 19, Color(3));
+ add_colored_starting_point(geo, 3, 20, Color(3));
+ add_colored_starting_point(geo, 5, 20, Color(3));
+ add_colored_starting_point(geo, 4, 21, Color(3));
+ break;
+ case BoardType::callisto:
+ case BoardType::callisto_2:
+ case BoardType::callisto_3:
+ break;
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/StartingPoints.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_STARTING_POINTS_H
+#define LIBPENTOBI_BASE_STARTING_POINTS_H
+
+#include "Color.h"
+#include "ColorMap.h"
+#include "Geometry.h"
+#include "Grid.h"
+#include "Variant.h"
+#include "libboardgame_util/ArrayList.h"
+
+namespace libpentobi_base {
+
+using libboardgame_util::ArrayList;
+
+//-----------------------------------------------------------------------------
+
+class StartingPoints
+{
+public:
+ static const unsigned max_starting_points = 16;
+
+ void init(Variant variant, const Geometry& geo);
+
+ bool is_colored_starting_point(Point p) const;
+
+ bool is_colorless_starting_point(Point p) const;
+
+ Color get_starting_point_color(Point p) const;
+
+ const ArrayList<Point,StartingPoints::max_starting_points>&
+ get_starting_points(Color c) const;
+
+private:
+ Grid<bool> m_is_colored_starting_point;
+
+ Grid<bool> m_is_colorless_starting_point;
+
+ Grid<Color> m_starting_point_color;
+
+ ColorMap<ArrayList<Point,max_starting_points>> m_starting_points;
+
+ void add_colored_starting_point(const Geometry& geo, unsigned x,
+ unsigned y, Color c);
+
+ void add_colorless_starting_point(const Geometry& geo, unsigned x,
+ unsigned y);
+};
+
+inline Color StartingPoints::get_starting_point_color(Point p) const
+{
+ LIBBOARDGAME_ASSERT(m_is_colored_starting_point[p]);
+ return m_starting_point_color[p];
+}
+
+inline const ArrayList<Point,StartingPoints::max_starting_points>&
+ StartingPoints::get_starting_points(Color c) const
+{
+ return m_starting_points[c];
+}
+
+inline bool StartingPoints::is_colored_starting_point(Point p) const
+{
+ return m_is_colored_starting_point[p];
+}
+
+inline bool StartingPoints::is_colorless_starting_point(Point p) const
+{
+ return m_is_colorless_starting_point[p];
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_STARTING_POINTS_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file SymmetricPoints.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "SymmetricPoints.h"
+
+#include "libboardgame_base/PointTransform.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::PointTransfRot180;
+
+//-----------------------------------------------------------------------------
+
+void SymmetricPoints::init(const Geometry& geo)
+{
+ PointTransfRot180<Point> transform;
+ for (Point p : geo)
+ m_symmetric_point[p] = transform.get_transformed(p, geo);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/SymmetricPoints.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_SYMMETRIC_POINTS_H
+#define LIBPENTOBI_BASE_SYMMETRIC_POINTS_H
+
+#include "Geometry.h"
+#include "Grid.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Lookup table to quickly get points that are symmetric with respect to the
+ center of the board. */
+class SymmetricPoints
+{
+public:
+ void init(const Geometry& geo);
+
+ Point operator[](Point p) const;
+
+private:
+ Grid<Point> m_symmetric_point;
+};
+
+inline Point SymmetricPoints::operator[](Point p) const
+{
+ return m_symmetric_point[p];
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_SYMMETRIC_POINTS_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TreeUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TreeUtil.h"
+
+#include "NodeUtil.h"
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libpentobi_base {
+namespace tree_util {
+
+using libboardgame_sgf::util::get_move_annotation;
+using libboardgame_sgf::util::get_variation_string;
+
+//-----------------------------------------------------------------------------
+
+unsigned get_move_number(const PentobiTree& tree, const SgfNode& node)
+{
+ unsigned move_number = 0;
+ auto current = &node;
+ while (current)
+ {
+ if (! tree.get_move_ignore_invalid(*current).is_null())
+ ++move_number;
+ if (libpentobi_base::node_util::has_setup(*current))
+ break;
+ current = current->get_parent_or_null();
+ }
+ return move_number;
+}
+
+unsigned get_moves_left(const PentobiTree& tree, const SgfNode& node)
+{
+ unsigned moves_left = 0;
+ auto current = node.get_first_child_or_null();
+ while (current)
+ {
+ if (libpentobi_base::node_util::has_setup(*current))
+ break;
+ if (! tree.get_move_ignore_invalid(*current).is_null())
+ ++moves_left;
+ current = current->get_first_child_or_null();
+ }
+ return moves_left;
+}
+
+string get_position_info(const PentobiTree& tree, const SgfNode& node)
+{
+ auto move = get_move_number(tree, node);
+ auto left = get_moves_left(tree, node);
+ auto total = move + left;
+ auto variation = get_variation_string(node);
+ auto annotation = get_move_annotation(tree, node);
+ ostringstream s;
+ if (left > 0 || move > 0)
+ s << move << annotation;
+ if (left > 0)
+ s << '/' << total;
+ if (! variation.empty())
+ s << " (" << variation << ')';
+ return s.str();
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace tree_util
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TreeUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_TREE_UTIL_H
+#define LIBPENTOBI_BASE_TREE_UTIL_H
+
+#include "PentobiTree.h"
+
+namespace libpentobi_base {
+namespace tree_util {
+
+//-----------------------------------------------------------------------------
+
+/** Get the move number at a node.
+ Counts the number of moves since the root node or the last node
+ that contained setup properties. Invalid moves are ignored. */
+unsigned get_move_number(const PentobiTree& tree, const SgfNode& node);
+
+/** Get the number of remaining moves in the current variation.
+ Counts the number of moves remaining in the current variation
+ until the end of the variation or the next node that contains setup
+ properties. Invalid moves are ignored. */
+unsigned get_moves_left(const PentobiTree& tree, const SgfNode& node);
+
+/** Return a single line that describes the location of the current move
+ in the tree.
+ Includes the move number, move annotationm symbols, the total number of
+ moves in this variation, and a string describing the variation. */
+string get_position_info(const PentobiTree& tree, const SgfNode& node);
+
+//-----------------------------------------------------------------------------
+
+} // namespace tree_util
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_TREE_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TrigonGeometry.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TrigonGeometry.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+map<unsigned, shared_ptr<TrigonGeometry>> TrigonGeometry::s_geometry;
+
+TrigonGeometry::TrigonGeometry(unsigned sz)
+{
+ m_sz = sz;
+ Geometry::init(sz * 4 - 1, sz * 2);
+}
+
+const TrigonGeometry& TrigonGeometry::get(unsigned sz)
+{
+ auto pos = s_geometry.find(sz);
+ if (pos != s_geometry.end())
+ return *pos->second;
+ shared_ptr<TrigonGeometry> geometry(new TrigonGeometry(sz));
+ return *s_geometry.insert(make_pair(sz, geometry)).first->second;
+}
+
+auto TrigonGeometry::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+ AdjCoordList l;
+ if (get_point_type(x, y) == 0)
+ {
+ l.push_back(CoordPoint(x - 1, y));
+ l.push_back(CoordPoint(x + 1, y));
+ l.push_back(CoordPoint(x, y + 1));
+ }
+ else
+ {
+ l.push_back(CoordPoint(x, y - 1));
+ l.push_back(CoordPoint(x - 1, y));
+ l.push_back(CoordPoint(x + 1, y));
+ }
+ return l;
+}
+
+auto TrigonGeometry::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+ // The order does not matter logically but it is better to put far away
+ // points first because BoardConst uses the forbidden status of the first
+ // points during move generation and far away points can reject more moves.
+ DiagCoordList l;
+ if (get_point_type(x, y) == 0)
+ {
+ l.push_back(CoordPoint(x - 2, y));
+ l.push_back(CoordPoint(x + 2, y));
+ l.push_back(CoordPoint(x - 1, y - 1));
+ l.push_back(CoordPoint(x + 1, y - 1));
+ l.push_back(CoordPoint(x + 1, y + 1));
+ l.push_back(CoordPoint(x - 1, y + 1));
+ l.push_back(CoordPoint(x, y - 1));
+ l.push_back(CoordPoint(x - 2, y + 1));
+ l.push_back(CoordPoint(x + 2, y + 1));
+ }
+ else
+ {
+ l.push_back(CoordPoint(x - 2, y));
+ l.push_back(CoordPoint(x + 2, y));
+ l.push_back(CoordPoint(x - 1, y + 1));
+ l.push_back(CoordPoint(x + 1, y + 1));
+ l.push_back(CoordPoint(x + 1, y - 1));
+ l.push_back(CoordPoint(x - 1, y - 1));
+ l.push_back(CoordPoint(x, y + 1));
+ l.push_back(CoordPoint(x - 2, y - 1));
+ l.push_back(CoordPoint(x + 2, y - 1));
+ }
+ return l;
+}
+
+unsigned TrigonGeometry::get_period_x() const
+{
+ return 2;
+}
+
+unsigned TrigonGeometry::get_period_y() const
+{
+ return 2;
+}
+
+unsigned TrigonGeometry::get_point_type(int x, int y) const
+{
+ if (m_sz % 2 == 0)
+ {
+ if (x % 2 == 0)
+ return y % 2 == 0 ? 1 : 0;
+ else
+ return y % 2 != 0 ? 1 : 0;
+ }
+ else
+ {
+ if (x % 2 != 0)
+ return y % 2 == 0 ? 1 : 0;
+ else
+ return y % 2 != 0 ? 1 : 0;
+ }
+}
+
+bool TrigonGeometry::init_is_onboard(unsigned x, unsigned y) const
+{
+ auto width = get_width();
+ auto height = get_height();
+ unsigned dy = min(y, height - y - 1);
+ unsigned min_x = m_sz - dy - 1;
+ unsigned max_x = width - min_x - 1;
+ return x >= min_x && x <= max_x;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TrigonGeometry.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_TRIGON_GEOMETRY_H
+#define LIBPENTOBI_BASE_TRIGON_GEOMETRY_H
+
+#include <map>
+#include <memory>
+#include "Geometry.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Geometry as used in the game Blokus Trigon.
+ The board is a hexagon consisting of triangles. The coordinates are like
+ in this example of a hexagon with edge size 3:
+ <tt>
+ 0 1 2 3 4 5 6 7 8 9 10
+ 0 / \ / \ / \ / \
+ 1 / \ / \ / \ / \ / \
+ 2 / \ / \ / \ / \ / \ / \
+ 3 \ / \ / \ / \ / \ / \ /
+ 4 \ / \ / \ / \ / \ /
+ 5 \ / \ / \ / \ /
+ </tt>
+ There are two point types: 0=upward triangle, 1=downward triangle. */
+class TrigonGeometry final
+ : public Geometry
+{
+public:
+ /** Create or reuse an already created geometry with a given size.
+ @param sz The edge size of the hexagon. */
+ static const TrigonGeometry& get(unsigned sz);
+
+
+ AdjCoordList get_adj_coord(int x, int y) const override;
+
+ DiagCoordList get_diag_coord(int x, int y) const override;
+
+ unsigned get_point_type(int x, int y) const override;
+
+ unsigned get_period_x() const override;
+
+ unsigned get_period_y() const override;
+
+protected:
+ bool init_is_onboard(unsigned x, unsigned y) const override;
+
+private:
+ /** Stores already created geometries by size. */
+ static map<unsigned, shared_ptr<TrigonGeometry>> s_geometry;
+
+ unsigned m_sz;
+
+
+ explicit TrigonGeometry(unsigned sz);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_TRIGON_GEOMETRY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TrigonTransform.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TrigonTransform.h"
+
+#include <cmath>
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonIdentity::get_transformed(const CoordPoint& p) const
+{
+ return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRefl::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(-p.x, p.y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot60::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(ceil(0.5f * px - 1.5f * py));
+ int y = static_cast<int>(floor(0.5f * px + 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot120::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(ceil(-0.5f * px - 1.5f * py));
+ int y = static_cast<int>(ceil(0.5f * px - 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot180::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(-p.x, -p.y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot240::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(floor(-0.5f * px + 1.5f * py));
+ int y = static_cast<int>(ceil(-0.5f * px - 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot300::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(floor(0.5f * px + 1.5f * py));
+ int y = static_cast<int>(floor(-0.5f * px + 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot60::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(ceil(0.5f * (-px) - 1.5f * py));
+ int y = static_cast<int>(floor(0.5f * (-px) + 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot120::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(ceil(-0.5f * (-px) - 1.5f * py));
+ int y = static_cast<int>(ceil(0.5f * (-px) - 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot180::get_transformed(const CoordPoint& p) const
+{
+ return CoordPoint(p.x, -p.y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot240::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(floor(-0.5f * (-px) + 1.5f * py));
+ int y = static_cast<int>(ceil(-0.5f * (-px) - 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot300::get_transformed(const CoordPoint& p) const
+{
+ float px = static_cast<float>(p.x);
+ float py = static_cast<float>(p.y);
+ int x = static_cast<int>(floor(0.5f * (-px) + 1.5f * py));
+ int y = static_cast<int>(floor(-0.5f * (-px) + 0.5f * py));
+ return CoordPoint(x, y);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/TrigonTransform.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_TRIGON_TRANSFORM_H
+#define LIBPENTOBI_BASE_TRIGON_TRANSFORM_H
+
+#include "libboardgame_base/Transform.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+using libboardgame_base::Transform;
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonIdentity
+ : public Transform
+{
+public:
+ TransfTrigonIdentity() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot60
+ : public Transform
+{
+public:
+ TransfTrigonRot60() : Transform(1) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot120
+ : public Transform
+{
+public:
+ TransfTrigonRot120() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot180
+ : public Transform
+{
+public:
+ TransfTrigonRot180() : Transform(1) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot240
+ : public Transform
+{
+public:
+ TransfTrigonRot240() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot300
+ : public Transform
+{
+public:
+ TransfTrigonRot300() : Transform(1) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRefl
+ : public Transform
+{
+public:
+ TransfTrigonRefl() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot60
+ : public Transform
+{
+public:
+ TransfTrigonReflRot60() : Transform(1) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot120
+ : public Transform
+{
+public:
+ TransfTrigonReflRot120() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot180
+ : public Transform
+{
+public:
+ TransfTrigonReflRot180() : Transform(1) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot240
+ : public Transform
+{
+public:
+ TransfTrigonReflRot240() : Transform(0) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot300
+ : public Transform
+{
+public:
+ TransfTrigonReflRot300() : Transform(1) {}
+
+ CoordPoint get_transformed(const CoordPoint& p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_TRIGON_TRANSFORM_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Variant.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Variant.h"
+
+#include "CallistoGeometry.h"
+#include "NexosGeometry.h"
+#include "TrigonGeometry.h"
+#include "libboardgame_base/RectGeometry.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::PointTransfIdent;
+using libboardgame_base::PointTransfRefl;
+using libboardgame_base::PointTransfReflRot180;
+using libboardgame_base::PointTransfRot90;
+using libboardgame_base::PointTransfRot180;
+using libboardgame_base::PointTransfRot270;
+using libboardgame_base::PointTransfRot90Refl;
+using libboardgame_base::PointTransfRot270Refl;
+using libboardgame_base::PointTransfTrigonReflRot60;
+using libboardgame_base::PointTransfTrigonReflRot120;
+using libboardgame_base::PointTransfTrigonReflRot240;
+using libboardgame_base::PointTransfTrigonReflRot300;
+using libboardgame_base::PointTransfTrigonRot60;
+using libboardgame_base::PointTransfTrigonRot120;
+using libboardgame_base::PointTransfTrigonRot240;
+using libboardgame_base::PointTransfTrigonRot300;
+using libboardgame_base::RectGeometry;
+using libboardgame_util::trim;
+using libboardgame_util::to_lower;
+
+//-----------------------------------------------------------------------------
+
+BoardType get_board_type(Variant variant)
+{
+ BoardType result = BoardType::classic; // Init to avoid compiler warning
+ switch (variant)
+ {
+ case Variant::duo:
+ case Variant::junior:
+ result = BoardType::duo;
+ break;
+ case Variant::classic:
+ case Variant::classic_2:
+ case Variant::classic_3:
+ result = BoardType::classic;
+ break;
+ case Variant::trigon:
+ case Variant::trigon_2:
+ result = BoardType::trigon;
+ break;
+ case Variant::trigon_3:
+ result = BoardType::trigon_3;
+ break;
+ case Variant::nexos:
+ case Variant::nexos_2:
+ result = BoardType::nexos;
+ break;
+ case Variant::callisto:
+ result = BoardType::callisto;
+ break;
+ case Variant::callisto_2:
+ result = BoardType::callisto_2;
+ break;
+ case Variant::callisto_3:
+ result = BoardType::callisto_3;
+ break;
+ }
+ return result;
+}
+
+const Geometry& get_geometry(BoardType board_type)
+{
+ const Geometry* result = nullptr; // Init to avoid compiler warning
+ switch (board_type)
+ {
+ case BoardType::duo:
+ result = &RectGeometry<Point>::get(14, 14);
+ break;
+ case BoardType::classic:
+ result = &RectGeometry<Point>::get(20, 20);
+ break;
+ case BoardType::trigon:
+ result = &TrigonGeometry::get(9);
+ break;
+ case BoardType::trigon_3:
+ result = &TrigonGeometry::get(8);
+ break;
+ case BoardType::nexos:
+ result = &NexosGeometry::get(13);
+ break;
+ case BoardType::callisto:
+ result = &CallistoGeometry::get(4);
+ break;
+ case BoardType::callisto_2:
+ result = &CallistoGeometry::get(2);
+ break;
+ case BoardType::callisto_3:
+ result = &CallistoGeometry::get(3);
+ break;
+ }
+ return *result;
+}
+
+const Geometry& get_geometry(Variant variant)
+{
+ return get_geometry(get_board_type(variant));
+}
+
+Color::IntType get_nu_colors(Variant variant)
+{
+ Color::IntType result = 0; // Init to avoid compiler warning
+ switch (variant)
+ {
+ case Variant::duo:
+ case Variant::junior:
+ case Variant::callisto_2:
+ result = 2;
+ break;
+ case Variant::trigon_3:
+ case Variant::callisto_3:
+ result = 3;
+ break;
+ case Variant::classic:
+ case Variant::classic_2:
+ case Variant::classic_3:
+ case Variant::trigon:
+ case Variant::trigon_2:
+ case Variant::nexos:
+ case Variant::nexos_2:
+ case Variant::callisto:
+ result = 4;
+ break;
+ }
+ return result;
+}
+
+Color::IntType get_nu_players(Variant variant)
+{
+ Color::IntType result = 0; // Init to avoid compiler warning
+ switch (variant)
+ {
+ case Variant::duo:
+ case Variant::junior:
+ case Variant::classic_2:
+ case Variant::trigon_2:
+ case Variant::nexos_2:
+ case Variant::callisto_2:
+ result = 2;
+ break;
+ case Variant::classic_3:
+ case Variant::trigon_3:
+ case Variant::callisto_3:
+ result = 3;
+ break;
+ case Variant::classic:
+ case Variant::trigon:
+ case Variant::nexos:
+ case Variant::callisto:
+ result = 4;
+ break;
+ }
+ return result;
+}
+
+PieceSet get_piece_set(Variant variant)
+{
+ PieceSet result = PieceSet::classic; // Init to avoid compiler warning
+ switch (variant)
+ {
+ case Variant::classic:
+ case Variant::classic_2:
+ case Variant::classic_3:
+ case Variant::duo:
+ result = PieceSet::classic;
+ break;
+ case Variant::trigon:
+ case Variant::trigon_2:
+ case Variant::trigon_3:
+ result = PieceSet::trigon;
+ break;
+ case Variant::junior:
+ result = PieceSet::junior;
+ break;
+ case Variant::nexos:
+ case Variant::nexos_2:
+ result = PieceSet::nexos;
+ break;
+ case Variant::callisto:
+ case Variant::callisto_2:
+ case Variant::callisto_3:
+ result = PieceSet::callisto;
+ break;
+ }
+ return result;
+}
+
+void get_transforms(Variant variant,
+ vector<unique_ptr<PointTransform<Point>>>& transforms,
+ vector<unique_ptr<PointTransform<Point>>>& inv_transforms)
+{
+ transforms.clear();
+ inv_transforms.clear();
+ transforms.emplace_back(new PointTransfIdent<Point>);
+ inv_transforms.emplace_back(new PointTransfIdent<Point>);
+ switch (get_board_type(variant))
+ {
+ case BoardType::duo:
+ transforms.emplace_back(new PointTransfRot270Refl<Point>);
+ inv_transforms.emplace_back(new PointTransfRot270Refl<Point>);
+ break;
+ case BoardType::trigon:
+ transforms.emplace_back(new PointTransfTrigonRot60<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonRot300<Point>);
+ transforms.emplace_back(new PointTransfTrigonRot120<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonRot240<Point>);
+ transforms.emplace_back(new PointTransfRot180<Point>);
+ inv_transforms.emplace_back(new PointTransfRot180<Point>);
+ transforms.emplace_back(new PointTransfTrigonRot240<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonRot120<Point>);
+ transforms.emplace_back(new PointTransfTrigonRot300<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonRot60<Point>);
+ transforms.emplace_back(new PointTransfRefl<Point>);
+ inv_transforms.emplace_back(new PointTransfRefl<Point>);
+ transforms.emplace_back(new PointTransfTrigonReflRot60<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonReflRot60<Point>);
+ transforms.emplace_back(new PointTransfTrigonReflRot120<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonReflRot120<Point>);
+ transforms.emplace_back(new PointTransfReflRot180<Point>);
+ inv_transforms.emplace_back(new PointTransfReflRot180<Point>);
+ transforms.emplace_back(new PointTransfTrigonReflRot240<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonReflRot240<Point>);
+ transforms.emplace_back(new PointTransfTrigonReflRot300<Point>);
+ inv_transforms.emplace_back(new PointTransfTrigonReflRot300<Point>);
+ break;
+ case BoardType::callisto_2:
+ case BoardType::callisto:
+ case BoardType::callisto_3:
+ transforms.emplace_back(new PointTransfRot90<Point>);
+ inv_transforms.emplace_back(new PointTransfRot270<Point>);
+ transforms.emplace_back(new PointTransfRot180<Point>);
+ inv_transforms.emplace_back(new PointTransfRot180<Point>);
+ transforms.emplace_back(new PointTransfRot270<Point>);
+ inv_transforms.emplace_back(new PointTransfRot90<Point>);
+ transforms.emplace_back(new PointTransfRefl<Point>);
+ inv_transforms.emplace_back(new PointTransfRefl<Point>);
+ transforms.emplace_back(new PointTransfReflRot180<Point>);
+ inv_transforms.emplace_back(new PointTransfReflRot180<Point>);
+ transforms.emplace_back(new PointTransfRot90Refl<Point>);
+ inv_transforms.emplace_back(new PointTransfRot90Refl<Point>);
+ transforms.emplace_back(new PointTransfRot270Refl<Point>);
+ inv_transforms.emplace_back(new PointTransfRot270Refl<Point>);
+ break;
+ case BoardType::classic:
+ case BoardType::trigon_3:
+ case BoardType::nexos:
+ break;
+ }
+}
+
+bool has_central_symmetry(Variant variant)
+{
+ return variant == Variant::duo || variant == Variant::junior
+ || variant == Variant::trigon_2 || variant == Variant::callisto_2;
+}
+
+bool parse_variant(const string& s, Variant& variant)
+{
+ string t = to_lower(trim(s));
+ if (t == "blokus")
+ variant = Variant::classic;
+ else if (t == "blokus two-player")
+ variant = Variant::classic_2;
+ else if (t == "blokus three-player")
+ variant = Variant::classic_3;
+ else if (t == "blokus trigon")
+ variant = Variant::trigon;
+ else if (t == "blokus trigon two-player")
+ variant = Variant::trigon_2;
+ else if (t == "blokus trigon three-player")
+ variant = Variant::trigon_3;
+ else if (t == "blokus duo")
+ variant = Variant::duo;
+ else if (t == "blokus junior")
+ variant = Variant::junior;
+ else if (t == "nexos")
+ variant = Variant::nexos;
+ else if (t == "nexos two-player")
+ variant = Variant::nexos_2;
+ else if (t == "callisto")
+ variant = Variant::callisto;
+ else if (t == "callisto two-player")
+ variant = Variant::callisto_2;
+ else if (t == "callisto three-player")
+ variant = Variant::callisto_3;
+ else
+ return false;
+ return true;
+}
+
+bool parse_variant_id(const string& s, Variant& variant)
+{
+ string t = to_lower(trim(s));
+ if (t == "classic" || t == "c")
+ variant = Variant::classic;
+ else if (t == "classic_2" || t == "c2")
+ variant = Variant::classic_2;
+ else if (t == "classic_3" || t == "c3")
+ variant = Variant::classic_3;
+ else if (t == "trigon" || t == "t")
+ variant = Variant::trigon;
+ else if (t == "trigon_2" || t == "t2")
+ variant = Variant::trigon_2;
+ else if (t == "trigon_3" || t == "t3")
+ variant = Variant::trigon_3;
+ else if (t == "duo" || t == "d")
+ variant = Variant::duo;
+ else if (t == "junior" || t == "j")
+ variant = Variant::junior;
+ else if (t == "nexos" || t == "n")
+ variant = Variant::nexos;
+ else if (t == "nexos_2" || t == "n2")
+ variant = Variant::nexos_2;
+ else if (t == "callisto" || t == "ca")
+ variant = Variant::callisto;
+ else if (t == "callisto_2" || t == "ca2")
+ variant = Variant::callisto_2;
+ else if (t == "callisto_3" || t == "ca3")
+ variant = Variant::callisto_3;
+ else
+ return false;
+ return true;
+}
+
+const char* to_string(Variant variant)
+{
+ const char* result = nullptr; // Init to avoid compiler warning
+ switch (variant)
+ {
+ case Variant::classic:
+ result = "Blokus";
+ break;
+ case Variant::classic_2:
+ result = "Blokus Two-Player";
+ break;
+ case Variant::classic_3:
+ result = "Blokus Three-Player";
+ break;
+ case Variant::duo:
+ result = "Blokus Duo";
+ break;
+ case Variant::junior:
+ result = "Blokus Junior";
+ break;
+ case Variant::trigon:
+ result = "Blokus Trigon";
+ break;
+ case Variant::trigon_2:
+ result = "Blokus Trigon Two-Player";
+ break;
+ case Variant::trigon_3:
+ result = "Blokus Trigon Three-Player";
+ break;
+ case Variant::nexos:
+ result = "Nexos";
+ break;
+ case Variant::nexos_2:
+ result = "Nexos Two-Player";
+ break;
+ case Variant::callisto:
+ result = "Callisto";
+ break;
+ case Variant::callisto_2:
+ result = "Callisto Two-Player";
+ break;
+ case Variant::callisto_3:
+ result = "Callisto Three-Player";
+ break;
+ }
+ return result;
+}
+
+const char* to_string_id(Variant variant)
+{
+ const char* result = nullptr; // Init to avoid compiler warning
+ switch (variant)
+ {
+ case Variant::classic:
+ result = "classic";
+ break;
+ case Variant::classic_2:
+ result = "classic_2";
+ break;
+ case Variant::classic_3:
+ result = "classic_3";
+ break;
+ case Variant::duo:
+ result = "duo";
+ break;
+ case Variant::junior:
+ result = "junior";
+ break;
+ case Variant::trigon:
+ result = "trigon";
+ break;
+ case Variant::trigon_2:
+ result = "trigon_2";
+ break;
+ case Variant::trigon_3:
+ result = "trigon_3";
+ break;
+ case Variant::nexos:
+ result = "nexos";
+ break;
+ case Variant::nexos_2:
+ result = "nexos_2";
+ break;
+ case Variant::callisto:
+ result = "callisto";
+ break;
+ case Variant::callisto_2:
+ result = "callisto_2";
+ break;
+ case Variant::callisto_3:
+ result = "callisto_3";
+ break;
+ }
+ return result;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Variant.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_VARIANT_H
+#define LIBPENTOBI_BASE_VARIANT_H
+
+#include <memory>
+#include <string>
+#include <vector>
+#include "Color.h"
+#include "Geometry.h"
+#include "libboardgame_base/PointTransform.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::PointTransform;
+
+//-----------------------------------------------------------------------------
+
+enum class PieceSet
+{
+ classic,
+
+ junior,
+
+ trigon,
+
+ nexos,
+
+ callisto
+};
+
+//-----------------------------------------------------------------------------
+
+enum class BoardType
+{
+ classic,
+
+ duo,
+
+ trigon,
+
+ trigon_3,
+
+ nexos,
+
+ callisto,
+
+ callisto_2,
+
+ callisto_3,
+};
+
+//-----------------------------------------------------------------------------
+
+/** Game variant. */
+enum class Variant
+{
+ classic,
+
+ classic_2,
+
+ classic_3,
+
+ duo,
+
+ junior,
+
+ trigon,
+
+ trigon_2,
+
+ trigon_3,
+
+ nexos,
+
+ nexos_2,
+
+ callisto,
+
+ callisto_2,
+
+ callisto_3
+};
+
+//-----------------------------------------------------------------------------
+
+/** Get name of game variant as in the GM property in Blokus SGF files. */
+const char* to_string(Variant variant);
+
+/** Get a short lowercase string without spaces that can be used as
+ a identifier for a game variant.
+ The strings used are "classic", "classic_2", "duo", "trigon", "trigon_2",
+ "trigon_3", "junior" */
+const char* to_string_id(Variant variant);
+
+/** Parse name of game variant as in the GM property in Blokus SGF files.
+ The parsing is case-insensitive, leading and trailing whitespaced are
+ ignored.
+ @param s
+ @param[out] variant
+ @result True if the string contained a valid game variant. */
+bool parse_variant(const string& s, Variant& variant);
+
+/** Parse short lowercase name of game variant as returned to_string_id().
+ @param s
+ @param[out] variant
+ @result True if the string contained a valid game variant. */
+bool parse_variant_id(const string& s, Variant& variant);
+
+Color::IntType get_nu_colors(Variant variant);
+
+inline Color::Range get_colors(Variant variant)
+{
+ return Color::Range(get_nu_colors(variant));
+}
+
+Color::IntType get_nu_players(Variant variant);
+
+const Geometry& get_geometry(BoardType board_type);
+
+const Geometry& get_geometry(Variant variant);
+
+BoardType get_board_type(Variant variant);
+
+PieceSet get_piece_set(Variant variant);
+
+/** Get invariance transformations for a game variant.
+ The invariance transformations depend on the symmetry of the board type and
+ the starting points.
+ @param variant The game variant.
+ @param[out] transforms The invariance transformations.
+ @param[out] inv_transforms The inverse transformations of the elements in
+ transforms. */
+void get_transforms(Variant variant,
+ vector<unique_ptr<PointTransform<Point>>>& transforms,
+ vector<unique_ptr<PointTransform<Point>>>& inv_transforms);
+
+/** Is the variant a two-player variant with the board including the starting
+ points invariant through point reflection through its center? */
+bool has_central_symmetry(Variant variant);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_VARIANT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/BoardPainter.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "BoardPainter.h"
+#include "libpentobi_base/CallistoGeometry.h"
+
+#include <algorithm>
+#include <cmath>
+#include "Util.h"
+
+using namespace std;
+using libboardgame_util::ArrayList;
+using libpentobi_base::BoardType;
+using libpentobi_base::CallistoGeometry;
+using libpentobi_base::Move;
+using libpentobi_base::PieceSet;
+using libpentobi_base::PointState;
+
+//-----------------------------------------------------------------------------
+
+BoardPainter::BoardPainter()
+{
+ m_font.setFamily("Helvetica");
+ m_font.setStyleHint(QFont::SansSerif);
+ m_font.setStyleStrategy(QFont::PreferOutline);
+ m_fontSemiCondensed = m_font;
+ m_fontSemiCondensed.setStretch(QFont::SemiCondensed);
+ m_fontCondensed = m_font;
+ m_fontCondensed.setStretch(QFont::Condensed);
+ m_fontCoordLabels = m_font;
+ m_fontCoordLabels.setStretch(QFont::SemiCondensed);
+}
+
+BoardPainter::~BoardPainter() = default;
+
+CoordPoint BoardPainter::getCoordPoint(int x, int y)
+{
+ if (! m_hasPainted)
+ return CoordPoint::null();
+ x = static_cast<int>((x - m_boardOffset.x()) / m_fieldWidth);
+ y = static_cast<int>((y - m_boardOffset.y()) / m_fieldHeight);
+ if (x < 0 || x >= m_width || y < 0 || y >= m_height)
+ return CoordPoint::null();
+ else
+ return CoordPoint(x, y);
+}
+
+void BoardPainter::paintCoordinates(QPainter& painter)
+{
+ painter.setPen(m_coordinateColor);
+ for (int x = 0; x < m_width; ++x)
+ {
+ QString label;
+ if (x < 26)
+ label = QString(QChar('A' + x));
+ else
+ {
+ label = "A";
+ label.append(QChar('A' + (x - 26)));
+ }
+ paintLabel(painter, x * m_fieldWidth, m_height * m_fieldHeight,
+ m_fieldWidth, m_fieldHeight, label, true);
+ paintLabel(painter, x * m_fieldWidth, -m_fieldHeight,
+ m_fieldWidth, m_fieldHeight, label, true);
+ }
+ for (int y = 0; y < m_height; ++y)
+ {
+ QString label;
+ label.setNum(y + 1);
+ qreal left;
+ qreal right;
+ if (m_isTrigon)
+ {
+ left = -1.5 * m_fieldWidth;
+ right = (m_width + 0.5) * m_fieldWidth;
+ }
+ else
+ {
+ left = -m_fieldWidth;
+ right = m_width * m_fieldWidth;
+ }
+ paintLabel(painter, left, (m_height - y - 1) * m_fieldHeight,
+ m_fieldWidth, m_fieldHeight, label, true);
+ paintLabel(painter, right, (m_height - y - 1) * m_fieldHeight,
+ m_fieldWidth, m_fieldHeight, label, true);
+ }
+}
+
+void BoardPainter::paintEmptyBoard(QPainter& painter, unsigned width,
+ unsigned height, Variant variant,
+ const Geometry& geo)
+{
+ m_hasPainted = true;
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ m_variant = variant;
+ auto pieceSet = get_piece_set(variant);
+ m_geo = &geo;
+ m_width = static_cast<int>(m_geo->get_width());
+ m_height = static_cast<int>(m_geo->get_height());
+ m_isTrigon = (pieceSet == PieceSet::trigon);
+ m_isNexos = (pieceSet == PieceSet::nexos);
+ m_isCallisto = (pieceSet == PieceSet::callisto);
+ qreal ratio;
+ if (m_isTrigon)
+ {
+ ratio = 1.732;
+ if (m_coordinates)
+ m_fieldWidth =
+ min(qreal(width) / (m_width + 3),
+ height / (ratio * (m_height + 2)));
+ else
+ m_fieldWidth =
+ min(qreal(width) / (m_width + 1), height / (ratio * m_height));
+ }
+ else
+ {
+ ratio = 1;
+ if (m_coordinates)
+ m_fieldWidth =
+ min(qreal(width) / (m_width + 2),
+ qreal(height) / (m_height + 2));
+ else
+ m_fieldWidth =
+ min(qreal(width) / m_width, qreal(height) / m_height);
+ }
+ if (m_fieldWidth > 8)
+ // Prefer pixel alignment if board is not too small
+ m_fieldWidth = floor(m_fieldWidth);
+ m_fieldHeight = ratio * m_fieldWidth;
+ m_boardOffset = QPointF(0.5 * (width - m_fieldWidth * m_width),
+ 0.5 * (height - m_fieldHeight * m_height));
+ // QFont::setPixelSize(0) prints a warning even if it works and the docs
+ // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)).
+ int fontSize =
+ max(1, static_cast<int>((m_isTrigon ? 0.7 : 0.5) * m_fieldWidth));
+ m_font.setPixelSize(fontSize);
+ m_fontSemiCondensed.setPixelSize(fontSize);
+ m_fontCondensed.setPixelSize(fontSize);
+ m_fontCoordLabels.setPixelSize(fontSize);
+ painter.save();
+ painter.translate(m_boardOffset);
+ if (m_coordinates)
+ paintCoordinates(painter);
+ if (m_isNexos)
+ painter.fillRect(QRectF(m_fieldWidth / 4, m_fieldHeight / 4,
+ m_width * m_fieldWidth - m_fieldWidth / 2,
+ m_height * m_fieldHeight - m_fieldHeight / 2),
+ QColor(174, 167, 172));
+ auto nu_players = get_nu_players(m_variant);
+ for (Point p : *m_geo)
+ {
+ int x = m_geo->get_x(p);
+ int y = m_geo->get_y(p);
+ qreal fieldX = x * m_fieldWidth;
+ qreal fieldY = y * m_fieldHeight;
+ auto pointType = m_geo->get_point_type(p);
+ if (m_isTrigon)
+ {
+ bool isUpward = (pointType == 0);
+ Util::paintEmptyTriangle(painter, isUpward, fieldX, fieldY,
+ m_fieldWidth, m_fieldHeight);
+ }
+ else if (m_isNexos)
+ {
+ if (pointType == 1 || pointType == 2)
+ {
+ bool isHorizontal = (pointType == 1);
+ Util::paintEmptySegment(painter, isHorizontal, fieldX, fieldY,
+ m_fieldWidth);
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(pointType == 0);
+ Util::paintEmptyJunction(painter, fieldX, fieldY,
+ m_fieldWidth);
+ }
+ }
+ else if (m_isCallisto
+ && CallistoGeometry::is_center_section(x, y, nu_players))
+ Util::paintEmptySquareCallistoCenter(painter, fieldX, fieldY,
+ m_fieldWidth);
+ else if (m_isCallisto)
+ Util::paintEmptySquareCallisto(painter, fieldX, fieldY,
+ m_fieldWidth);
+ else
+ Util::paintEmptySquare(painter, fieldX, fieldY, m_fieldWidth);
+ }
+ painter.restore();
+}
+
+void BoardPainter::paintJunction(QPainter& painter, Variant variant,
+ const Grid<PointState>& pointState,
+ const Grid<unsigned>& pieceId, int x, int y,
+ qreal fieldX, qreal fieldY)
+{
+ LIBBOARDGAME_ASSERT(m_geo->get_point_type(x, y) == 0);
+ ArrayList<unsigned, 4> pieces;
+ if (x > 0)
+ {
+ auto piece = pieceId[m_geo->get_point(x - 1, y)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ if (x < m_width - 1)
+ {
+ auto piece = pieceId[m_geo->get_point(x + 1, y)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ if (y > 0)
+ {
+ auto piece = pieceId[m_geo->get_point(x, y - 1)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ if (y < m_height - 1)
+ {
+ auto piece = pieceId[m_geo->get_point(x, y + 1)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ for (auto piece : pieces)
+ {
+ Color c;
+ bool hasLeft = false;
+ if (x > 0)
+ {
+ Point p = m_geo->get_point(x - 1, y);
+ if (pieceId[p] == piece)
+ {
+ hasLeft = true;
+ c = pointState[p].to_color();
+ }
+ }
+ bool hasRight = false;
+ if (x < m_width - 1)
+ {
+ Point p = m_geo->get_point(x + 1, y);
+ if (pieceId[p] == piece)
+ {
+ hasRight = true;
+ c = pointState[p].to_color();
+ }
+ }
+ bool hasUp = false;
+ if (y > 0)
+ {
+ Point p = m_geo->get_point(x, y - 1);
+ if (pieceId[p] == piece)
+ {
+ hasUp = true;
+ c = pointState[p].to_color();
+ }
+ }
+ bool hasDown = false;
+ if (y < m_height - 1)
+ {
+ Point p = m_geo->get_point(x, y + 1);
+ if (pieceId[p] == piece)
+ {
+ hasDown = true;
+ c = pointState[p].to_color();
+ }
+ }
+ Util::paintJunction(painter, variant, c, fieldX, fieldY, m_fieldWidth,
+ m_fieldHeight, hasLeft, hasRight, hasUp, hasDown);
+ }
+}
+
+void BoardPainter::paintLabel(QPainter& painter, qreal x, qreal y,
+ qreal width, qreal height, const QString& label,
+ bool isCoordLabel)
+{
+ if (isCoordLabel)
+ painter.setFont(m_fontCoordLabels);
+ else
+ painter.setFont(m_font);
+ QFontMetrics metrics(painter.font());
+ QRect boundingRect = metrics.boundingRect(label);
+ if (! isCoordLabel)
+ {
+ if (boundingRect.width() > width)
+ {
+ painter.setFont(m_fontSemiCondensed);
+ QFontMetrics metrics(painter.font());
+ boundingRect = metrics.boundingRect(label);
+ }
+ if (boundingRect.width() > width)
+ {
+ painter.setFont(m_fontCondensed);
+ QFontMetrics metrics(painter.font());
+ boundingRect = metrics.boundingRect(label);
+ }
+ }
+ qreal dx = 0.5 * (width - boundingRect.width());
+ qreal dy = 0.5 * (height - boundingRect.height());
+ QRectF rect;
+ rect.setCoords(floor(x + dx), floor(y + dy),
+ ceil(x + width - dx + 1), ceil(y + height - dy + 1));
+ painter.drawText(rect, Qt::TextDontClip, label);
+}
+
+void BoardPainter::paintLabels(QPainter& painter,
+ const Grid<PointState>& pointState,
+ Variant variant, const Grid<QString>& labels)
+{
+ for (Point p : *m_geo)
+ if (! labels[p].isEmpty())
+ {
+ painter.setPen(Util::getLabelColor(variant, pointState[p]));
+ qreal x = m_geo->get_x(p) * m_fieldWidth;
+ qreal y = m_geo->get_y(p) * m_fieldHeight;
+ qreal width = m_fieldWidth;
+ qreal height = m_fieldHeight;
+ if (m_isTrigon)
+ {
+ bool isUpward = (m_geo->get_point_type(p) == 0);
+ if (isUpward)
+ y += 0.333 * height;
+ height = 0.666 * height;
+ }
+ paintLabel(painter, x, y, width, height, labels[p], false);
+ }
+}
+
+void BoardPainter::paintMarks(QPainter& painter,
+ const Grid<PointState>& pointState,
+ Variant variant, const Grid<int>& marks)
+{
+ for (Point p : *m_geo)
+ if (marks[p] & (dot | circle))
+ {
+ qreal x = (static_cast<float>(m_geo->get_x(p)) + 0.5f)
+ * m_fieldWidth;
+ qreal y = (static_cast<float>(m_geo->get_y(p)) + 0.5f)
+ * m_fieldHeight;
+ qreal size;
+ if (m_isTrigon)
+ {
+ bool isUpward = (m_geo->get_point_type(p) == 0);
+ if (isUpward)
+ y += 0.167 * m_fieldHeight;
+ else
+ y -= 0.167 * m_fieldHeight;
+ size = 0.1 * m_fieldHeight;
+ }
+ else if (m_isCallisto)
+ size = 0.1 * m_fieldHeight;
+ else
+ size = 0.12 * m_fieldHeight;
+ QColor color = Util::getMarkColor(variant, pointState[p]);
+ qreal penWidth = 0.05 * m_fieldHeight;
+ if (marks[p] & dot)
+ {
+ color.setAlphaF(0.5);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ size *= (1 + 0.25 * penWidth);
+ }
+ else
+ {
+ color.setAlphaF(0.6);
+ QPen pen(color);
+ pen.setWidthF(penWidth);
+ painter.setPen(pen);
+ painter.setBrush(Qt::NoBrush);
+ }
+ painter.drawEllipse(QPointF(x, y), size, size);
+ }
+}
+
+void BoardPainter::paintPieces(QPainter& painter,
+ const Grid<PointState>& pointState,
+ const Grid<unsigned>& pieceId,
+ const Grid<QString>* labels,
+ const Grid<int>* marks)
+{
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.save();
+ painter.translate(m_boardOffset);
+ ColorMap<bool> isFirstPiece(true);
+ for (Point p : *m_geo)
+ {
+ int x = m_geo->get_x(p);
+ int y = m_geo->get_y(p);
+ PointState s = pointState[p];
+ qreal fieldX = x * m_fieldWidth;
+ qreal fieldY = y * m_fieldHeight;
+ auto pointType = m_geo->get_point_type(p);
+ if (m_isTrigon)
+ {
+ if (s.is_empty())
+ continue;
+ Color c = s.to_color();
+ isFirstPiece[c] = false;
+ bool isUpward = (pointType == 0);
+ Util::paintColorTriangle(painter, m_variant, c, isUpward, fieldX,
+ fieldY, m_fieldWidth, m_fieldHeight);
+ }
+ else if (m_isNexos)
+ {
+ if (pointType == 1 || pointType == 2)
+ {
+ if (s.is_empty())
+ continue;
+ Color c = s.to_color();
+ isFirstPiece[c] = false;
+ bool isHorizontal = (pointType == 1);
+ Util::paintColorSegment(painter, m_variant, c, isHorizontal,
+ fieldX, fieldY, m_fieldWidth);
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(pointType == 0);
+ paintJunction(painter, m_variant, pointState, pieceId, x, y,
+ fieldX, fieldY);
+ }
+ }
+ else
+ {
+ if (s.is_empty())
+ continue;
+ Color c = s.to_color();
+ isFirstPiece[c] = false;
+ if (m_isCallisto)
+ {
+ bool hasLeft =
+ (x > 0 && m_geo->is_onboard(x - 1, y)
+ && pieceId[p] == pieceId[m_geo->get_point(x - 1, y)]);
+ bool hasRight =
+ (x < m_width - 1 && m_geo->is_onboard(x + 1, y)
+ && pieceId[p] == pieceId[m_geo->get_point(x + 1, y)]);
+ bool hasUp =
+ (y > 0 && m_geo->is_onboard(x, y - 1)
+ && pieceId[p] == pieceId[m_geo->get_point(x, y - 1)]);
+ bool hasDown =
+ (y < m_height - 1 && m_geo->is_onboard(x, y + 1)
+ && pieceId[p] == pieceId[m_geo->get_point(x, y + 1)]);
+ bool isOnePiece =
+ (! hasLeft && ! hasRight && ! hasUp && ! hasDown);
+ Util::paintColorSquareCallisto(painter, m_variant, c, fieldX,
+ fieldY, m_fieldWidth, hasRight,
+ hasDown, isOnePiece);
+ }
+ else
+ Util::paintColorSquare(painter, m_variant, c, fieldX, fieldY,
+ m_fieldWidth);
+ }
+ }
+ paintStartingPoints(painter, m_variant, pointState, isFirstPiece);
+ if (marks)
+ paintMarks(painter, pointState, m_variant, *marks);
+ if (labels)
+ paintLabels(painter, pointState, m_variant, *labels);
+ painter.restore();
+}
+
+void BoardPainter::paintSelectedPiece(QPainter& painter, Color c,
+ const MovePoints& points, bool isLegal)
+{
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.save();
+ painter.translate(m_boardOffset);
+ qreal alpha;
+ qreal saturation;
+ bool flat;
+ if (isLegal)
+ {
+ alpha = 0.9;
+ saturation = 0.8;
+ flat = false;
+ }
+ else
+ {
+ alpha = 0.63;
+ saturation = 0.5;
+ flat = true;
+ }
+ ArrayList<Point, 2 * PieceInfo::max_size> junctions;
+ for (Point p : points)
+ {
+ if (p.is_null())
+ continue;
+ auto x = m_geo->get_x(p);
+ auto y = m_geo->get_y(p);
+ auto pointType = m_geo->get_point_type(p);
+ qreal fieldX = x * m_fieldWidth;
+ qreal fieldY = y * m_fieldHeight;
+ if (m_isTrigon)
+ {
+ bool isUpward = (pointType == 0);
+ Util::paintColorTriangle(painter, m_variant, c, isUpward,
+ fieldX, fieldY, m_fieldWidth,
+ m_fieldHeight, alpha, saturation, flat);
+ }
+ else if (m_isNexos)
+ {
+ if (pointType == 1 || pointType == 2)
+ {
+ bool isHorizontal = (pointType == 1);
+ Util::paintColorSegment(painter, m_variant, c, isHorizontal,
+ fieldX, fieldY, m_fieldWidth, alpha,
+ saturation, flat);
+ if (isHorizontal)
+ {
+ if (m_geo->is_onboard(x - 1, y))
+ junctions.include(m_geo->get_point(x - 1, y));
+ if (m_geo->is_onboard(x + 1, y))
+ junctions.include(m_geo->get_point(x + 1, y));
+ }
+ else
+ {
+ if (m_geo->is_onboard(x, y - 1))
+ junctions.include(m_geo->get_point(x, y - 1));
+ if (m_geo->is_onboard(x, y + 1))
+ junctions.include(m_geo->get_point(x, y + 1));
+ }
+ }
+ }
+ else if (m_isCallisto)
+ {
+ bool hasRight = (m_geo->is_onboard(CoordPoint(x + 1, y))
+ && points.contains(m_geo->get_point(x + 1, y)));
+ bool hasDown = (m_geo->is_onboard(CoordPoint(x, y + 1))
+ && points.contains(m_geo->get_point(x, y + 1)));
+ bool isOnePiece = (points.size() == 1);
+ Util::paintColorSquareCallisto(painter, m_variant, c, fieldX,
+ fieldY, m_fieldWidth, hasRight,
+ hasDown, isOnePiece, alpha,
+ saturation, flat);
+ }
+ else
+ Util::paintColorSquare(painter, m_variant, c, fieldX, fieldY,
+ m_fieldWidth, alpha, saturation, flat);
+ }
+ if (m_isNexos)
+ for (auto p : junctions)
+ {
+ auto x = m_geo->get_x(p);
+ auto y = m_geo->get_y(p);
+ bool hasLeft = (m_geo->is_onboard(CoordPoint(x - 1, y))
+ && points.contains(m_geo->get_point(x - 1, y)));
+ bool hasRight = (m_geo->is_onboard(CoordPoint(x + 1, y))
+ && points.contains(m_geo->get_point(x + 1, y)));
+ bool hasUp = (m_geo->is_onboard(CoordPoint(x, y - 1))
+ && points.contains(m_geo->get_point(x, y - 1)));
+ bool hasDown = (m_geo->is_onboard(CoordPoint(x, y + 1))
+ && points.contains(m_geo->get_point(x, y + 1)));
+ Util::paintJunction(painter, m_variant, c, x * m_fieldWidth,
+ y * m_fieldHeight, m_fieldWidth, m_fieldHeight,
+ hasLeft, hasRight, hasUp, hasDown, alpha,
+ saturation);
+ }
+ painter.restore();
+}
+
+void BoardPainter::paintStartingPoints(QPainter& painter, Variant variant,
+ const Grid<PointState>& pointState,
+ const ColorMap<bool>& isFirstPiece)
+{
+ m_startingPoints.init(variant, *m_geo);
+ auto colors = get_colors(variant);
+ if (m_isTrigon)
+ {
+ bool isFirstPieceAny = false;
+ for (Color c : colors)
+ if (isFirstPiece[c])
+ {
+ isFirstPieceAny = true;
+ break;
+ }
+ if (! isFirstPieceAny)
+ return;
+ for (Point p : m_startingPoints.get_starting_points(Color(0)))
+ {
+ if (! pointState[p].is_empty())
+ continue;
+ int x = m_geo->get_x(p);
+ int y = m_geo->get_y(p);
+ qreal fieldX = x * m_fieldWidth;
+ qreal fieldY = y * m_fieldHeight;
+ bool isUpward = (m_geo->get_point_type(p) == 0);
+ Util::paintTriangleStartingPoint(painter, isUpward, fieldX, fieldY,
+ m_fieldWidth, m_fieldHeight);
+ }
+ }
+ else
+ {
+ for (Color c : colors)
+ {
+ if (! isFirstPiece[c])
+ continue;
+ for (Point p : m_startingPoints.get_starting_points(c))
+ {
+ if (! pointState[p].is_empty())
+ continue;
+ int x = m_geo->get_x(p);
+ int y = m_geo->get_y(p);
+ qreal fieldX = x * m_fieldWidth;
+ qreal fieldY = y * m_fieldHeight;
+ if (m_isNexos)
+ Util::paintSegmentStartingPoint(painter, variant, c,
+ fieldX, fieldY,
+ m_fieldWidth);
+ else
+ Util::paintSquareStartingPoint(painter, variant, c, fieldX,
+ fieldY, m_fieldWidth);
+ }
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/BoardPainter.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_BOARD_PAINTER_H
+#define LIBPENTOBI_GUI_BOARD_PAINTER_H
+
+#include <QPainter>
+#include "libpentobi_base/Grid.h"
+#include "libpentobi_base/Board.h"
+
+using libboardgame_base::CoordPoint;
+using libboardgame_base::Transform;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMap;
+using libpentobi_base::Variant;
+using libpentobi_base::Geometry;
+using libpentobi_base::Grid;
+using libpentobi_base::MovePoints;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::Point;
+using libpentobi_base::PointState;
+using libpentobi_base::StartingPoints;
+
+//-----------------------------------------------------------------------------
+
+/** Paints a board.
+ The painter can be used without having to create an instance of class Board,
+ which is undesirable for use cases like the thumbnailer because of the slow
+ creation of the BoardConst class. Instead, the board state is passed to the
+ paint() function as a grid of point states. */
+class BoardPainter
+{
+public:
+ enum
+ {
+ dot = 1 << 1,
+
+ circle = 1 << 2
+ };
+
+ BoardPainter();
+
+ ~BoardPainter();
+
+ void setCoordinates(bool enable) { m_coordinates = enable; }
+
+ void setCoordinateColor(const QColor& color) { m_coordinateColor = color; }
+
+ /** Paint the board.
+ This function must be called before painting any pieces because it
+ initializes some members that are used by the piece painting
+ functions. */
+ void paintEmptyBoard(QPainter& painter, unsigned width, unsigned height,
+ Variant variant, const Geometry& geo);
+
+ /** Paint the pieces and markup.
+ The pieceId parameter only needs to be initialized in game variant
+ Nexos and is needed to paint the junctions between segment. Only
+ segment points of pieceId are used (point type 1 or 2) and must be 0 if
+ the point is empty or contain a unique value for segments of the same
+ piece. */
+ void paintPieces(QPainter& painter, const Grid<PointState>& pointState,
+ const Grid<unsigned>& pieceId,
+ const Grid<QString>* labels = nullptr,
+ const Grid<int>* marks = nullptr);
+
+ /** Paint the selected piece.
+ Paints the selected piece either transparent (if not legal) or opaque
+ (if legal). */
+ void paintSelectedPiece(QPainter& painter, Color c,
+ const MovePoints& points, bool isLegal);
+
+ /** Get the corresponding board coordinates of a pixel.
+ @return The board coordinates or CoordPoint::null() if paint() was
+ not called yet or the pixel is outside the board. */
+ CoordPoint getCoordPoint(int x, int y);
+
+ bool hasPainted() const { return m_hasPainted; }
+
+private:
+ bool m_hasPainted = false;
+
+ bool m_coordinates = false;
+
+ bool m_isTrigon;
+
+ bool m_isNexos;
+
+ bool m_isCallisto;
+
+ const Geometry* m_geo;
+
+ Variant m_variant;
+
+ /** The width of the last board painted. */
+ int m_width;
+
+ /** The height of the last board painted. */
+ int m_height;
+
+ QColor m_coordinateColor = Qt::black;
+
+ qreal m_fieldWidth;
+
+ qreal m_fieldHeight;
+
+ QPointF m_boardOffset;
+
+ QFont m_font;
+
+ QFont m_fontCondensed;
+
+ QFont m_fontSemiCondensed;
+
+ QFont m_fontCoordLabels;
+
+ StartingPoints m_startingPoints;
+
+
+ void paintCoordinates(QPainter& painter);
+
+ void paintJunction(QPainter& painter, Variant variant,
+ const Grid<PointState>& pointState,
+ const Grid<unsigned>& pieceId, int x, int y,
+ qreal fieldX, qreal fieldY);
+
+ void paintLabel(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QString& label, bool isCoordLabel);
+
+ void paintLabels(QPainter& painter, const Grid<PointState>& pointState,
+ Variant variant, const Grid<QString>& labels);
+
+ void paintMarks(QPainter& painter, const Grid<PointState>& pointState,
+ Variant variant, const Grid<int>& marks);
+
+ void paintStartingPoints(QPainter& painter, Variant variant,
+ const Grid<PointState>& pointState,
+ const ColorMap<bool>& isFirstPiece);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_BOARD_PAINTER_H
--- /dev/null
+set(CMAKE_AUTOMOC TRUE)
+
+set(pentobi_gui_STAT_SRCS
+ BoardPainter.h
+ BoardPainter.cpp
+ ComputerColorDialog.h
+ ComputerColorDialog.cpp
+ GameInfoDialog.h
+ GameInfoDialog.cpp
+ GuiBoard.h
+ GuiBoard.cpp
+ GuiBoardUtil.h
+ GuiBoardUtil.cpp
+ HelpWindow.h
+ HelpWindow.cpp
+ InitialRatingDialog.h
+ InitialRatingDialog.cpp
+ LeaveFullscreenButton.h
+ LeaveFullscreenButton.cpp
+ LineEdit.h
+ LineEdit.cpp
+ OrientationDisplay.h
+ OrientationDisplay.cpp
+ PieceSelector.h
+ PieceSelector.cpp
+ SameHeightLayout.h
+ SameHeightLayout.cpp
+ ScoreDisplay.h
+ ScoreDisplay.cpp
+ Util.h
+ Util.cpp
+)
+
+set(pentobi_gui_ICNS
+ go-home.png
+ go-next.png
+ go-previous.png
+)
+
+set(pentobi_gui_TS
+ translations/libpentobi_gui_de.ts
+ )
+
+# Create PNG icons from SVG icons using the helper program src/convert
+file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/icons)
+file(COPY libpentobi_gui_resources.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
+foreach(icon ${pentobi_gui_ICNS})
+ string(REPLACE ".png" ".svg" svgicon ${icon})
+ add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}"
+ COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+ ${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+ )
+endforeach()
+qt5_add_resources(pentobi_gui_RC_SRCS
+ ${CMAKE_CURRENT_BINARY_DIR}/libpentobi_gui_resources.qrc
+ OPTIONS -no-compress)
+file(COPY libpentobi_gui_resources_2x.qrc DESTINATION
+ ${CMAKE_CURRENT_BINARY_DIR})
+foreach(icon ${pentobi_gui_ICNS})
+string(REPLACE ".png" ".svg" svgicon ${icon})
+string(REPLACE ".png" "@2x.png" hdpiicon ${icon})
+add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}"
+ COMMAND convert --hdpi ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+ ${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+)
+endforeach()
+qt5_add_resources(pentobi_gui_RC_SRCS
+ ${CMAKE_CURRENT_BINARY_DIR}/libpentobi_gui_resources_2x.qrc
+ OPTIONS -no-compress)
+
+qt5_add_translation(pentobi_gui_QM_SRCS ${pentobi_gui_TS})
+
+add_library(pentobi_gui STATIC
+ ${pentobi_gui_STAT_SRCS}
+ ${pentobi_gui_RC_SRCS}
+ ${pentobi_gui_QM_SRCS})
+
+target_link_libraries(pentobi_gui Qt5::Widgets)
+
+# Install translation files. If you change the destination, you need to
+# update the default for PENTOBI_TRANSLATIONS in the main CMakeLists.txt
+install(FILES ${pentobi_gui_QM_SRCS}
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/translations)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/ComputerColorDialog.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ComputerColorDialog.h"
+
+#include <QDialogButtonBox>
+#include <QLabel>
+#include <QVBoxLayout>
+
+//-----------------------------------------------------------------------------
+
+ComputerColorDialog::ComputerColorDialog(QWidget* parent,
+ Variant variant,
+ ColorMap<bool>& computerColor)
+ : QDialog(parent),
+ m_computerColor(computerColor),
+ m_variant(variant)
+{
+ setWindowTitle(tr("Computer Colors"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ auto layout = new QVBoxLayout;
+ setLayout(layout);
+ layout->setSizeConstraint(QLayout::SetFixedSize);
+ layout->addWidget(new QLabel(tr("Computer plays:")));
+ for (Color::IntType i = 0; i < get_nu_players(m_variant); ++i)
+ createCheckBox(layout, Color(i));
+ auto buttonBox =
+ new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ layout->addWidget(buttonBox);
+ connect(buttonBox, SIGNAL(accepted()), SLOT(accept()));
+ connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
+ buttonBox->setFocus();
+}
+
+void ComputerColorDialog::accept()
+{
+ auto nuPlayers = get_nu_players(m_variant);
+ auto nuColors = get_nu_colors(m_variant);
+ if (nuPlayers == nuColors || m_variant == Variant::classic_3)
+ for (Color c : Color::Range(nuPlayers))
+ m_computerColor[c] = m_checkBox[c.to_int()]->isChecked();
+ else
+ {
+ LIBBOARDGAME_ASSERT(nuPlayers == 2 && nuColors == 4);
+ m_computerColor[Color(0)] = m_checkBox[0]->isChecked();
+ m_computerColor[Color(2)] = m_checkBox[0]->isChecked();
+ m_computerColor[Color(1)] = m_checkBox[1]->isChecked();
+ m_computerColor[Color(3)] = m_checkBox[1]->isChecked();
+ }
+ QDialog::accept();
+}
+
+void ComputerColorDialog::createCheckBox(QLayout* layout, Color c)
+{
+ auto checkBox = new QCheckBox(getPlayerString(c));
+ checkBox->setChecked(m_computerColor[c]);
+ layout->addWidget(checkBox);
+ m_checkBox[c.to_int()] = checkBox;
+}
+
+QString ComputerColorDialog::getPlayerString(Color c)
+{
+ auto nuPlayers = get_nu_players(m_variant);
+ auto nuColors = get_nu_colors(m_variant);
+ auto i = c.to_int();
+ if (nuPlayers == 2 && nuColors == 4)
+ return i == 0 || i == 2 ? tr("&Blue/Red") : tr("&Yellow/Green");
+ if (i == 0)
+ return tr("&Blue");
+ if (i == 1)
+ return nuColors == 2 ? tr("&Green") : tr("&Yellow");
+ if (i == 2)
+ return tr("&Red");
+ LIBBOARDGAME_ASSERT(i == 3);
+ return tr("&Green");
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/ComputerColorDialog.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H
+#define LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <array>
+#include <QCheckBox>
+#include <QDialog>
+#include "libpentobi_base/Variant.h"
+#include "libpentobi_base/ColorMap.h"
+
+using namespace std;
+using libpentobi_base::Variant;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMap;
+
+//-----------------------------------------------------------------------------
+
+class ComputerColorDialog final
+ : public QDialog
+{
+ Q_OBJECT
+
+public:
+ ComputerColorDialog(QWidget* parent, Variant variant,
+ ColorMap<bool>& computerColor);
+
+public slots:
+ void accept() override;
+
+private:
+ ColorMap<bool>& m_computerColor;
+
+ Variant m_variant;
+
+ array<QCheckBox*, 4> m_checkBox;
+
+ void createCheckBox(QLayout* layout, Color c);
+
+ QString getPlayerString(Color c);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_COMPUTER_COLOR_DIALOG_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/GameInfoDialog.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "GameInfoDialog.h"
+
+#include <QDialogButtonBox>
+#include "LineEdit.h"
+#include "libpentobi_gui/Util.h"
+
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+GameInfoDialog::GameInfoDialog(QWidget* parent, Game& game)
+ : QDialog(parent),
+ m_game(game),
+ m_charset(game.get_root().get_property("CA", ""))
+{
+ setWindowTitle(tr("Game Info"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ auto layout = new QVBoxLayout;
+ setLayout(layout);
+ m_formLayout = new QFormLayout;
+ layout->addLayout(m_formLayout);
+ auto variant = game.get_variant();
+ auto nuColors = get_nu_colors(variant);
+ auto nuPlayers = get_nu_players(variant);
+ if (nuColors == 2)
+ {
+ m_playerBlue = createPlayerName(tr("Player &Blue:"), Color(0));
+ m_playerGreen = createPlayerName(tr("Player &Green:"), Color(1));
+ }
+ else if (nuPlayers == 2)
+ {
+ m_playerBlueRed = createPlayerName(tr("Player &Blue/Red:"), Color(0));
+ m_playerYellowGreen =
+ createPlayerName(tr("Player &Yellow/Green:"), Color(1));
+ }
+ else
+ {
+ m_playerBlue = createPlayerName(tr("Player &Blue:"), Color(0));
+ m_playerYellow = createPlayerName(tr("Player &Yellow:"), Color(1));
+ m_playerRed = createPlayerName(tr("Player &Red:"), Color(2));
+ if (nuPlayers == 4)
+ m_playerGreen = createPlayerName(tr("Player &Green:"), Color(3));
+ }
+ m_date = createLine(tr("&Date:"), m_game.get_date());
+ m_time = createLine(tr("&Time limits:"), m_game.get_time());
+ m_event = createLine(tr("&Event:"), m_game.get_event());
+ m_round = createLine(tr("R&ound:"), m_game.get_round());
+ auto buttonBox =
+ new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ layout->addWidget(buttonBox);
+ // We assume that the user wants to edit the game info if it is still empty
+ // and that he only wants to display it if not empty. Therefore, we leave
+ // the focus at the first text field if it is empty and put it on the
+ // button box otherwise.
+ if (nuColors == 4 && nuPlayers == 2)
+ {
+ if (! m_playerBlueRed->text().isEmpty())
+ buttonBox->setFocus();
+ }
+ else if (! m_playerBlue->text().isEmpty())
+ buttonBox->setFocus();
+ connect(buttonBox, SIGNAL(accepted()), SLOT(accept()));
+ connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
+}
+
+GameInfoDialog::~GameInfoDialog()
+{
+}
+
+void GameInfoDialog::accept()
+{
+ auto variant = m_game.get_variant();
+ auto nuColors = get_nu_colors(variant);
+ auto nuPlayers = get_nu_players(variant);
+ string value;
+ if (nuColors == 2)
+ {
+ if (acceptLine(m_playerBlue, value))
+ m_game.set_player_name(Color(0), value);
+ if (acceptLine(m_playerGreen, value))
+ m_game.set_player_name(Color(1), value);
+ }
+ else if (nuPlayers == 2)
+ {
+ if (acceptLine(m_playerBlueRed, value))
+ m_game.set_player_name(Color(0), value);
+ if (acceptLine(m_playerYellowGreen, value))
+ m_game.set_player_name(Color(1), value);
+ }
+ else
+ {
+ if (acceptLine(m_playerBlue, value))
+ m_game.set_player_name(Color(0), value);
+ if (acceptLine(m_playerYellow, value))
+ m_game.set_player_name(Color(1), value);
+ if (acceptLine(m_playerRed, value))
+ m_game.set_player_name(Color(2), value);
+ if (nuPlayers == 4)
+ if (acceptLine(m_playerGreen, value))
+ m_game.set_player_name(Color(3), value);
+ }
+ if (acceptLine(m_date, value))
+ m_game.set_date(value);
+ if (acceptLine(m_time, value))
+ m_game.set_time(value);
+ if (acceptLine(m_event, value))
+ m_game.set_event(value);
+ if (acceptLine(m_round, value))
+ m_game.set_round(value);
+ QDialog::accept();
+}
+
+bool GameInfoDialog::acceptLine(QLineEdit* lineEdit, string& value)
+{
+ if (! lineEdit->isModified())
+ return false;
+ QString text = lineEdit->text();
+ value = Util::convertSgfValueFromQString(text, m_charset);
+ return true;
+}
+
+QLineEdit* GameInfoDialog::createLine(const QString& label, const string& text)
+{
+ auto lineEdit = new LineEdit(30);
+ if (! text.empty())
+ {
+ lineEdit->setText(Util::convertSgfValueToQString(text, m_charset));
+ lineEdit->setCursorPosition(0);
+ }
+ m_formLayout->addRow(label, lineEdit);
+ return lineEdit;
+}
+
+QLineEdit* GameInfoDialog::createPlayerName(const QString& label, Color c)
+{
+ return createLine(label, m_game.get_player_name(c));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/GameInfoDialog.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_GAME_INFO_DIALOG_H
+#define LIBPENTOBI_GUI_GAME_INFO_DIALOG_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QDialog>
+#include <QFormLayout>
+#include <QLineEdit>
+#include "libpentobi_base/Game.h"
+
+using namespace std;
+using libpentobi_base::Color;
+using libpentobi_base::Game;
+
+//-----------------------------------------------------------------------------
+
+class GameInfoDialog final
+ : public QDialog
+{
+ Q_OBJECT
+
+public:
+ GameInfoDialog(QWidget* parent, Game& game);
+
+ ~GameInfoDialog();
+
+public slots:
+ void accept() override;
+
+private:
+ Game& m_game;
+
+ string m_charset;
+
+ QFormLayout* m_formLayout;
+
+ QLineEdit* m_playerBlue;
+
+ QLineEdit* m_playerYellow;
+
+ QLineEdit* m_playerRed;
+
+ QLineEdit* m_playerGreen;
+
+ QLineEdit* m_playerBlueRed;
+
+ QLineEdit* m_playerYellowGreen;
+
+ QLineEdit* m_date;
+
+ QLineEdit* m_event;
+
+ QLineEdit* m_round;
+
+ QLineEdit* m_time;
+
+ bool acceptLine(QLineEdit* lineEdit, string& value);
+
+ QLineEdit* createLine(const QString& label, const string& text);
+
+ QLineEdit* createPlayerName(const QString& label, Color c);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_GAME_INFO_DIALOG_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/GuiBoard.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "GuiBoard.h"
+
+#include <QApplication>
+#include <QMouseEvent>
+#include "libboardgame_base/Transform.h"
+
+using namespace std;
+using libboardgame_base::Transform;
+using libpentobi_base::Geometry;
+using libpentobi_base::MovePoints;
+using libpentobi_base::PiecePoints;
+using libpentobi_base::PieceSet;
+using libpentobi_base::Point;
+using libpentobi_base::PointState;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+bool allPointEmpty(const Board& bd, Move mv)
+{
+ for (Point p : bd.get_move_points(mv))
+ if (! bd.get_point_state(p).is_empty())
+ return false;
+ return true;
+}
+
+QPixmap* createPixmap(const QPainter& painter, const QSize& size)
+{
+ auto devicePixelRatio = painter.device()->devicePixelRatio();
+ auto pixmap = new QPixmap(devicePixelRatio * size);
+ pixmap->setDevicePixelRatio(devicePixelRatio);
+ return pixmap;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+GuiBoard::GuiBoard(QWidget* parent, const Board& bd)
+ : QWidget(parent),
+ m_bd(bd)
+{
+ setMinimumSize(350, 350);
+ connect(&m_currentMoveShownAnimationTimer, SIGNAL(timeout()),
+ SLOT(showMoveAnimation()));
+}
+
+void GuiBoard::changeEvent(QEvent* event)
+{
+ if (event->type() == QEvent::StyleChange)
+ setEmptyBoardDirty();
+}
+
+void GuiBoard::clearMarkup()
+{
+ for (Point p : m_bd)
+ {
+ m_marks[p] = 0;
+ setLabel(p, "");
+ }
+}
+
+void GuiBoard::clearPiece()
+{
+ m_selectedPiece = Piece::null();
+ m_selectedPieceTransform = nullptr;
+ setSelectedPiecePoints();
+ setMouseTracking(false);
+}
+
+void GuiBoard::copyFromBoard(const Board& bd)
+{
+ auto& geo = bd.get_geometry();
+ auto variant = bd.get_variant();
+ m_pointState.copy_from(bd.get_point_state(), geo);
+ auto pieceSet = get_piece_set(variant);
+ if (pieceSet == PieceSet::nexos || pieceSet == PieceSet::callisto)
+ {
+ m_pieceId.fill(0, geo);
+ unsigned n = 0;
+ for (Color c : bd.get_colors())
+ for (Move mv : bd.get_setup().placements[c])
+ {
+ ++n;
+ for (Point p : bd.get_move_points(mv))
+ m_pieceId[p] = n;
+ }
+ for (auto mv : bd.get_moves())
+ {
+ ++n;
+ for (Point p : bd.get_move_points(mv.move))
+ m_pieceId[p] = n;
+ }
+ }
+ if (! m_isInitialized || m_variant != variant)
+ {
+ m_variant = variant;
+ m_isInitialized = true;
+ m_labels.fill("", geo);
+ m_marks.fill(0, geo);
+ setEmptyBoardDirty();
+ }
+ else
+ setDirty();
+}
+
+Move GuiBoard::findSelectedPieceMove()
+{
+ if (m_selectedPiece.is_null() || m_selectedPieceOffset.is_null())
+ return Move::null();
+ const PiecePoints& points =
+ m_bd.get_piece_info(m_selectedPiece).get_points();
+ auto& geo = m_bd.get_geometry();
+ int width = static_cast<int>(geo.get_width());
+ int height = static_cast<int>(geo.get_height());
+ MovePoints movePoints;
+ for (CoordPoint p : points)
+ {
+ p = m_selectedPieceTransform->get_transformed(p);
+ int x = p.x + m_selectedPieceOffset.x;
+ int y = p.y + m_selectedPieceOffset.y;
+ if (x < 0 || x >= width || y < 0 || y >= height)
+ return Move::null();
+ Point pp = geo.get_point(x, y);
+ if (pp.is_null())
+ return Move::null();
+ movePoints.push_back(pp);
+ }
+ Move mv;
+ if (! m_bd.find_move(movePoints, m_selectedPiece, mv)
+ || (m_freePlacement && ! allPointEmpty(m_bd, mv))
+ || (! m_freePlacement
+ && ! m_bd.is_legal(m_selectedPieceColor, mv)))
+ return Move::null();
+ else
+ return mv;
+}
+
+void GuiBoard::leaveEvent(QEvent*)
+{
+ m_selectedPieceOffset = CoordPoint::null();
+ setSelectedPiecePoints();
+}
+
+void GuiBoard::mouseMoveEvent(QMouseEvent* event)
+{
+ if (m_selectedPiece.is_null())
+ return;
+ CoordPoint oldOffset = m_selectedPieceOffset;
+ setSelectedPieceOffset(*event);
+ if (m_selectedPieceOffset != oldOffset)
+ setSelectedPiecePoints();
+}
+
+void GuiBoard::mousePressEvent(QMouseEvent* event)
+{
+ if (m_selectedPiece.is_null())
+ {
+ CoordPoint p = m_boardPainter.getCoordPoint(event->x(), event->y());
+ auto& geo = m_bd.get_geometry();
+ if (geo.is_onboard(p))
+ emit pointClicked(geo.get_point(p.x, p.y));
+ return;
+ }
+ setSelectedPieceOffset(*event);
+ placePiece();
+}
+
+void GuiBoard::movePieceDown()
+{
+ if (m_selectedPiece.is_null())
+ return;
+ auto& geo = m_bd.get_geometry();
+ CoordPoint newOffset;
+ if (m_selectedPieceOffset.is_null())
+ {
+ newOffset = CoordPoint(geo.get_width() / 2, 0);
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ else
+ {
+ newOffset = m_selectedPieceOffset;
+ if (m_bd.get_piece_set() == PieceSet::trigon)
+ {
+ if (m_selectedPieceOffset.x % 2 == 0)
+ ++newOffset.x;
+ else
+ --newOffset.x;
+ ++newOffset.y;
+ }
+ else
+ newOffset.y += geo.get_period_y();
+ if (geo.is_onboard(newOffset))
+ {
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ }
+}
+
+void GuiBoard::movePieceLeft()
+{
+ if (m_selectedPiece.is_null())
+ return;
+ auto& geo = m_bd.get_geometry();
+ CoordPoint newOffset;
+ if (m_selectedPieceOffset.is_null())
+ {
+ newOffset = CoordPoint(geo.get_width() - 1, geo.get_height() / 2);
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ else
+ {
+ newOffset = m_selectedPieceOffset;
+ newOffset.x -= geo.get_period_x();
+ if (geo.is_onboard(newOffset))
+ {
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ }
+}
+
+void GuiBoard::movePieceRight()
+{
+ if (m_selectedPiece.is_null())
+ return;
+ auto& geo = m_bd.get_geometry();
+ CoordPoint newOffset;
+ if (m_selectedPieceOffset.is_null())
+ {
+ newOffset = CoordPoint(0, geo.get_height() / 2);
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ else
+ {
+ newOffset = m_selectedPieceOffset;
+ newOffset.x += geo.get_period_x();
+ if (geo.is_onboard(newOffset))
+ {
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ }
+}
+
+void GuiBoard::movePieceUp()
+{
+ if (m_selectedPiece.is_null())
+ return;
+ auto& geo = m_bd.get_geometry();
+ CoordPoint newOffset;
+ if (m_selectedPieceOffset.is_null())
+ {
+ newOffset = CoordPoint(geo.get_width() / 2, geo.get_height() - 1);
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ else
+ {
+ newOffset = m_selectedPieceOffset;
+ if (m_bd.get_piece_set() == PieceSet::trigon)
+ {
+ if (m_selectedPieceOffset.x % 2 == 0)
+ ++newOffset.x;
+ else
+ --newOffset.x;
+ --newOffset.y;
+ }
+ else
+ newOffset.y -= geo.get_period_y();
+ if (geo.is_onboard(newOffset))
+ {
+ setSelectedPieceOffset(newOffset);
+ setSelectedPiecePoints();
+ }
+ }
+}
+
+void GuiBoard::paintEvent(QPaintEvent*)
+{
+ if (! m_isInitialized)
+ return;
+ QPainter painter(this);
+ if (! m_emptyBoardPixmap || m_emptyBoardPixmap->size() != size())
+ {
+ m_emptyBoardPixmap.reset(createPixmap(painter, size()));
+ m_emptyBoardDirty = true;
+ }
+ if (! m_boardPixmap || m_boardPixmap->size() != size())
+ {
+ m_boardPixmap.reset(createPixmap(painter, size()));
+ m_dirty = true;
+ }
+ if (m_emptyBoardDirty)
+ {
+ QColor coordLabelColor =
+ QApplication::palette().color(QPalette::WindowText);
+ m_boardPainter.setCoordinateColor(coordLabelColor);
+ m_emptyBoardPixmap->fill(Qt::transparent);
+ QPainter painter(m_emptyBoardPixmap.get());
+ m_boardPainter.paintEmptyBoard(painter, width(), height(), m_variant,
+ m_bd.get_geometry());
+ m_emptyBoardDirty = false;
+ }
+ if (m_dirty)
+ {
+ m_boardPixmap->fill(Qt::transparent);
+ QPainter painter(m_boardPixmap.get());
+ painter.drawPixmap(0, 0, *m_emptyBoardPixmap);
+ m_boardPainter.paintPieces(painter, m_pointState, m_pieceId, &m_labels,
+ &m_marks);
+ m_dirty = false;
+ }
+ painter.drawPixmap(0, 0, *m_boardPixmap);
+ if (m_isMoveShown)
+ {
+ if (m_currentMoveShownAnimationIndex % 2 == 0)
+ m_boardPainter.paintSelectedPiece(painter, m_currentMoveShownColor,
+ m_currentMoveShownPoints, true);
+ }
+ else if (! m_selectedPiecePoints.empty())
+ {
+ bool isLegal = ! findSelectedPieceMove().is_null();
+ m_boardPainter.paintSelectedPiece(painter, m_selectedPieceColor,
+ m_selectedPiecePoints, isLegal);
+ }
+}
+
+void GuiBoard::placePiece()
+{
+ auto mv = findSelectedPieceMove();
+ if (! mv.is_null())
+ emit play(m_selectedPieceColor, mv);
+}
+
+void GuiBoard::selectPiece(Color color, Piece piece)
+{
+ if (m_selectedPiece == piece && m_selectedPieceColor == color)
+ return;
+ m_selectedPieceColor = color;
+ m_selectedPieceTransform = m_bd.get_transforms().get_default();
+ if (m_selectedPiece.is_null())
+ m_selectedPieceOffset = CoordPoint::null();
+ m_selectedPiece = piece;
+ setSelectedPieceOffset(m_selectedPieceOffset);
+ setSelectedPiecePoints();
+ setMouseTracking(true);
+}
+
+void GuiBoard::setEmptyBoardDirty()
+{
+ m_emptyBoardDirty = true;
+ m_dirty = true;
+ update();
+}
+
+void GuiBoard::setDirty()
+{
+ m_dirty = true;
+ update();
+}
+
+void GuiBoard::setCoordinates(bool enable)
+{
+ m_boardPainter.setCoordinates(enable);
+ setEmptyBoardDirty();
+}
+
+void GuiBoard::setFreePlacement(bool enable)
+{
+ m_freePlacement = enable;
+ update();
+}
+
+void GuiBoard::setLabel(Point p, const QString& text)
+{
+ if (! m_isInitialized)
+ return;
+ if (m_labels[p] != text)
+ {
+ m_labels[p] = text;
+ setDirty();
+ }
+}
+
+void GuiBoard::setMark(Point p, int mark, bool enable)
+{
+ if (! m_isInitialized)
+ return;
+ if (((m_marks[p] & mark) != 0) != enable)
+ {
+ m_marks[p] ^= mark;
+ setDirty();
+ }
+}
+
+void GuiBoard::setSelectedPieceOffset(const QMouseEvent& event)
+{
+ setSelectedPieceOffset(m_boardPainter.getCoordPoint(event.x(), event.y()));
+}
+
+void GuiBoard::setSelectedPieceOffset(const CoordPoint& offset)
+{
+ if (offset.is_null())
+ {
+ m_selectedPieceOffset = offset;
+ return;
+ }
+ auto& geo = m_bd.get_geometry();
+ auto pieceSet = m_bd.get_piece_set();
+ unsigned old_point_type = geo.get_point_type(offset);
+ CoordPoint type_matched_offset = offset;
+ if (pieceSet == PieceSet::trigon)
+ {
+ // Offset must match the point type (triangle up/down) of
+ // CoordPoint(0, 0) after the piece transformation
+ unsigned point_type = m_selectedPieceTransform->get_new_point_type();
+ bool hasLeft = geo.is_onboard(CoordPoint(offset.x - 1, offset.y));
+ bool hasRight = geo.is_onboard(CoordPoint(offset.x + 1, offset.y));
+ if (old_point_type != point_type)
+ {
+ if ((point_type == 0 && hasRight)
+ || (point_type == 1 && ! hasLeft))
+ ++type_matched_offset.x;
+ else
+ --type_matched_offset.x;
+ }
+ }
+ if (pieceSet == PieceSet::nexos)
+ {
+ // Offset must be a junction
+ if (old_point_type == 1) // horiz. segment
+ --type_matched_offset.x;
+ else if (old_point_type == 2) // vert. segment
+ --type_matched_offset.y;
+ else if (old_point_type == 3) // hole
+ {
+ --type_matched_offset.x;
+ --type_matched_offset.y;
+ }
+ }
+ m_selectedPieceOffset = type_matched_offset;
+}
+
+void GuiBoard::setSelectedPiecePoints(Move mv)
+{
+ m_selectedPiecePoints.clear();
+ for (Point p : m_bd.get_move_points(mv))
+ m_selectedPiecePoints.push_back(p);
+ update();
+}
+
+void GuiBoard::setSelectedPiecePoints()
+{
+ m_selectedPiecePoints.clear();
+ if (! m_selectedPiece.is_null() && ! m_selectedPieceOffset.is_null())
+ {
+ auto& geo = m_bd.get_geometry();
+ int width = static_cast<int>(geo.get_width());
+ int height = static_cast<int>(geo.get_height());
+ for (CoordPoint p : m_bd.get_piece_info(m_selectedPiece).get_points())
+ {
+ p = m_selectedPieceTransform->get_transformed(p);
+ int x = p.x + m_selectedPieceOffset.x;
+ int y = p.y + m_selectedPieceOffset.y;
+ if (x >= 0 && x < width && y >= 0 && y < height)
+ m_selectedPiecePoints.push_back(geo.get_point(x, y));
+ }
+ }
+ update();
+}
+
+void GuiBoard::setSelectedPieceTransform(const Transform* transform)
+{
+ if (m_selectedPieceTransform == transform)
+ return;
+ m_selectedPieceTransform = transform;
+ setSelectedPieceOffset(m_selectedPieceOffset);
+ setSelectedPiecePoints();
+}
+
+void GuiBoard::showMove(Color c, Move mv)
+{
+ m_isMoveShown = true;
+ m_currentMoveShownColor = c;
+ m_currentMoveShownPoints.clear();
+ for (Point p : m_bd.get_move_points(mv))
+ m_currentMoveShownPoints.push_back(p);
+ m_currentMoveShownAnimationIndex = 0;
+ m_currentMoveShownAnimationTimer.start(500);
+ update();
+}
+
+void GuiBoard::showMoveAnimation()
+{
+ ++m_currentMoveShownAnimationIndex;
+ if (m_currentMoveShownAnimationIndex > 5)
+ {
+ m_isMoveShown = false;
+ m_currentMoveShownAnimationTimer.stop();
+ }
+ update();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/GuiBoard.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_GUI_BOARD_H
+#define LIBPENTOBI_GUI_GUI_BOARD_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <memory>
+#include <QTimer>
+#include <QWidget>
+#include "BoardPainter.h"
+#include "libboardgame_base/CoordPoint.h"
+#include "libpentobi_base/Board.h"
+
+using namespace std;
+using libpentobi_base::Color;
+using libboardgame_base::CoordPoint;
+using libpentobi_base::Board;
+using libpentobi_base::Grid;
+using libpentobi_base::Move;
+using libpentobi_base::Piece;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::Point;
+
+//-----------------------------------------------------------------------------
+
+class GuiBoard
+ : public QWidget
+{
+ Q_OBJECT
+
+public:
+ GuiBoard(QWidget* parent, const Board& bd);
+
+ void setCoordinates(bool enable);
+
+ const Board& getBoard() const;
+
+ const Grid<QString>& getLabels() const;
+
+ Piece getSelectedPiece() const;
+
+ const Transform* getSelectedPieceTransform() const;
+
+ void setSelectedPieceTransform(const Transform* transform);
+
+ void showMove(Color c, Move mv);
+
+ void copyFromBoard(const Board& bd);
+
+ void setLabel(Point p, const QString& text);
+
+ void setMark(Point p, int mark, bool enable = true);
+
+ void clearMarkup();
+
+ void setFreePlacement(bool enable);
+
+ void setSelectedPiecePoints(Move mv);
+
+public slots:
+ void clearPiece();
+
+ void selectPiece(Color color, Piece piece);
+
+ void movePieceLeft();
+
+ void movePieceRight();
+
+ void movePieceUp();
+
+ void movePieceDown();
+
+ void placePiece();
+
+signals:
+ void play(Color color, Move mv);
+
+ void pointClicked(Point p);
+
+protected:
+ void changeEvent(QEvent* event) override;
+
+ void leaveEvent(QEvent* event) override;
+
+ void mouseMoveEvent(QMouseEvent* event) override;
+
+ void mousePressEvent(QMouseEvent* event) override;
+
+ void paintEvent(QPaintEvent* event) override;
+
+private:
+ const Board& m_bd;
+
+ bool m_isInitialized = false;
+
+ bool m_freePlacement = false;
+
+ /** Does the empty board need redrawing? */
+ bool m_emptyBoardDirty = true;
+
+ /** Do the pieces and markup on the board need redrawing?
+ If true, the cached board pixmap needs to be repainted. This does not
+ include the selected piece (the selected piece is always painted). */
+ bool m_dirty = true;
+
+ bool m_isMoveShown = false;
+
+ Variant m_variant;
+
+ Board::PointStateGrid m_pointState;
+
+ Grid<unsigned> m_pieceId;
+
+ Piece m_selectedPiece = Piece::null();
+
+ Color m_selectedPieceColor;
+
+ const Transform* m_selectedPieceTransform = nullptr;
+
+ CoordPoint m_selectedPieceOffset;
+
+ MovePoints m_selectedPiecePoints;
+
+ Grid<QString> m_labels;
+
+ Grid<int> m_marks;
+
+ BoardPainter m_boardPainter;
+
+ unique_ptr<QPixmap> m_emptyBoardPixmap;
+
+ unique_ptr<QPixmap> m_boardPixmap;
+
+ Color m_currentMoveShownColor;
+
+ MovePoints m_currentMoveShownPoints;
+
+ int m_currentMoveShownAnimationIndex;
+
+ QTimer m_currentMoveShownAnimationTimer;
+
+ Move findSelectedPieceMove();
+
+ void setEmptyBoardDirty();
+
+ void setDirty();
+
+ void setSelectedPieceOffset(const QMouseEvent& event);
+
+ void setSelectedPieceOffset(const CoordPoint& offset);
+
+ void setSelectedPiecePoints();
+
+private slots:
+ void showMoveAnimation();
+};
+
+inline const Board& GuiBoard::getBoard() const
+{
+ return m_bd;
+}
+
+inline const Grid<QString>& GuiBoard::getLabels() const
+{
+ return m_labels;
+}
+
+inline Piece GuiBoard::getSelectedPiece() const
+{
+ return m_selectedPiece;
+}
+
+inline const Transform* GuiBoard::getSelectedPieceTransform() const
+{
+ return m_selectedPieceTransform;
+}
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_GUI_BOARD_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/GuiBoardUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "GuiBoardUtil.h"
+
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace gui_board_util {
+
+using libpentobi_base::ColorMove;
+using libpentobi_base::PentobiTree;
+using libboardgame_sgf::SgfNode;
+using libboardgame_sgf::util::is_main_variation;
+using libboardgame_sgf::util::get_move_annotation;
+using libboardgame_util::get_letter_coord;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** Get the index of a variation.
+ This ignores child nodes without moves so that the moves are still labeled
+ 1a, 1b, 1c, etc. even if this does not correspond to the child node
+ index. (Note that this is a different convention from variation strings
+ which does not use move number and child move index, but node depth and
+ child node index) */
+bool getVariationIndex(const PentobiTree& tree, const SgfNode& node,
+ unsigned& moveIndex)
+{
+ auto parent = node.get_parent_or_null();
+ if (! parent || parent->has_single_child())
+ return false;
+ unsigned nuSiblingMoves = 0;
+ moveIndex = 0;
+ for (auto& i : parent->get_children())
+ {
+ if (! tree.has_move(i))
+ continue;
+ if (&i == &node)
+ moveIndex = nuSiblingMoves;
+ ++nuSiblingMoves;
+ }
+ if (nuSiblingMoves == 1)
+ return false;
+ return true;
+}
+
+void markMove(GuiBoard& guiBoard, const Game& game, const SgfNode& node,
+ unsigned moveNumber, ColorMove mv, bool markVariations,
+ bool markWithDot)
+{
+ if (mv.is_null())
+ return;
+ auto& bd = game.get_board();
+ Point p = bd.get_move_info_ext_2(mv.move).label_pos;
+ if (markWithDot)
+ {
+ if (markVariations && ! is_main_variation(game.get_current()))
+ guiBoard.setMark(p, BoardPainter::circle);
+ else
+ guiBoard.setMark(p, BoardPainter::dot);
+ return;
+ }
+ QString label;
+ label.setNum(moveNumber);
+ if (markVariations)
+ {
+ unsigned moveIndex;
+ if (getVariationIndex(game.get_tree(), node, moveIndex))
+ label.append(get_letter_coord(moveIndex).c_str());
+ }
+ label.append(get_move_annotation(game.get_tree(), node));
+ guiBoard.setLabel(p, label);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+void setMarkup(GuiBoard& guiBoard, const Game& game, unsigned markMovesBegin,
+ unsigned markMovesEnd, bool markVariations, bool markWithDot)
+{
+ guiBoard.clearMarkup();
+ if (markMovesBegin == 0)
+ return;
+ auto& tree = game.get_tree();
+ auto& bd = game.get_board();
+ unsigned moveNumber = bd.get_nu_moves();
+ auto node = &game.get_current();
+ do
+ {
+ auto mv = tree.get_move_ignore_invalid(*node);
+ if (! mv.is_null())
+ {
+ if (moveNumber >= markMovesBegin && moveNumber <= markMovesEnd)
+ markMove(guiBoard, game, *node, moveNumber, mv, markVariations,
+ markWithDot);
+ --moveNumber;
+ }
+ node = node->get_parent_or_null();
+ }
+ while (node);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace gui_board_util
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/GuiBoardUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_GUI_BOARD_UTIL_H
+#define LIBPENTOBI_GUI_GUI_BOARD_UTIL_H
+
+#include "GuiBoard.h"
+#include "libpentobi_base/Game.h"
+
+namespace gui_board_util {
+
+using libpentobi_base::Game;
+
+//-----------------------------------------------------------------------------
+
+void setMarkup(GuiBoard& guiBoard, const Game& game,
+ unsigned markMovesBegin, unsigned markMovesEnd,
+ bool markVariations, bool markWithDot);
+
+//-----------------------------------------------------------------------------
+
+} // namespace gui_board_util
+
+#endif // LIBPENTOBI_GUI_GUI_BOARD_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/HelpWindow.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "HelpWindow.h"
+
+#include <QApplication>
+#include <QAction>
+#include <QDesktopWidget>
+#include <QFile>
+#include <QLocale>
+#include <QSettings>
+#include <QTextBrowser>
+#include <QToolBar>
+#include "libboardgame_util/Log.h"
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void setIcon(QAction* action, const QString& name)
+{
+ QString fallback = QString(":/libpentobi_gui/icons/%1.png").arg(name);
+ action->setIcon(QIcon::fromTheme(name, QIcon(fallback)));
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+HelpWindow::HelpWindow(QWidget* parent, const QString& title,
+ const QString& mainPage)
+ : QMainWindow(parent)
+{
+ LIBBOARDGAME_LOG("Loading ", mainPage.toLocal8Bit().constData());
+ setWindowTitle(title);
+ if (QIcon::hasThemeIcon("help-browser"))
+ setWindowIcon(QIcon::fromTheme("help-browser"));
+ m_mainPageUrl = QUrl::fromLocalFile(mainPage);
+ auto browser = new QTextBrowser;
+ setCentralWidget(browser);
+ browser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ browser->setSource(m_mainPageUrl);
+ auto actionBack = new QAction(tr("Back"), this);
+ actionBack->setToolTip(tr("Show previous page in history"));
+ actionBack->setEnabled(false);
+ setIcon(actionBack, "go-previous");
+ connect(actionBack, SIGNAL(triggered()), browser, SLOT(backward()));
+ connect(browser, SIGNAL(backwardAvailable(bool)),
+ actionBack, SLOT(setEnabled(bool)));
+ auto actionForward = new QAction(tr("Forward"), this);
+ actionForward->setToolTip(tr("Show next page in history"));
+ actionForward->setEnabled(false);
+ setIcon(actionForward, "go-next");
+ connect(actionForward, SIGNAL(triggered()), browser, SLOT(forward()));
+ connect(browser, SIGNAL(forwardAvailable(bool)),
+ actionForward, SLOT(setEnabled(bool)));
+ m_actionHome = new QAction(tr("Contents"), this);
+ m_actionHome->setToolTip(tr("Show table of contents"));
+ m_actionHome->setEnabled(false);
+ setIcon(m_actionHome, "go-home");
+ connect(m_actionHome, SIGNAL(triggered()), browser, SLOT(home()));
+ connect(browser, SIGNAL(sourceChanged(const QUrl&)),
+ SLOT(handleSourceChanged(const QUrl&)));
+ auto actionClose = new QAction("", this);
+ actionClose->setShortcut(QKeySequence::Close);
+ connect(actionClose, SIGNAL(triggered()), SLOT(hide()));
+ addAction(actionClose);
+ auto toolBar = new QToolBar;
+ toolBar->setMovable(false);
+ toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
+ toolBar->addAction(actionBack);
+ toolBar->addAction(actionForward);
+ toolBar->addAction(m_actionHome);
+ addToolBar(toolBar);
+ QSettings settings;
+ if (! restoreGeometry(settings.value("helpwindow_geometry").toByteArray()))
+ adjustSize();
+}
+
+QString HelpWindow::findMainPage(QString helpDir, QString appName)
+{
+ auto locale = QLocale::system().name();
+ auto path = QString("%1/%2/%3/index.html").arg(helpDir, locale, appName);
+ if (QFile(path).exists())
+ return path;
+ path = QString("%1/%2/%3/index.html")
+ .arg(helpDir, locale.split("_")[0], appName);
+ if (QFile(path).exists())
+ return path;
+ return QString("%1/C/%3/index.html").arg(helpDir, appName);
+}
+
+void HelpWindow::closeEvent(QCloseEvent* event)
+{
+ QSettings settings;
+ settings.setValue("helpwindow_geometry", saveGeometry());
+ QMainWindow::closeEvent(event);
+}
+
+void HelpWindow::handleSourceChanged(const QUrl& src)
+{
+ m_actionHome->setEnabled(src != m_mainPageUrl);
+}
+
+QSize HelpWindow::sizeHint() const
+{
+ auto geo = QApplication::desktop()->screenGeometry();
+ return QSize(geo.width() * 4 / 10, geo.height() * 9 / 10);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/HelpWindow.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_HELP_WINDOW_H
+#define LIBPENTOBI_GUI_HELP_WINDOW_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QMainWindow>
+#include <QUrl>
+
+//-----------------------------------------------------------------------------
+
+class HelpWindow
+ : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ /** Find the main page for a given language.
+ Assumes that the layout of the help directory is according to
+ http://www.freedesktop.org/wiki/Specifications/help-spec/
+ @param helpDir The help directory.
+ @param appName The subdirectory name for the application.
+ @return The full path of index.html. */
+ static QString findMainPage(QString helpDir, QString appName);
+
+ HelpWindow(QWidget* parent, const QString& title, const QString& mainPage);
+
+ QSize sizeHint() const override;
+
+protected:
+ void closeEvent(QCloseEvent* event) override;
+
+private:
+ QUrl m_mainPageUrl;
+
+ QAction* m_actionHome;
+
+private slots:
+ void handleSourceChanged(const QUrl& src);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_HELP_WINDOW_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/InitialRatingDialog.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "InitialRatingDialog.h"
+
+#include <QDialogButtonBox>
+#include <QLabel>
+#include <QSlider>
+#include <QVBoxLayout>
+
+//-----------------------------------------------------------------------------
+
+InitialRatingDialog::InitialRatingDialog(QWidget* parent)
+ : QDialog(parent)
+{
+ setWindowTitle(tr("Initial Rating"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ auto layout = new QVBoxLayout;
+ setLayout(layout);
+ layout->setSizeConstraint(QLayout::SetFixedSize);
+ auto label =
+ new QLabel(tr("You have not yet played rated games in this game"
+ " variant. Estimate your playing strength to"
+ " initialize your rating."));
+ label->setWordWrap(true);
+ layout->addWidget(label);
+ auto sliderBoxLayout = new QHBoxLayout;
+ layout->addLayout(sliderBoxLayout);
+ sliderBoxLayout->addWidget(new QLabel(tr("Beginner")));
+ m_slider = new QSlider(Qt::Horizontal);
+ m_slider->setMinimum(1000);
+ m_slider->setMaximum(2000);
+ m_slider->setSingleStep(10);
+ m_slider->setPageStep(100);
+ sliderBoxLayout->addWidget(m_slider);
+ sliderBoxLayout->addWidget(new QLabel(tr("Expert")));
+ m_ratingLabel = new QLabel;
+ layout->addWidget(m_ratingLabel);
+ setRating(1000);
+ connect(m_slider, SIGNAL(valueChanged(int)), SLOT(setRating(int)));
+ auto buttonBox =
+ new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ layout->addWidget(buttonBox);
+ connect(buttonBox, SIGNAL(accepted()), SLOT(accept()));
+ connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
+}
+
+void InitialRatingDialog::setRating(int rating)
+{
+ m_rating = rating;
+ m_ratingLabel->setText(tr("Your initial rating: %1").arg(rating));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/InitialRatingDialog.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H
+#define LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QDialog>
+
+class QLabel;
+class QSlider;
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Dialog that asks the user to estimate his initial rating. */
+class InitialRatingDialog final
+ : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit InitialRatingDialog(QWidget* parent);
+
+ int getRating() const;
+
+public slots:
+ void setRating(int rating);
+
+private:
+ int m_rating;
+
+ QSlider* m_slider;
+
+ QLabel* m_ratingLabel;
+};
+
+inline int InitialRatingDialog::getRating() const
+{
+ return m_rating;
+}
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_INITIAL_RATING_DIALOG_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/LeaveFullscreenButton.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "LeaveFullscreenButton.h"
+
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QPropertyAnimation>
+#include <QTimer>
+#include <QToolButton>
+
+//-----------------------------------------------------------------------------
+
+LeaveFullscreenButton::LeaveFullscreenButton(QWidget* parent, QAction* action)
+ : QObject(parent)
+{
+ m_timer = new QTimer;
+ m_timer->setSingleShot(true);
+ m_triggerArea = new QWidget(parent);
+ m_triggerArea->setMouseTracking(true);
+ m_button = new QToolButton(parent);
+ m_button->setDefaultAction(action);
+ m_button->setToolTip("");
+ m_button->setToolButtonStyle(Qt::ToolButtonTextOnly);
+ m_button->show();
+ // Resize to size hint as a workaround for a bug that clips the
+ // long button text (tested on Qt 4.8.3 on Linux/KDE).
+ m_button->resize(m_button->sizeHint());
+ int x = qApp->desktop()->screenGeometry().width() - m_button->width();
+ m_buttonPos = QPoint(x, 0);
+ m_triggerArea->resize(m_button->width(), m_button->height() / 2);
+ m_triggerArea->move(m_buttonPos);
+ m_animation = new QPropertyAnimation(m_button, "pos");
+ m_animation->setDuration(1000);
+ m_animation->setStartValue(m_buttonPos);
+ m_animation->setEndValue(QPoint(x, -m_button->height() + 5));
+ qApp->installEventFilter(this);
+ connect(m_timer, SIGNAL(timeout()), SLOT(slideOut()));
+}
+
+void LeaveFullscreenButton::hideButton()
+{
+ m_animation->stop();
+ m_timer->stop();
+ m_triggerArea->hide();
+ m_button->hide();
+}
+
+bool LeaveFullscreenButton::eventFilter(QObject* watched, QEvent* event)
+{
+ if (m_button->isVisible() && event->type() == QEvent::MouseMove
+ && (watched == m_triggerArea || watched == m_button))
+ showButton();
+ return false;
+}
+
+void LeaveFullscreenButton::showButton()
+{
+ m_animation->stop();
+ m_button->move(m_buttonPos);
+ m_button->show();
+ m_triggerArea->hide();
+ m_timer->start(5000);
+}
+
+void LeaveFullscreenButton::slideOut()
+{
+ m_triggerArea->show();
+ m_animation->start();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/LeaveFullscreenButton.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H
+#define LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QObject>
+#include <QPoint>
+
+class QAction;
+class QPropertyAnimation;
+class QTimer;
+class QToolButton;
+
+//-----------------------------------------------------------------------------
+
+/** A button at the top right of the screen to leave fullscreen mode that
+ slides of the screen after a few seconds.
+ A few pixels of the button stay visible and also an invisible slightly
+ larger trigger area. If the mouse is moved over this area, the button
+ becomes visible again. */
+class LeaveFullscreenButton
+ : public QObject
+{
+ Q_OBJECT
+
+public:
+ /** Constructor.
+ @param parent The widget that will become fullscreen. This class adds
+ two child widgets to the parent: the actual button and the trigger area
+ (an invisible widget that listens for mouse movements and triggers the
+ button to become visible again if it is slid out).
+ @param action The action for leaving fullscreen mode associated with
+ the button */
+ LeaveFullscreenButton(QWidget* parent, QAction* action);
+
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+ void showButton();
+
+ void hideButton();
+
+private:
+ QToolButton* m_button;
+
+ QWidget* m_triggerArea;
+
+ QPoint m_buttonPos;
+
+ QTimer* m_timer;
+
+ QPropertyAnimation* m_animation;
+
+private slots:
+ void slideOut();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_LEAVE_FULLSCREEN_BUTTON_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/LineEdit.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "LineEdit.h"
+
+#include <QApplication>
+
+//-----------------------------------------------------------------------------
+
+LineEdit::LineEdit(int nuCharactersHint)
+ : m_nuCharactersHint(nuCharactersHint)
+{
+}
+
+QSize LineEdit::sizeHint() const
+{
+ QFont font = QApplication::font();
+ QFontMetrics metrics(font);
+ QSize size = QLineEdit::sizeHint();
+ size.setWidth(m_nuCharactersHint * metrics.averageCharWidth());
+ return size;
+}
+
+//-----------------------------------------------------------------------------
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/LineEdit.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_LINE_EDIT_H
+#define LIBPENTOBI_GUI_LINE_EDIT_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QLineEdit>
+
+//-----------------------------------------------------------------------------
+
+/** QLineEdit with a configurable size hint depending on the expected
+ number of characters. */
+class LineEdit
+ : public QLineEdit
+{
+ Q_OBJECT
+
+public:
+ explicit LineEdit(int nuCharactersHint);
+
+ QSize sizeHint() const override;
+
+private:
+ int m_nuCharactersHint;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_LINE_EDIT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/OrientationDisplay.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "OrientationDisplay.h"
+
+#include <QPainter>
+#include "libboardgame_base/GeometryUtil.h"
+#include "libpentobi_gui/Util.h"
+
+using namespace std;
+using libboardgame_base::ArrayList;
+using libboardgame_base::CoordPoint;
+using libboardgame_base::Transform;
+using libboardgame_base::geometry_util::normalize_offset;
+using libboardgame_base::geometry_util::type_match_offset;
+using libboardgame_base::geometry_util::type_match_shift;
+using libpentobi_base::Geometry;
+using libpentobi_base::PiecePoints;
+using libpentobi_base::PieceSet;
+
+//-----------------------------------------------------------------------------
+
+OrientationDisplay::OrientationDisplay(QWidget* parent, const Board& bd)
+ : QWidget(parent),
+ m_bd(bd)
+{
+ setMinimumSize(30, 30);
+}
+
+void OrientationDisplay::clearSelectedColor()
+{
+ if (m_isColorSelected)
+ {
+ m_isColorSelected = false;
+ update();
+ }
+}
+
+void OrientationDisplay::clearPiece()
+{
+ if (m_piece.is_null())
+ return;
+ m_piece = Piece::null();
+ update();
+}
+
+void OrientationDisplay::mousePressEvent(QMouseEvent*)
+{
+ if (m_isColorSelected && m_piece.is_null())
+ emit colorClicked(m_color);
+}
+
+void OrientationDisplay::paintEvent(QPaintEvent*)
+{
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ auto variant = m_bd.get_variant();
+ qreal fieldWidth;
+ qreal fieldHeight;
+ qreal displayWidth;
+ qreal displayHeight;
+ auto pieceSet = m_bd.get_piece_set();
+ bool isTrigon = (pieceSet == PieceSet::trigon);
+ bool isNexos = (pieceSet == PieceSet::nexos);
+ bool isCallisto = (pieceSet == PieceSet::callisto);
+ qreal ratio;
+ int columns;
+ int rows;
+ if (isTrigon)
+ {
+ ratio = 1.732;
+ columns = 7;
+ rows = 4;
+ }
+ else if (isNexos)
+ {
+ ratio = 1;
+ columns = 8;
+ rows = 8;
+ }
+ else
+ {
+ ratio = 1;
+ columns = 5;
+ rows = 5;
+ }
+ fieldWidth = min(qreal(width()) / columns,
+ qreal(height()) / (ratio * rows));
+ if (fieldWidth > 8)
+ // Prefer pixel alignment if piece is not too small
+ fieldWidth = floor(fieldWidth);
+ fieldHeight = ratio * fieldWidth;
+ displayWidth = fieldWidth * columns;
+ displayHeight = fieldHeight * rows;
+ if (m_piece.is_null())
+ {
+ if (m_isColorSelected)
+ {
+ qreal dotSize = 0.07 * height();
+ QColor color = Util::getPaintColor(variant, m_color);
+ painter.setBrush(color);
+ painter.setPen(Qt::NoPen);
+ painter.drawEllipse(QPointF(0.5 * width(), 0.5 * height()),
+ dotSize, dotSize);
+ }
+ return;
+ }
+ painter.save();
+ painter.translate(0.5 * (width() - displayWidth),
+ 0.5 * (height() - displayHeight));
+ PiecePoints points = m_bd.get_piece_info(m_piece).get_points();
+ m_transform->transform(points.begin(), points.end());
+ auto& geo = m_bd.get_geometry();
+ type_match_shift(geo, points.begin(), points.end(),
+ m_transform->get_new_point_type());
+ unsigned width;
+ unsigned height;
+ CoordPoint offset;
+ normalize_offset(points.begin(), points.end(), width, height, offset);
+ offset = type_match_offset(geo, geo.get_point_type(offset));
+ painter.save();
+ painter.translate(0.5 * (displayWidth - width * fieldWidth),
+ 0.5 * (displayHeight - height * fieldHeight));
+ ArrayList<CoordPoint, 2 * PieceInfo::max_size> junctions;
+ for (CoordPoint p : points)
+ {
+ qreal x = p.x * fieldWidth;
+ qreal y = p.y * fieldHeight;
+ auto pointType = geo.get_point_type(p + offset);
+ if (isTrigon)
+ {
+ bool isUpward = (pointType == 0);
+ Util::paintColorTriangle(painter, variant, m_color, isUpward,
+ x, y, fieldWidth, fieldHeight);
+ }
+ else if (isNexos)
+ {
+ if (pointType == 1 || pointType == 2)
+ {
+ bool isHorizontal = (pointType == 1);
+ Util::paintColorSegment(painter, variant, m_color,
+ isHorizontal, x, y, fieldWidth);
+ if (pointType == 1) // Horiz. segment
+ {
+ junctions.include(CoordPoint(p.x - 1, p.y));
+ junctions.include(CoordPoint(p.x + 1, p.y));
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(pointType == 2); // Vert. segment
+ junctions.include(CoordPoint(p.x, p.y - 1));
+ junctions.include(CoordPoint(p.x, p.y + 1));
+ }
+ }
+ }
+ else if (isCallisto)
+ {
+ bool hasRight = points.contains(CoordPoint(p.x + 1, p.y));
+ bool hasDown = points.contains(CoordPoint(p.x, p.y + 1));
+ bool isOnePiece = (points.size() == 1);
+ Util::paintColorSquareCallisto(painter, variant, m_color, x, y,
+ fieldWidth, hasRight, hasDown,
+ isOnePiece);
+ }
+ else
+ Util::paintColorSquare(painter, variant, m_color, x, y,
+ fieldWidth);
+ }
+ if (isNexos)
+ for (CoordPoint p : junctions)
+ {
+ bool hasLeft = points.contains(CoordPoint(p.x - 1, p.y));
+ bool hasRight = points.contains(CoordPoint(p.x + 1, p.y));
+ bool hasUp = points.contains(CoordPoint(p.x, p.y - 1));
+ bool hasDown = points.contains(CoordPoint(p.x, p.y + 1));
+ Util::paintJunction(painter, variant, m_color, p.x * fieldWidth,
+ p.y * fieldHeight, fieldWidth, fieldHeight,
+ hasLeft, hasRight, hasUp, hasDown);
+ }
+ painter.restore();
+ painter.restore();
+}
+
+void OrientationDisplay::selectColor(Color c)
+{
+ if (m_isColorSelected && m_color == c)
+ return;
+ m_isColorSelected = true;
+ m_color = c;
+ update();
+}
+
+void OrientationDisplay::setSelectedPiece(Piece piece)
+{
+ auto transform = m_bd.get_transforms().get_default();
+ if (m_piece == piece && m_transform == transform)
+ return;
+ m_piece = piece;
+ m_transform = transform;
+ update();
+}
+
+void OrientationDisplay::setSelectedPieceTransform(const Transform* transform)
+{
+ if (m_transform == transform)
+ return;
+ m_transform = transform;
+ update();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/OrientationDisplay.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H
+#define LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QWidget>
+#include "libpentobi_base/Board.h"
+
+using libboardgame_base::Transform;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::Piece;
+using libpentobi_base::PieceInfo;
+
+//-----------------------------------------------------------------------------
+
+class OrientationDisplay
+ : public QWidget
+{
+ Q_OBJECT
+
+public:
+ OrientationDisplay(QWidget* parent, const Board& bd);
+
+ void selectColor(Color c);
+
+ void clearSelectedColor();
+
+ void clearPiece();
+
+ void setSelectedPiece(Piece piece);
+
+ void setSelectedPieceTransform(const Transform* transform);
+
+signals:
+ /** A mouse click on the orientation display while a color but no no piece
+ was selected. */
+ void colorClicked(Color color);
+
+protected:
+ void mousePressEvent(QMouseEvent* event) override;
+
+ void paintEvent(QPaintEvent* event) override;
+
+private:
+ const Board& m_bd;
+
+ Piece m_piece = Piece::null();
+
+ const Transform* m_transform = nullptr;
+
+ bool m_isColorSelected = false;
+
+ Color m_color;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_ORIENTATION_DISPLAY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/PieceSelector.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PieceSelector.h"
+
+#include <QMouseEvent>
+#include <QPainter>
+#include "libboardgame_base/GeometryUtil.h"
+#include "libboardgame_util/StringUtil.h"
+#include "libpentobi_gui/Util.h"
+
+using libboardgame_base::CoordPoint;
+using libboardgame_base::geometry_util::type_match_shift;
+using libboardgame_util::trim;
+using libpentobi_base::BoardConst;
+using libpentobi_base::BoardType;
+using libpentobi_base::Geometry;
+using libpentobi_base::PieceMap;
+using libpentobi_base::PieceSet;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const char* pieceLayoutCallisto =
+ " 1 . U U U . O O . O O . L L . L . Z . . Z . . I . I . 2"
+ " . . U . U . O O . O O . L . . L . Z Z . Z Z . I . I . 2"
+ " 1 . . . . . . . . . . . L . L L . . Z . . Z . I . I . ."
+ " . .T5T5T5 . . W . . X . . . . . . . . . . . . . . . . 2"
+ " 1 . .T5 . . W W . X X X .T4T4T4 .T4T4T4 . V . . V . . 2"
+ " . . .T5 . W W . . . X . . .T4 . . .T4 . . V V . V V . .";
+
+const char* pieceLayoutClassic =
+ " 1 .Z4Z4 . .L4L4L4 . O O . P P .L5L5L5L5 .V5V5V5 . U U U . N . . ."
+ " . . .Z4Z4 . . .L4 . O O . P P .L5 . . . .V5 . . . U . U . N N .I5"
+ " 2 2 . . . .T4 . . . . . . P . . . . X . .V5 .Z5 . . . . . . N .I5"
+ " . . .I3 .T4T4T4 . . W W . . . F . X X X . . .Z5Z5Z5 . .T5 . N .I5"
+ "V3 . .I3 . . . . . . . W W . F F . . X . . Y . . .Z5 . .T5 . . .I5"
+ "V3V3 .I3 . .I4I4I4I4 . . W . . F F . . . Y Y Y Y . . .T5T5T5 . .I5";
+
+const char* pieceLayoutJunior =
+ "1 . 1 . V3V3. . L4L4L4. T4T4T4. . O O . O O . P P . . I5. I5. . L5L5"
+ ". . . . V3. . . L4. . . . T4. . . O O . O O . P P . . I5. I5. . . L5"
+ "2 . 2 . . . V3. . . . L4. . . T4. . . . . . . P . . . I5. I5. L5. L5"
+ "2 . 2 . . V3V3. . L4L4L4. . T4T4T4. . Z4. Z4. . . P . I5. I5. L5. L5"
+ ". . . . . . . . . . . . . . . . . . Z4Z4. Z4Z4. P P . I5. I5. L5. . "
+ "I3I3I3. I3I3I3. I4I4I4I4. I4I4I4I4. Z4. . . Z4. P P . . . . . L5L5. ";
+
+const char* pieceLayoutTrigon =
+ "L5L5 . . F F F F . .L6L6 . . O O O . . X X X . . .A6A6 . . G G . G . .C4C4 . . Y Y Y Y"
+ "L5L5 . . F . F . . .L6L6 . . O O O . . X X X . .A6A6A6A6 . . G G G . .C4C4 . . Y Y . ."
+ " .L5 . . . . . . S . .L6L6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2"
+ " . . . . . S S S S . . . . . . . .P5P5P5P5 . . .I6I6 . .I5I5I5I5I5 . . W W W W W . . 2"
+ "C5C5 . . . S . . . . V V . .P6 . . . .P5 . .A4 . .I6I6 . . . . . . . . . . W . . . . ."
+ "C5C5C5 . . . . V V V V . .P6P6P6P6P6 . . .A4A4A4 . .I6I6 . .I3I3I3 . . 1 . . .I4I4I4I4";
+
+// To increase the clickable area and to ensure that the pieces can be found
+// in the string with flood filling, the Nexos pieces also include some
+// crossable junction points that are not part of the piece definition(they
+// will be filtered out before finding the piece). But the number of points per
+// piece must be at most PiecePoints::max_size.
+const char* pieceLayoutNexos =
+ " . . F F F F F . . . O O O .U4U4U4U4U4 . . . . N N N N . . . . H H H . .U3 .U3 . . .V2V2V2"
+ "I4 . . . F . F . Y . O . O .U4 . . .U4 .T4 . . . . . N . . . . . H . . .U3 .U3 . . . . .V2"
+ "I4 . . . . . . . Y . . O O . . . . . . .T4T4T4T4 . . N N . . . . H H . .U3U3U3 . . . . .V2"
+ "I4 .L4 . . . . . Y . . . . . . . . . . .T4 . . . . . . . . . X . . . . . . . . . . . J . ."
+ "I4 .L4 . . . . Y Y .L3 . G G . . . . . . . . . . .Z3Z3 . . X X X . . . . . .I2 . . . J . ."
+ "I4 .L4 . W . . . Y .L3 . G . . . E . . . . .T3 . . .Z3 . . . X . . . . .Z4 .I2 . J . J .V4"
+ "I4 .L4 . W W W . Y .L3 . G G G . E E E E . .T3T3 . .Z3Z3 . . . .Z4Z4Z4Z4Z4 .I2 . J J J .V4"
+ "I4 .L4 . . . W . . .L3 . . . G . . . E . . .T3 . . . . . . . . .Z4 . . . . .I2 . . . . .V4"
+ " . .L4L4 . . W W . .L3L3L3 . . . . . . . . . . . .I3I3I3I3I3 . . . . 1 1 1 .I2 . .V4V4V4V4";
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+PieceSelector::PieceSelector(QWidget* parent, const Board& bd, Color color)
+ : QWidget(parent),
+ m_bd(bd),
+ m_color(color)
+{
+ setMinimumSize(170, 30);
+ init();
+}
+
+void PieceSelector::checkUpdate()
+{
+ bool disabledStatus[maxColumns][maxRows];
+ setDisabledStatus(disabledStatus);
+ for (unsigned x = 0; x < m_nuColumns; ++x)
+ for (unsigned y = 0; y < m_nuRows; ++y)
+ if (! m_piece[x][y].is_null()
+ && disabledStatus[x][y] != m_disabled[x][y])
+ {
+ update();
+ return;
+ }
+}
+
+void PieceSelector::filterCrossableJunctions(PiecePoints& points) const
+{
+ auto& geo = m_bd.get_geometry();
+ PiecePoints newPoints;
+ for (auto& p : points)
+ {
+ if (geo.get_point_type(p) != 0)
+ // Not a junction
+ newPoints.push_back(p);
+ else if (points.contains(CoordPoint(p.x - 1, p.y))
+ && points.contains(CoordPoint(p.x + 1, p.y))
+ && ! points.contains(CoordPoint(p.x, p.y - 1))
+ && ! points.contains(CoordPoint(p.x, p.y + 1)))
+ // Necessary junction
+ newPoints.push_back(p);
+ else if (! points.contains(CoordPoint(p.x - 1, p.y))
+ && ! points.contains(CoordPoint(p.x + 1, p.y))
+ && points.contains(CoordPoint(p.x, p.y - 1))
+ && points.contains(CoordPoint(p.x, p.y + 1)))
+ // Necessary junction
+ newPoints.push_back(p);
+ }
+ points = newPoints;
+}
+
+void PieceSelector::findPiecePoints(Piece piece, unsigned x, unsigned y,
+ PiecePoints& points) const
+{
+ CoordPoint p(x, y);
+ if (x >= m_nuColumns || y >= m_nuRows || m_piece[x][y] != piece
+ || points.contains(p))
+ return;
+ points.push_back(p);
+ // This assumes that no Trigon pieces touch at the corners, otherwise
+ // we would need to iterate over neighboring CoordPoint's corresponding to
+ // Geometry::get_adj()
+ findPiecePoints(piece, x + 1, y, points);
+ findPiecePoints(piece, x - 1, y, points);
+ findPiecePoints(piece, x, y + 1, points);
+ findPiecePoints(piece, x, y - 1, points);
+}
+
+void PieceSelector::init()
+{
+ auto pieceSet = m_bd.get_piece_set();
+ switch (pieceSet)
+ {
+ case PieceSet::classic:
+ m_pieceLayout = pieceLayoutClassic;
+ m_nuColumns = 33;
+ m_nuRows = 6;
+ break;
+ case PieceSet::trigon:
+ m_pieceLayout = pieceLayoutTrigon;
+ m_nuColumns = 43;
+ m_nuRows = 6;
+ break;
+ case PieceSet::junior:
+ m_pieceLayout = pieceLayoutJunior;
+ m_nuColumns = 34;
+ m_nuRows = 6;
+ break;
+ case PieceSet::nexos:
+ m_pieceLayout = pieceLayoutNexos;
+ m_nuColumns = 45;
+ m_nuRows = 9;
+ break;
+ case PieceSet::callisto:
+ m_pieceLayout = pieceLayoutCallisto;
+ m_nuColumns = 28;
+ m_nuRows = 6;
+ break;
+ }
+ LIBBOARDGAME_ASSERT(m_nuColumns <= maxColumns);
+ LIBBOARDGAME_ASSERT(m_nuRows <= maxRows);
+ for (unsigned y = 0; y < m_nuRows; ++y)
+ for (unsigned x = 0; x < m_nuColumns; ++x)
+ {
+ string name = m_pieceLayout.substr(y * m_nuColumns * 2 + x * 2, 2);
+ name = trim(name);
+ Piece piece = Piece::null();
+ if (name != ".")
+ {
+ m_bd.get_piece_by_name(name, piece);
+ LIBBOARDGAME_ASSERT(! piece.is_null());
+ }
+ m_piece[x][y] = piece;
+ }
+ auto& geo = m_bd.get_geometry();
+ for (unsigned y = 0; y < m_nuRows; ++y)
+ for (unsigned x = 0; x < m_nuColumns; ++x)
+ {
+ Piece piece = m_piece[x][y];
+ if (piece.is_null())
+ continue;
+ PiecePoints points;
+ findPiecePoints(piece, x, y, points);
+ // We need to match the coordinate system of the piece selector to
+ // the geometry, they are different in Trigon3.
+ type_match_shift(geo, points.begin(), points.end(), 0);
+ if (pieceSet == PieceSet::nexos)
+ filterCrossableJunctions(points);
+ m_transform[x][y] =
+ m_bd.get_piece_info(piece).find_transform(geo, points);
+ LIBBOARDGAME_ASSERT(m_transform[x][y]);
+ }
+ setDisabledStatus(m_disabled);
+ update();
+}
+
+void PieceSelector::mousePressEvent(QMouseEvent* event)
+{
+ qreal pixelX = event->x() - 0.5 * (width() - m_selectorWidth);
+ qreal pixelY = event->y() - 0.5 * (height() - m_selectorHeight);
+ if (pixelX < 0 || pixelX >= m_selectorWidth
+ || pixelY < 0 || pixelY >= m_selectorHeight)
+ return;
+ int x = static_cast<int>(pixelX / m_fieldWidth);
+ int y = static_cast<int>(pixelY / m_fieldHeight);
+ Piece piece = m_piece[x][y];
+ if (piece.is_null() || m_disabled[x][y])
+ return;
+ update();
+ emit pieceSelected(m_color, piece, m_transform[x][y]);
+}
+
+void PieceSelector::paintEvent(QPaintEvent*)
+{
+ setDisabledStatus(m_disabled);
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ auto pieceSet = m_bd.get_piece_set();
+ bool isTrigon = (pieceSet == PieceSet::trigon);
+ bool isNexos = (pieceSet == PieceSet::nexos);
+ bool isCallisto = (pieceSet == PieceSet::callisto);
+ qreal ratio;
+ if (isTrigon)
+ {
+ ratio = 1.732;
+ m_fieldWidth = min(qreal(width()) / (m_nuColumns + 1),
+ qreal(height()) / (ratio * m_nuRows));
+ }
+ else
+ {
+ ratio = 1;
+ m_fieldWidth = min(qreal(width()) / m_nuColumns,
+ qreal(height()) / m_nuRows);
+ }
+ if (m_fieldWidth > 8)
+ // Prefer pixel alignment if piece is not too small
+ m_fieldWidth = floor(m_fieldWidth);
+ m_fieldHeight = ratio * m_fieldWidth;
+ m_selectorWidth = m_fieldWidth * m_nuColumns;
+ m_selectorHeight = m_fieldHeight * m_nuRows;
+ painter.save();
+ painter.translate(0.5 * (width() - m_selectorWidth),
+ 0.5 * (height() - m_selectorHeight));
+ auto variant = m_bd.get_variant();
+ auto& geo = m_bd.get_geometry();
+ for (unsigned x = 0; x < m_nuColumns; ++x)
+ for (unsigned y = 0; y < m_nuRows; ++y)
+ {
+ auto pointType = geo.get_point_type(x, y);
+ Piece piece = m_piece[x][y];
+ if (isTrigon)
+ {
+ if (piece.is_null() || m_disabled[x][y])
+ continue;
+ bool isUpward = (pointType == geo.get_point_type(0, 0));
+ Util::paintColorTriangle(painter, variant, m_color, isUpward,
+ x * m_fieldWidth, y * m_fieldHeight,
+ m_fieldWidth, m_fieldHeight);
+ }
+ else if (isNexos)
+ {
+ if (pointType == 1 || pointType == 2)
+ {
+ if (piece.is_null() || m_disabled[x][y])
+ continue;
+ bool isHorizontal = (geo.get_point_type(x, y) == 1);
+ Util::paintColorSegment(painter, variant, m_color,
+ isHorizontal, x * m_fieldWidth,
+ y * m_fieldHeight, m_fieldWidth);
+ }
+ else if (pointType == 0)
+ {
+ bool hasLeft =
+ (x > 0 && ! m_piece[x - 1][y].is_null()
+ && ! m_disabled[x - 1][y]);
+ bool hasRight =
+ (x < m_nuColumns - 1
+ && ! m_piece[x + 1][y].is_null()
+ && ! m_disabled[x + 1][y]);
+ bool hasUp =
+ (y > 0 && ! m_piece[x][y - 1].is_null()
+ && ! m_disabled[x][y - 1]);
+ bool hasDown =
+ (y < m_nuRows - 1
+ && ! m_piece[x][y + 1].is_null()
+ && ! m_disabled[x][y + 1]);
+ Util::paintJunction(painter, variant, m_color,
+ x * m_fieldWidth, y * m_fieldHeight,
+ m_fieldWidth, m_fieldHeight, hasLeft,
+ hasRight, hasUp, hasDown);
+ }
+ }
+ else
+ {
+ if (piece.is_null() || m_disabled[x][y])
+ continue;
+ if (isCallisto)
+ {
+ bool hasLeft = (x > 0 && ! m_piece[x - 1][y].is_null());
+ bool hasRight =
+ (x < m_nuColumns - 1
+ && ! m_piece[x + 1][y].is_null());
+ bool hasUp = (y > 0 && ! m_piece[x][y - 1].is_null());
+ bool hasDown =
+ (y < m_nuRows - 1
+ && ! m_piece[x][y + 1].is_null());
+ bool isOnePiece =
+ (! hasLeft && ! hasRight && ! hasUp && ! hasDown);
+ Util::paintColorSquareCallisto(painter, variant, m_color,
+ x * m_fieldWidth,
+ y * m_fieldHeight,
+ m_fieldWidth, hasRight,
+ hasDown, isOnePiece);
+ }
+ else
+ Util::paintColorSquare(painter, variant, m_color,
+ x * m_fieldWidth, y * m_fieldHeight,
+ m_fieldWidth);
+ }
+ }
+ painter.restore();
+}
+
+void PieceSelector::setDisabledStatus(bool disabledStatus[maxColumns][maxRows])
+{
+ bool marker[maxColumns][maxRows];
+ for (unsigned x = 0; x < m_nuColumns; ++x)
+ for (unsigned y = 0; y < m_nuRows; ++y)
+ {
+ marker[x][y] = false;
+ disabledStatus[x][y] = false;
+ }
+ PieceMap<unsigned> nuInstances;
+ nuInstances.fill(0);
+ bool isColorUsed = (m_color.to_int() < m_bd.get_nu_colors());
+ for (unsigned x = 0; x < m_nuColumns; ++x)
+ for (unsigned y = 0; y < m_nuRows; ++y)
+ {
+ if (marker[x][y])
+ continue;
+ Piece piece = m_piece[x][y];
+ if (piece.is_null())
+ continue;
+ PiecePoints points;
+ findPiecePoints(piece, x, y, points);
+ bool disabled = false;
+ if (! isColorUsed
+ || ++nuInstances[piece] > m_bd.get_nu_left_piece(m_color,
+ piece))
+ disabled = true;
+ for (auto& p : points)
+ {
+ disabledStatus[p.x][p.y] = disabled;
+ marker[p.x][p.y] = true;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/PieceSelector.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_PIECE_SELECTOR_H
+#define LIBPENTOBI_GUI_PIECE_SELECTOR_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string>
+#include <QWidget>
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/Color.h"
+
+using namespace std;
+using libboardgame_base::Transform;
+using libboardgame_util::ArrayList;
+using libpentobi_base::Color;
+using libpentobi_base::Board;
+using libpentobi_base::Piece;
+using libpentobi_base::PiecePoints;
+
+//-----------------------------------------------------------------------------
+
+class PieceSelector
+ : public QWidget
+{
+ Q_OBJECT
+
+public:
+ PieceSelector(QWidget* parent, const Board& bd, Color color);
+
+ /** Needs to be called after the game variant of the current board has
+ changed because references to pieces are only unique within a
+ game variant. */
+ void init();
+
+ /** Call update() if pieces left have changed since last paint. */
+ void checkUpdate();
+
+signals:
+ void pieceSelected(Color color, Piece piece, const Transform* transform);
+
+protected:
+ void mousePressEvent(QMouseEvent* event) override;
+
+ void paintEvent(QPaintEvent* event) override;
+
+private:
+ static const unsigned maxColumns = 45;
+
+ static const unsigned maxRows = 9;
+
+ const Board& m_bd;
+
+ Color m_color;
+
+ unsigned m_nuColumns;
+
+ unsigned m_nuRows;
+
+ Piece m_piece[maxColumns][maxRows];
+
+ const Transform* m_transform[maxColumns][maxRows];
+
+ bool m_disabled[maxColumns][maxRows];
+
+ qreal m_fieldWidth;
+
+ qreal m_fieldHeight;
+
+ qreal m_selectorWidth;
+
+ qreal m_selectorHeight;
+
+ string m_pieceLayout;
+
+
+ void filterCrossableJunctions(PiecePoints& points) const;
+
+ void findPiecePoints(Piece piece, unsigned x, unsigned y,
+ PiecePoints& points) const;
+
+ void setDisabledStatus(bool disabledStatus[maxColumns][maxRows]);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_PIECE_SELECTOR_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/SameHeightLayout.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "SameHeightLayout.h"
+
+#include <QStyle>
+#include <QWidget>
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+SameHeightLayout::SameHeightLayout(QWidget* parent)
+ : QLayout(parent)
+{
+}
+
+SameHeightLayout::~SameHeightLayout()
+{
+ QLayoutItem* item;
+ while ((item = takeAt(0)))
+ delete item;
+}
+
+void SameHeightLayout::addItem(QLayoutItem* item)
+{
+ m_list.append(item);
+}
+
+QSize SameHeightLayout::sizeHint() const
+{
+ QSize s(0, 0);
+ int count = m_list.count();
+ int i = 0;
+ while (i < count)
+ {
+ QSize size = m_list.at(i)->sizeHint();
+ s.setWidth(max(size.width(), s.width()));
+ s.setHeight(s.height() + size.height());
+ ++i;
+ }
+ return s + (count - 1) * QSize(0, getSpacing());
+}
+
+QSize SameHeightLayout::minimumSize() const
+{
+ QSize s(0, 0);
+ int count = m_list.count();
+ int i = 0;
+ while (i < count)
+ {
+ QSize size = m_list.at(i)->minimumSize();
+ s.setWidth(max(size.width(), s.width()));
+ s.setHeight(s.height() + size.height());
+ ++i;
+ }
+ return s + (count - 1) * QSize(0, getSpacing());
+}
+
+int SameHeightLayout::count() const
+{
+ return m_list.size();
+}
+
+int SameHeightLayout::getSpacing() const
+{
+ // spacing() returns -1 with Qt 4.7 on KDE. It returns 6 on Gnome. Is this a
+ // bug? The documentation says: "If no value is explicitly set, the layout's
+ // spacing is inherited from the parent layout, or from the style settings
+ // for the parent widget."
+ int result = spacing();
+ if (result < 0 && parentWidget())
+ result = parentWidget()->style()->layoutSpacing(QSizePolicy::Frame,
+ QSizePolicy::Frame,
+ Qt::Vertical);
+ if (result < 0)
+ result = 5;
+ return result;
+}
+
+QLayoutItem* SameHeightLayout::itemAt(int i) const
+{
+ return m_list.value(i);
+}
+
+QLayoutItem* SameHeightLayout::takeAt(int i)
+{
+ return i >= 0 && i < m_list.size() ? m_list.takeAt(i) : nullptr;
+}
+
+void SameHeightLayout::setGeometry(const QRect& rect)
+{
+ QLayout::setGeometry(rect);
+ if (m_list.size() == 0)
+ return;
+ int count = m_list.count();
+ int width = rect.width();
+ int height = (rect.height() - (count - 1) * getSpacing()) / count;
+ int x = rect.x();
+ int y = rect.y();
+ for (int i = 0; i < count; ++i)
+ {
+ QRect geom(x, y, width, height);
+ m_list.at(i)->setGeometry(geom);
+ y = y + height + getSpacing();
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/SameHeightLayout.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H
+#define LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QLayout>
+
+//-----------------------------------------------------------------------------
+
+/** Layout that assigns exactly the same height to all items.
+ Needed for the box containing the piece selectors, because QBoxLayout
+ and QGridLayout do not always assign the exact same height to all items
+ if the height is not a multiple of the number of items. */
+class SameHeightLayout
+ : public QLayout
+{
+ Q_OBJECT
+
+public:
+ explicit SameHeightLayout(QWidget* parent = nullptr);
+
+ ~SameHeightLayout();
+
+ void addItem(QLayoutItem* item) override;
+
+ QSize sizeHint() const override;
+
+ QSize minimumSize() const override;
+
+ int count() const override;
+
+ QLayoutItem* itemAt(int i) const override;
+
+ QLayoutItem* takeAt(int i) override;
+
+ void setGeometry(const QRect& rect) override;
+
+private:
+ QList<QLayoutItem*> m_list;
+
+ int getSpacing() const;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_SAME_HEIGHT_LAYOUT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/ScoreDisplay.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ScoreDisplay.h"
+
+#include <cmath>
+#include <QApplication>
+#include <QPainter>
+#include "libpentobi_gui/Util.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+ScoreDisplay::ScoreDisplay(QWidget* parent)
+ : QWidget(parent)
+{
+ m_variant = Variant::classic;
+ m_font.setStyleStrategy(QFont::StyleStrategy(QFont::PreferOutline
+ | QFont::PreferQuality));
+ setMinimumSize(300, 20);
+ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+}
+
+void ScoreDisplay::drawScore(QPainter& painter, Color c, int x)
+{
+ QColor color = Util::getPaintColor(m_variant, c);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ QFontMetrics metrics(m_font);
+ int ascent = metrics.ascent();
+ // y is baseline
+ int y = static_cast<int>(ceil(0.5 * (height() - ascent)) + ascent);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.drawEllipse(x, y - m_colorDotSize, m_colorDotSize,
+ m_colorDotSize);
+ QString text = getScoreText(c);
+ bool underline = ! m_hasMoves[c];
+ bool hasBonus = m_bonus[c] != 0;
+ drawText(painter, text, x + m_colorDotWidth, y, underline, hasBonus);
+}
+
+void ScoreDisplay::drawScore2(QPainter& painter, Color c1, Color c2, int x)
+{
+ auto color = Util::getPaintColor(m_variant, c1);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ QFontMetrics metrics(m_font);
+ int ascent = metrics.ascent();
+ // y is baseline
+ int y = static_cast<int>(ceil(0.5 * (height() - ascent)) + ascent);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.drawEllipse(x, y - m_colorDotSize, m_colorDotSize, m_colorDotSize);
+ color = Util::getPaintColor(m_variant, c2);
+ painter.setBrush(color);
+ painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize, m_colorDotSize,
+ m_colorDotSize);
+ QString text = getScoreText2(c1, c2);
+ bool underline = (! m_hasMoves[c1] && ! m_hasMoves[c2]);
+ drawText(painter, text, x + m_twoColorDotWidth, y, underline, false);
+}
+
+void ScoreDisplay::drawScore3(QPainter& painter, int x)
+{
+ auto color = Util::getPaintColor(m_variant, Color(3));
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ QFontMetrics metrics(m_font);
+ int ascent = metrics.ascent();
+ // y is baseline
+ int y = static_cast<int>(ceil(0.5 * (height() - ascent)) + ascent);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ if (m_hasMoves[Color(3)])
+ {
+ painter.drawEllipse(x, y - m_colorDotSize,
+ m_colorDotSize, m_colorDotSize);
+ color = Util::getPaintColor(m_variant, m_altPlayer);
+ painter.setBrush(color);
+ painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize,
+ m_colorDotSize, m_colorDotSize);
+ }
+ else
+ painter.drawEllipse(x + m_colorDotSize, y - m_colorDotSize,
+ m_colorDotSize, m_colorDotSize);
+ QString text = getScoreText3();
+ bool underline = ! m_hasMoves[Color(3)];
+ drawText(painter, text, x + m_twoColorDotWidth, y, underline, false);
+}
+
+void ScoreDisplay::drawText(QPainter& painter, const QString& text, int x,
+ int y, bool underline, bool hasBonus)
+{
+ painter.setFont(m_font);
+ QFontMetrics metrics(m_font);
+ auto color = QApplication::palette().color(QPalette::WindowText);
+ painter.setPen(color);
+ painter.setRenderHint(QPainter::Antialiasing, false);
+ painter.drawText(x, y, text);
+ if (underline)
+ {
+ // Draw underline (instead of using an underlined font because the
+ // underline of some fonts is too close to the text and we want it
+ // to be very visible)
+ int lineWidth = metrics.lineWidth();
+ QPen pen(color);
+ pen.setWidth(lineWidth);
+ painter.setPen(pen);
+ y += 2 * lineWidth;
+ if (y > height() - 1)
+ y = height() - 1;
+ painter.drawLine(x + (hasBonus ? metrics.width(text.left(1)) : 0), y,
+ x + metrics.width(text), y);
+ }
+}
+
+QString ScoreDisplay::getScoreText(ScoreType points, ScoreType bonus) const
+{
+ return QString("%1%2").arg(bonus > 0 ? "*" : "", QString::number(points));
+}
+
+QString ScoreDisplay::getScoreText(Color c)
+{
+ return getScoreText(m_points[c], m_bonus[c]);
+}
+
+QString ScoreDisplay::getScoreText2(Color c1, Color c2)
+{
+ return getScoreText(m_points[c1] + m_points[c2], 0);
+}
+
+QString ScoreDisplay::getScoreText3()
+{
+ return "(" + getScoreText(Color(3)) + ")";
+}
+
+int ScoreDisplay::getScoreTextWidth(Color c)
+{
+ return getTextWidth(getScoreText(c));
+}
+
+int ScoreDisplay::getScoreTextWidth2(Color c1, Color c2)
+{
+ return getTextWidth(getScoreText2(c1, c2));
+}
+
+int ScoreDisplay::getScoreTextWidth3()
+{
+ return getTextWidth(getScoreText3());
+}
+
+int ScoreDisplay::getTextWidth(QString text) const
+{
+ // Make text width only depend on number of digits to avoid frequent small
+ // changes to the layout
+ QFontMetrics metrics(m_font);
+ int maxDigitWidth = 0;
+ maxDigitWidth = max(maxDigitWidth, metrics.width('0'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('1'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('2'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('3'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('4'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('5'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('6'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('7'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('8'));
+ maxDigitWidth = max(maxDigitWidth, metrics.width('9'));
+ return max(text.length() * maxDigitWidth,
+ metrics.boundingRect(text).width());
+}
+
+void ScoreDisplay::paintEvent(QPaintEvent*)
+{
+ QPainter painter(this);
+ m_colorDotSize = static_cast<int>(0.8 * m_fontSize);
+ m_colorDotSpace = static_cast<int>(0.3 * m_fontSize);
+ m_colorDotWidth = m_colorDotSize + m_colorDotSpace;
+ m_twoColorDotWidth = 2 * m_colorDotSize + m_colorDotSpace;
+ auto nuColors = get_nu_colors(m_variant);
+ auto nuPlayers = get_nu_players(m_variant);
+ if (nuColors == 2)
+ {
+ int textWidthBlue = getScoreTextWidth(Color(0));
+ int textWidthGreen = getScoreTextWidth(Color(1));
+ int totalWidth = textWidthBlue + textWidthGreen + 2 * m_colorDotWidth;
+ qreal pad = qreal(width() - totalWidth) / 3.f;
+ qreal x = pad;
+ drawScore(painter, Color(0), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthBlue + pad;
+ drawScore(painter, Color(1), static_cast<int>(x));
+ }
+ else if (nuColors == 4 && nuPlayers == 4)
+ {
+ int textWidthBlue = getScoreTextWidth(Color(0));
+ int textWidthYellow = getScoreTextWidth(Color(1));
+ int textWidthRed = getScoreTextWidth(Color(2));
+ int textWidthGreen = getScoreTextWidth(Color(3));
+ int totalWidth =
+ textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen
+ + 4 * m_colorDotWidth;
+ qreal pad = qreal(width() - totalWidth) / 5.f;
+ qreal x = pad;
+ drawScore(painter, Color(0), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthBlue + pad;
+ drawScore(painter, Color(1), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthYellow + pad;
+ drawScore(painter, Color(2), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthRed + pad;
+ drawScore(painter, Color(3), static_cast<int>(x));
+ }
+ else if (nuColors == 4 && nuPlayers == 3)
+ {
+ int textWidthBlue = getScoreTextWidth(Color(0));
+ int textWidthYellow = getScoreTextWidth(Color(1));
+ int textWidthRed = getScoreTextWidth(Color(2));
+ int textWidthGreen = getScoreTextWidth3();
+ int totalWidth =
+ textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen
+ + 3 * m_colorDotWidth + m_twoColorDotWidth;
+ qreal pad = qreal(width() - totalWidth) / 5.f;
+ qreal x = pad;
+ drawScore(painter, Color(0), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthBlue + pad;
+ drawScore(painter, Color(1), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthYellow + pad;
+ drawScore(painter, Color(2), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthRed + pad;
+ drawScore3(painter, static_cast<int>(x));
+ }
+ else if (nuColors == 3 && nuPlayers == 3)
+ {
+ int textWidthBlue = getScoreTextWidth(Color(0));
+ int textWidthYellow = getScoreTextWidth(Color(1));
+ int textWidthRed = getScoreTextWidth(Color(2));
+ int totalWidth =
+ textWidthBlue + textWidthRed + textWidthYellow
+ + 3 * m_colorDotWidth;
+ qreal pad = qreal(width() - totalWidth) / 4.f;
+ qreal x = pad;
+ drawScore(painter, Color(0), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthBlue + pad;
+ drawScore(painter, Color(1), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthYellow + pad;
+ drawScore(painter, Color(2), static_cast<int>(x));
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(nuColors == 4 && nuPlayers == 2);
+ int textWidthBlueRed = getScoreTextWidth2(Color(0), Color(2));
+ int textWidthYellowGreen = getScoreTextWidth2(Color(1), Color(3));
+ int textWidthBlue = getScoreTextWidth(Color(0));
+ int textWidthYellow = getScoreTextWidth(Color(1));
+ int textWidthRed = getScoreTextWidth(Color(2));
+ int textWidthGreen = getScoreTextWidth(Color(3));
+ int totalWidth =
+ textWidthBlueRed + textWidthYellowGreen
+ + textWidthBlue + textWidthRed + textWidthYellow + textWidthGreen
+ + 2 * m_twoColorDotWidth + 4 * m_colorDotWidth;
+ qreal pad = qreal(width() - totalWidth) / 7.f;
+ qreal x = pad;
+ drawScore2(painter, Color(0), Color(2), static_cast<int>(x));
+ x += m_twoColorDotWidth + textWidthBlueRed + pad;
+ drawScore2(painter, Color(1), Color(3), static_cast<int>(x));
+ x += m_twoColorDotWidth + textWidthYellowGreen + pad;
+ drawScore(painter, Color(0), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthBlue + pad;
+ drawScore(painter, Color(1), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthYellow + pad;
+ drawScore(painter, Color(2), static_cast<int>(x));
+ x += m_colorDotWidth + textWidthRed + pad;
+ drawScore(painter, Color(3), static_cast<int>(x));
+ }
+}
+
+void ScoreDisplay::resizeEvent(QResizeEvent*)
+{
+ // QFont::setPixelSize(0) prints a warning even if it works and the docs
+ // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)).
+ m_fontSize = max(1, static_cast<int>(floor(0.6 * height())));
+ m_font.setPixelSize(m_fontSize);
+}
+
+void ScoreDisplay::updateScore(const Board& bd)
+{
+ auto variant = bd.get_variant();
+ bool hasChanged = (m_variant != variant);
+ m_variant = variant;
+ for (Color c : bd.get_colors())
+ {
+ bool hasMoves = bd.has_moves(c);
+ auto points = bd.get_points(c);
+ auto bonus = bd.get_bonus(c);
+ if (hasMoves != m_hasMoves[c] || m_points[c] != points
+ || m_bonus[c] != points)
+ {
+ hasChanged = true;
+ m_hasMoves[c] = hasMoves;
+ m_points[c] = points;
+ m_bonus[c] = bonus;
+ }
+ }
+ if (variant == Variant::classic_3)
+ m_altPlayer = Color(bd.get_alt_player());
+ if (hasChanged)
+ update();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/ScoreDisplay.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_SCORE_DISPLAY_H
+#define LIBPENTOBI_GUI_SCORE_DISPLAY_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QWidget>
+#include "libpentobi_base/Board.h"
+
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMap;
+using libpentobi_base::ScoreType;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class ScoreDisplay
+ : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ScoreDisplay(QWidget* parent = nullptr);
+
+ void updateScore(const Board& bd);
+
+protected:
+ void paintEvent(QPaintEvent* event) override;
+
+ void resizeEvent(QResizeEvent* event) override;
+
+private:
+ int m_fontSize;
+
+ QFont m_font;
+
+ Variant m_variant;
+
+ ColorMap<bool> m_hasMoves{false};
+
+ ColorMap<ScoreType> m_points{0};
+
+ ColorMap<ScoreType> m_bonus{0};
+
+ /** Current player of 4th color in Variant::classic_3. */
+ Color m_altPlayer;
+
+ int m_colorDotSize;
+
+ int m_colorDotSpace;
+
+ int m_colorDotWidth;
+
+ int m_twoColorDotWidth;
+
+
+ QString getScoreText(Color c);
+
+ QString getScoreText2(Color c1, Color c2);
+
+ QString getScoreText3();
+
+ int getScoreTextWidth(Color c);
+
+ int getScoreTextWidth2(Color c1, Color c2);
+
+ int getScoreTextWidth3();
+
+ void drawScore(QPainter& painter, Color c, int x);
+
+ void drawScore2(QPainter& painter, Color c1, Color c2, int x);
+
+ void drawScore3(QPainter& painter, int x);
+
+ QString getScoreText(ScoreType points, ScoreType bonus) const;
+
+ int getTextWidth(QString text) const;
+
+ void drawText(QPainter& painter, const QString& text, int x, int y,
+ bool underline, bool hasBonus);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_SCORE_DISPLAY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/Util.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Util.h"
+
+#include <QCoreApplication>
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const QColor blue(0, 115, 207);
+
+const QColor green(0, 192, 0);
+
+const QColor red(230, 62, 44);
+
+const QColor yellow(235, 205, 35);
+
+const QColor gray(174, 167, 172);
+
+void setAlphaSaturation(QColor& c, qreal alpha, qreal saturation)
+{
+ if (saturation != 1)
+ c.setHsv(c.hue(), static_cast<int>(saturation * c.saturation()),
+ c.value());
+ if (alpha != 1)
+ c.setAlphaF(alpha);
+}
+
+void paintDot(QPainter& painter, QColor color, qreal x, qreal y, qreal width,
+ qreal height, qreal size)
+{
+ painter.save();
+ painter.translate(x, y);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ painter.drawEllipse(QPointF(0.5 * width, 0.5 * height), size, size);
+ painter.restore();
+}
+
+void paintSquare(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& rectColor,
+ const QColor& upLeftColor, const QColor& downRightColor,
+ bool onlyBorder = false)
+{
+ painter.save();
+ painter.translate(x, y);
+ if (! onlyBorder)
+ painter.fillRect(QRectF(0, 0, width, height), rectColor);
+ qreal border = 0.05 * max(width, height);
+ const QPointF downRightPolygon[6] =
+ {
+ QPointF(border, height - border),
+ QPointF(width - border, height - border),
+ QPointF(width - border, border),
+ QPointF(width, 0),
+ QPointF(width, height),
+ QPointF(0, height)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(downRightColor);
+ painter.drawPolygon(downRightPolygon, 6);
+ const QPointF upLeftPolygon[6] =
+ {
+ QPointF(0, 0),
+ QPointF(width, 0),
+ QPointF(width - border, border),
+ QPointF(border, border),
+ QPointF(border, height - border),
+ QPointF(0, height)
+ };
+ painter.setBrush(upLeftColor);
+ painter.drawPolygon(upLeftPolygon, 6);
+ painter.restore();
+}
+
+void paintTriangle(QPainter& painter, bool isUpward, qreal x, qreal y,
+ qreal width, qreal height, const QColor& color,
+ const QColor& upLeftColor, const QColor& downRightColor)
+{
+ painter.save();
+ painter.translate(x, y);
+ qreal left = -0.5 * width;
+ qreal right = 1.5 * width;
+ if (isUpward)
+ {
+ const QPointF polygon[3] =
+ {
+ QPointF(left, height),
+ QPointF(right, height),
+ QPointF(0.5 * width, 0)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ painter.drawConvexPolygon(polygon, 3);
+ qreal border = 0.08 * width;
+ const QPointF downRightPolygon[6] =
+ {
+ QPointF(left, height),
+ QPointF(right, height),
+ QPointF(0.5 * width, 0),
+ QPointF(0.5 * width, 2 * border),
+ QPointF(right - 1.732 * border, height - border),
+ QPointF(left + 1.732 * border, height - border)
+ };
+ painter.setBrush(downRightColor);
+ painter.drawPolygon(downRightPolygon, 6);
+ const QPointF upLeftPolygon[4] =
+ {
+ QPointF(0.5 * width, 0),
+ QPointF(0.5 * width, 2 * border),
+ QPointF(left + 1.732 * border, height - border),
+ QPointF(left, height),
+ };
+ painter.setBrush(upLeftColor);
+ painter.drawPolygon(upLeftPolygon, 4);
+ }
+ else
+ {
+ const QPointF polygon[3] =
+ {
+ QPointF(left, 0),
+ QPointF(right, 0),
+ QPointF(0.5 * width, height)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ painter.drawConvexPolygon(polygon, 3);
+ qreal border = 0.05 * width;
+ const QPointF downRightPolygon[4] =
+ {
+ QPointF(0.5 * width, height),
+ QPointF(0.5 * width, height - 2 * border),
+ QPointF(right - 1.732 * border, border),
+ QPointF(right, 0)
+ };
+ painter.setBrush(downRightColor);
+ painter.drawPolygon(downRightPolygon, 4);
+ const QPointF upLeftPolygon[6] =
+ {
+ QPointF(right, 0),
+ QPointF(right - 1.732 * border, border),
+ QPointF(left + 1.732 * border, border),
+ QPointF(0.5 * width, height - 2 * border),
+ QPointF(0.5 * width, height),
+ QPointF(left, 0)
+ };
+ painter.setBrush(upLeftColor);
+ painter.drawPolygon(upLeftPolygon, 6);
+ }
+ painter.restore();
+}
+
+void paintSquareFrame(QPainter& painter, qreal x, qreal y, qreal size,
+ const QColor& rectColor, const QColor& upLeftColor,
+ const QColor& downRightColor)
+{
+ painter.save();
+ painter.translate(x, y);
+ painter.setPen(Qt::NoPen);
+ qreal border = 0.05 * size;
+ qreal frameSize = 0.17 * size;
+ painter.fillRect(QRectF(0, 0, size, frameSize), rectColor);
+ painter.fillRect(QRectF(0, size - frameSize, size, frameSize), rectColor);
+ painter.fillRect(QRectF(0, 0, frameSize, size), rectColor);
+ painter.fillRect(QRectF(size - frameSize, 0, frameSize, size), rectColor);
+ const QPointF downRightPolygon[6] =
+ {
+ QPointF(border, size - border),
+ QPointF(size - border, size - border),
+ QPointF(size - border, border),
+ QPointF(size, 0),
+ QPointF(size, size),
+ QPointF(0, size)
+ };
+ painter.setBrush(downRightColor);
+ painter.drawPolygon(downRightPolygon, 6);
+ const QPointF upLeftPolygon[6] =
+ {
+ QPointF(0, 0),
+ QPointF(size, 0),
+ QPointF(size - border, border),
+ QPointF(border, border),
+ QPointF(border, size - border),
+ QPointF(0, size)
+ };
+ painter.setBrush(upLeftColor);
+ painter.drawPolygon(upLeftPolygon, 6);
+ painter.restore();
+}
+
+void paintColorSquareFrame(QPainter& painter, Variant variant, Color c,
+ qreal x, qreal y, qreal size, qreal alpha,
+ qreal saturation, bool flat)
+{
+ auto color = Util::getPaintColor(variant, c);
+ QColor upLeftColor;
+ QColor downRightColor;
+ if (flat)
+ {
+ upLeftColor = color;
+ downRightColor = color;
+ }
+ else
+ {
+ upLeftColor = color.lighter(130);
+ downRightColor = color.darker(160);
+ }
+ setAlphaSaturation(color, alpha, saturation);
+ setAlphaSaturation(upLeftColor, alpha, saturation);
+ setAlphaSaturation(downRightColor, alpha, saturation);
+ paintSquareFrame(painter, x, y, size, color, upLeftColor, downRightColor);
+}
+
+} //namespace
+
+//-----------------------------------------------------------------------------
+
+string Util::convertSgfValueFromQString(const QString& value,
+ const string& charset)
+{
+ // Is there a way in Qt to support arbitrary Ascii-compatible text
+ // encodings? Currently, we only support UTF8 (used by Pentobi) and
+ // treat everything else as ISO-8859-1/Latin1 (the default for SGF)
+ // even if the charset property specifies some other encoding.
+ QString charsetToLower = QString(charset.c_str()).trimmed().toLower();
+ if (charsetToLower == "utf-8" || charsetToLower == "utf8")
+ return value.toUtf8().constData();
+ else
+ return value.toLatin1().constData();
+}
+
+QString Util::convertSgfValueToQString(const string& value,
+ const string& charset)
+{
+ // See comment in convertSgfValueFromQString() about supported encodings
+ QString charsetToLower = QString(charset.c_str()).trimmed().toLower();
+ if (charsetToLower == "utf-8" || charsetToLower == "utf8")
+ return QString::fromUtf8(value.c_str());
+ else
+ return QString::fromLatin1(value.c_str());
+}
+
+QColor Util::getLabelColor(Variant variant, PointState s)
+{
+ if (s.is_empty())
+ return Qt::black;
+ Color c = s.to_color();
+ QColor paintColor = getPaintColor(variant, c);
+ if (paintColor == yellow || paintColor == green)
+ return Qt::black;
+ else
+ return Qt::white;
+}
+
+QColor Util::getMarkColor(Variant variant, PointState s)
+{
+ if (s.is_empty())
+ return Qt::white;
+ Color c = s.to_color();
+ QColor paintColor = getPaintColor(variant, c);
+ if (paintColor == yellow || paintColor == green)
+ return QColor("#333333");
+ else
+ return Qt::white;
+}
+
+QColor Util::getPaintColor(Variant variant, Color c)
+{
+ if (get_nu_colors(variant) == 2)
+ return c == Color(0) ? blue : green;
+ else
+ {
+ if (c == Color(0))
+ return blue;
+ if (c == Color(1))
+ return yellow;
+ if (c == Color(2))
+ return red;
+ LIBBOARDGAME_ASSERT(c == Color(3));
+ return green;
+ }
+}
+
+QString Util::getPlayerString(Variant variant, Color c)
+{
+ auto i = c.to_int();
+ if (get_nu_colors(variant) == 2)
+ return i == 0 ? qApp->translate("Util", "Blue")
+ : qApp->translate("Util", "Green");
+ if (get_nu_players(variant) == 2)
+ return i == 0 || i == 2 ? qApp->translate("Util", "Blue/Red")
+ : qApp->translate("Util", "Yellow/Green");
+ if (i == 0)
+ return qApp->translate("Util", "Blue");
+ if (i == 1)
+ return qApp->translate("Util", "Yellow");
+ if (i == 2)
+ return qApp->translate("Util", "Red");
+ return qApp->translate("Util", "Green");
+}
+
+void Util::paintColorSegment(QPainter& painter, Variant variant, Color c,
+ bool isHorizontal, qreal x, qreal y, qreal size,
+ qreal alpha, qreal saturation, bool flat)
+{
+ auto color = getPaintColor(variant, c);
+ QColor upLeftColor;
+ QColor downRightColor;
+ if (flat)
+ {
+ upLeftColor = color;
+ downRightColor = color;
+ }
+ else
+ {
+ upLeftColor = color.lighter(130);
+ downRightColor = color.darker(160);
+ }
+ setAlphaSaturation(color, alpha, saturation);
+ setAlphaSaturation(upLeftColor, alpha, saturation);
+ setAlphaSaturation(downRightColor, alpha, saturation);
+ if (isHorizontal)
+ paintSquare(painter, x - size / 4, y + size / 4, 1.5 * size, size / 2,
+ color, upLeftColor, downRightColor);
+ else
+ paintSquare(painter, x + size / 4, y - size / 4, size / 2, 1.5 * size,
+ color, upLeftColor, downRightColor);
+}
+
+void Util::paintColorSquare(QPainter& painter, Variant variant, Color c,
+ qreal x, qreal y, qreal size, qreal alpha,
+ qreal saturation, bool flat)
+{
+ auto color = getPaintColor(variant, c);
+ QColor upLeftColor;
+ QColor downRightColor;
+ if (flat)
+ {
+ upLeftColor = color;
+ downRightColor = color;
+ }
+ else
+ {
+ upLeftColor = color.lighter(130);
+ downRightColor = color.darker(160);
+ }
+ setAlphaSaturation(color, alpha, saturation);
+ setAlphaSaturation(upLeftColor, alpha, saturation);
+ setAlphaSaturation(downRightColor, alpha, saturation);
+ paintSquare(painter, x, y, size, size, color, upLeftColor, downRightColor);
+}
+
+void Util::paintColorSquareCallisto(QPainter& painter, Variant variant,
+ Color c, qreal x, qreal y, qreal size,
+ bool hasRight, bool hasDown,
+ bool isOnePiece, qreal alpha,
+ qreal saturation, bool flat)
+{
+ auto color = getPaintColor(variant, c);
+ setAlphaSaturation(color, alpha, saturation);
+ if (hasRight)
+ painter.fillRect(QRectF(x + 0.96 * size, y + 0.07 * size,
+ 0.08 * size, 0.86 * size), color);
+ if (hasDown)
+ painter.fillRect(QRectF(x + 0.07 * size, y + 0.96 * size,
+ 0.86 * size, 0.08 * size), color);
+ if (isOnePiece)
+ paintColorSquareFrame(painter, variant, c, x + 0.04 * size,
+ y + 0.04 * size, 0.92 * size, alpha, saturation,
+ flat);
+ else
+ paintColorSquare(painter, variant, c, x + 0.04 * size, y + 0.04 * size,
+ 0.92 * size, alpha, saturation, flat);
+}
+
+void Util::paintColorTriangle(QPainter& painter, Variant variant,
+ Color c, bool isUpward, qreal x, qreal y,
+ qreal width, qreal height, qreal alpha,
+ qreal saturation, bool flat)
+{
+ auto color = getPaintColor(variant, c);
+ QColor upLeftColor;
+ QColor downRightColor;
+ if (flat)
+ {
+ upLeftColor = color;
+ downRightColor = color;
+ }
+ else
+ {
+ upLeftColor = color.lighter(130);
+ downRightColor = color.darker(160);
+ }
+ setAlphaSaturation(color, alpha, saturation);
+ setAlphaSaturation(upLeftColor, alpha, saturation);
+ setAlphaSaturation(downRightColor, alpha, saturation);
+ paintTriangle(painter, isUpward, x, y, width, height, color, upLeftColor,
+ downRightColor);
+}
+
+void Util::paintEmptyJunction(QPainter& painter, qreal x, qreal y, qreal size)
+{
+ painter.fillRect(QRectF(x + 0.25 * size, y + 0.25 * size,
+ 0.5 * size, 0.5 * size),
+ gray);
+}
+
+void Util::paintEmptySegment(QPainter& painter, bool isHorizontal, qreal x,
+ qreal y, qreal size)
+{
+ if (isHorizontal)
+ paintSquare(painter, x - size / 4, y + size / 4, 1.5 * size, size / 2,
+ gray, gray.darker(130), gray.lighter(115));
+ else
+ paintSquare(painter, x + size / 4, y - size / 4, size / 2, 1.5 * size,
+ gray, gray.darker(130), gray.lighter(115));
+}
+
+void Util::paintEmptySquare(QPainter& painter, qreal x, qreal y, qreal size)
+{
+ paintSquare(painter, x, y, size, size, gray, gray.darker(130),
+ gray.lighter(115));
+}
+
+void Util::paintEmptySquareCallisto(QPainter& painter, qreal x, qreal y,
+ qreal size)
+{
+ painter.fillRect(QRectF(x, y, size, size), gray);
+ paintSquare(painter, x + 0.04 * size, y + 0.04 * size, 0.92 * size,
+ 0.92 * size, gray, gray.darker(130), gray.lighter(115), true);
+}
+
+void Util::paintEmptySquareCallistoCenter(QPainter& painter, qreal x, qreal y,
+ qreal size)
+{
+ painter.fillRect(QRectF(x, y, size, size), gray);
+ paintSquare(painter, x + 0.05 * size, y + 0.05 * size, 0.9 * size,
+ 0.9 * size, gray.darker(120), gray.darker(150),
+ gray.lighter(95), false);
+}
+
+void Util::paintEmptyTriangle(QPainter& painter, bool isUpward, qreal x,
+ qreal y, qreal width, qreal height)
+{
+ paintTriangle(painter, isUpward, x, y, width, height, gray,
+ gray.darker(130), gray.lighter(115));
+}
+
+void Util::paintJunction(QPainter& painter, Variant variant, Color c, qreal x,
+ qreal y, qreal width, qreal height, bool hasLeft,
+ bool hasRight, bool hasUp, bool hasDown, qreal alpha,
+ qreal saturation)
+{
+ auto color = getPaintColor(variant, c);
+ setAlphaSaturation(color, alpha, saturation);
+ painter.save();
+ painter.translate(x + 0.25 * width, y + 0.25 * height);
+ width *= 0.5;
+ height *= 0.5;
+ if (hasUp && hasDown)
+ painter.fillRect(QRectF(0.25 * width, 0, 0.5 * width, height), color);
+ if (hasLeft && hasRight)
+ painter.fillRect(QRectF(0, 0.25 * height, width, 0.5 * height), color);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ if (hasLeft && hasUp)
+ {
+ const QPointF polygon[3] = { QPointF(0, 0),
+ QPointF(0.75 * width, 0),
+ QPointF(0, 0.75 * height) };
+ painter.drawPolygon(polygon, 3);
+ }
+ if (hasRight && hasUp)
+ {
+ const QPointF polygon[3] = { QPointF(0.25 * width, 0),
+ QPointF(width, 0),
+ QPointF(width, 0.75 * height) };
+ painter.drawPolygon(polygon, 3);
+ }
+ if (hasLeft && hasDown)
+ {
+ const QPointF polygon[3] = { QPointF(0, 0.25 * height),
+ QPointF(0, height),
+ QPointF(0.75 * width, height) };
+ painter.drawPolygon(polygon, 3);
+ }
+ if (hasRight && hasDown)
+ {
+ const QPointF polygon[3] = { QPointF(0.25 * width, height),
+ QPointF(width, 0.25 * height),
+ QPointF(width, height) };
+ painter.drawPolygon(polygon, 3);
+ }
+ painter.restore();
+}
+
+void Util::paintSegmentStartingPoint(QPainter& painter, Variant variant,
+ Color c, qreal x, qreal y,
+ qreal size)
+{
+ paintDot(painter, getPaintColor(variant, c), x, y, size, size,
+ 0.15 * size);
+}
+
+void Util::paintSquareStartingPoint(QPainter& painter, Variant variant,
+ Color c, qreal x, qreal y, qreal size)
+{
+ paintDot(painter, getPaintColor(variant, c), x, y, size, size,
+ 0.13 * size);
+}
+
+void Util::paintTriangleStartingPoint(QPainter& painter, bool isUpward,
+ qreal x, qreal y, qreal width,
+ qreal height)
+{
+ if (isUpward)
+ y += 0.333 * height;
+ height = 0.666 * height;
+ paintDot(painter, gray.darker(130), x, y, width, height, 0.17 * width);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_gui/Util.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_GUI_UTIL_H
+#define LIBPENTOBI_GUI_UTIL_H
+
+#include <QColor>
+#include <QPainter>
+#include "libpentobi_base/Color.h"
+#include "libpentobi_base/Variant.h"
+#include "libpentobi_base/PointState.h"
+
+using namespace std;
+using libpentobi_base::Color;
+using libpentobi_base::Variant;
+using libpentobi_base::PointState;
+
+//-----------------------------------------------------------------------------
+
+namespace Util
+{
+
+QColor getPaintColor(Variant variant, Color c);
+
+QColor getLabelColor(Variant variant, PointState s);
+
+QColor getMarkColor(Variant variant, PointState s);
+
+/** Paint piece segment in Nexos. */
+void paintColorSegment(QPainter& painter, Variant variant, Color c,
+ bool isHorizontal, qreal x, qreal y, qreal size,
+ qreal alpha = 1, qreal saturation = 1,
+ bool flat = false);
+
+void paintColorSquare(QPainter& painter, Variant variant, Color c,
+ qreal x, qreal y, qreal size, qreal alpha = 1,
+ qreal saturation = 1, bool flat = false);
+
+void paintColorSquareCallisto(QPainter& painter, Variant variant, Color c,
+ qreal x, qreal y, qreal size, bool hasRight,
+ bool hasDown, bool isOnePiece, qreal alpha = 1,
+ qreal saturation = 1, bool flat = false);
+
+void paintColorTriangle(QPainter& painter, Variant variant,
+ Color c, bool isUpward, qreal x, qreal y, qreal width,
+ qreal height, qreal alpha = 1, qreal saturation = 1,
+ bool flat = false);
+
+/** Paint empty junction in Nexos. */
+void paintEmptyJunction(QPainter& painter, qreal x, qreal y, qreal size);
+
+/** Paint empty segment in Nexos. */
+void paintEmptySegment(QPainter& painter, bool isHorizontal, qreal x, qreal y,
+ qreal size);
+
+void paintEmptySquare(QPainter& painter, qreal x, qreal y, qreal size);
+
+void paintEmptySquareCallisto(QPainter& painter, qreal x, qreal y, qreal size);
+
+void paintEmptySquareCallistoCenter(QPainter& painter, qreal x, qreal y,
+ qreal size);
+
+void paintEmptyTriangle(QPainter& painter, bool isUpward, qreal x, qreal y,
+ qreal width, qreal height);
+
+void paintJunction(QPainter& painter, Variant variant, Color c, qreal x,
+ qreal y, qreal width, qreal height, bool hasLeft,
+ bool hasRight, bool hasUp, bool hasDown, qreal alpha = 1,
+ qreal saturation = 1);
+
+/** Paint starting point in Nexos. */
+void paintSegmentStartingPoint(QPainter& painter, Variant variant, Color c,
+ qreal x, qreal y, qreal size);
+
+void paintSquareStartingPoint(QPainter& painter, Variant variant, Color c,
+ qreal x, qreal y, qreal size);
+
+void paintTriangleStartingPoint(QPainter& painter, bool isUpward, qreal x,
+ qreal y, qreal width, qreal height);
+
+/** Convert a property value of a SGF tree unto a QString.
+ @param value
+ @param charset The value of the CA property of the root node in the tree
+ or an empty string if the tree has no such property.
+ This function currently only recognizes UTF8 and ISO-8859-1 (the latter
+ is the default for SGF if no CA property exists). Other charsets are
+ ignored and the string is converted using the default system charset. */
+string convertSgfValueFromQString(const QString& value, const string& charset);
+
+/** Convert a property value of a SGF tree unto a QString.
+ @param value
+ @param charset The value of the CA property of the root node in the tree
+ or an empty string if the tree has no such property.
+ This function currently only recognizes UTF8 and ISO-8859-1 (the latter
+ is the default for SGF if no CA property exists). Other charsets are
+ ignored and the string is converted using the default system charset. */
+QString convertSgfValueToQString(const string& value, const string& charset);
+
+/** Get a translated string identifying a player, like "Blue" or "Blue/Red".
+ @param variant The game variant
+ @param c The player color or one of the player colors in game variants
+ with multiple colors per player. */
+QString getPlayerString(Variant variant, Color c);
+
+}
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_GUI_UTIL_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <g stroke-linejoin="round" stroke="#2e3436">
+ <path d="m11 2-7.5 8.5v9h15v-9z" fill="#babdb6"/>
+ <path d="m11 1.5-10 10 1.53 1.34 8.47-8.34 8.5 8.38 1.5-1.38z" fill="#babdb6"/>
+ <rect x="8.5" y="12.5" width="5" height="7" fill="none"/>
+ </g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" d="m11.5 2.5v4h-10v9h10v4l8.5-8.5z" stroke="#2e3436" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" d="m10.5 2.5v4h10v9h-10v4l-8.5-8.5z" stroke="#2e3436" fill="#babdb6"/>
+</svg>
--- /dev/null
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/libpentobi_gui">
+ <file>icons/go-home.png</file>
+ <file>icons/go-next.png</file>
+ <file>icons/go-previous.png</file>
+ </qresource>
+</RCC>
--- /dev/null
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/libpentobi_gui">
+ <file>icons/go-home@2x.png</file>
+ <file>icons/go-next@2x.png</file>
+ <file>icons/go-previous@2x.png</file>
+ </qresource>
+</RCC>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+ <name>ComputerColorDialog</name>
+ <message>
+ <source>Computer Colors</source>
+ <translation>Computer-Farben</translation>
+ </message>
+ <message>
+ <source>Computer plays:</source>
+ <translation>Computer spielt:</translation>
+ </message>
+ <message>
+ <source>&Blue</source>
+ <translation>&Blau</translation>
+ </message>
+ <message>
+ <source>&Green</source>
+ <translation>&Grün</translation>
+ </message>
+ <message>
+ <source>&Yellow</source>
+ <translation>G&elb</translation>
+ </message>
+ <message>
+ <source>&Red</source>
+ <translation>&Rot</translation>
+ </message>
+ <message>
+ <source>&Blue/Red</source>
+ <translation>&Blau/Rot</translation>
+ </message>
+ <message>
+ <source>&Yellow/Green</source>
+ <translation>&Gelb/Grün</translation>
+ </message>
+</context>
+<context>
+ <name>GameInfoDialog</name>
+ <message>
+ <source>Game Info</source>
+ <translation>Spielinformation</translation>
+ </message>
+ <message>
+ <source>Player &Blue:</source>
+ <oldsource>Player Blue:</oldsource>
+ <translation>Spieler &Blau:</translation>
+ </message>
+ <message>
+ <source>Player &Green:</source>
+ <oldsource>Player Green:</oldsource>
+ <translation>Spieler &Grün:</translation>
+ </message>
+ <message>
+ <source>Player &Yellow:</source>
+ <oldsource>Player Yellow:</oldsource>
+ <translation>Spieler G&elb:</translation>
+ </message>
+ <message>
+ <source>Player &Red:</source>
+ <oldsource>Player Red:</oldsource>
+ <translation>Spieler &Rot:</translation>
+ </message>
+ <message>
+ <source>Player &Blue/Red:</source>
+ <oldsource>Player Blue/Red:</oldsource>
+ <translation>Spieler &Blau/Rot:</translation>
+ </message>
+ <message>
+ <source>Player &Yellow/Green:</source>
+ <oldsource>Player Yellow/Green:</oldsource>
+ <translation>Spieler &Gelb/Grün:</translation>
+ </message>
+ <message>
+ <source>&Date:</source>
+ <oldsource>Date:</oldsource>
+ <translation>&Datum:</translation>
+ </message>
+ <message>
+ <source>&Time limits:</source>
+ <translation>Bedenk&zeit:</translation>
+ </message>
+ <message>
+ <source>&Event:</source>
+ <translation>&Veranstaltung:</translation>
+ </message>
+ <message>
+ <source>R&ound:</source>
+ <translation>R&unde:</translation>
+ </message>
+</context>
+<context>
+ <name>HelpWindow</name>
+ <message>
+ <source>Back</source>
+ <translation>Zurück</translation>
+ </message>
+ <message>
+ <source>Show previous page in history</source>
+ <translation>Die vorherige Seite in der Chronik anzeigen</translation>
+ </message>
+ <message>
+ <source>Forward</source>
+ <translation>Vorwärts</translation>
+ </message>
+ <message>
+ <source>Show next page in history</source>
+ <translation>Die nächste Seite in der Chronik anzeigen</translation>
+ </message>
+ <message>
+ <source>Contents</source>
+ <translation>Inhalt</translation>
+ </message>
+ <message>
+ <source>Show table of contents</source>
+ <translation>Das Inhaltsverzeichnis anzeigen</translation>
+ </message>
+</context>
+<context>
+ <name>InitialRatingDialog</name>
+ <message>
+ <source>Initial Rating</source>
+ <translation>Anfangswertung</translation>
+ </message>
+ <message>
+ <source>You have not yet played rated games in this game variant. Estimate your playing strength to initialize your rating.</source>
+ <translation>Sie haben noch keine gewerteten Spiele in dieser Spielvariante gespielt. Schätzen Sie Ihre Spielstärke, um Ihre Wertung zu initialisieren.</translation>
+ </message>
+ <message>
+ <source>Beginner</source>
+ <translation>Anfänger</translation>
+ </message>
+ <message>
+ <source>Expert</source>
+ <translation>Experte</translation>
+ </message>
+ <message>
+ <source>Your initial rating: %1</source>
+ <translation>Ihre Anfangswertung: %1</translation>
+ </message>
+</context>
+<context>
+ <name>Util</name>
+ <message>
+ <source>Blue</source>
+ <translation>Blau</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Grün</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Gelb</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rot</translation>
+ </message>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Blau/Rot</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Gelb/Grün</translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+# libpentobi_kde_thumbnailer contains the files needed by
+# the pentobi_kde_thumbnailer plugin compiled with shared library options
+# (usually -fPIC) because this is required for building shared libraries on
+# some targets (e.g. x86_64).
+#
+# The alternative would be to add -fPIC to the global compiler flags even for
+# executables but this slows down Pentobi's search by 10% on some targets.
+#
+# Adding the source files directly to pentobi_kde_thumbnailer/CMakeList.txt is
+# not possible because the KDE CMake macros add -fno-exceptions to the
+# compiler flags, which causes errors in the Pentobi sources that use
+# exceptions (which should be fine as long as no exceptions are thrown
+# from the thumbnailer plugin functions).
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CMAKE_SHARED_LIBRARY_CXX_FLAGS}")
+
+add_library(pentobi_kde_thumbnailer STATIC
+ ../libboardgame_util/Assert.cpp
+ ../libboardgame_util/Log.cpp
+ ../libboardgame_util/StringUtil.cpp
+ ../libboardgame_base/StringRep.cpp
+ ../libboardgame_sgf/MissingProperty.cpp
+ ../libboardgame_sgf/Reader.cpp
+ ../libboardgame_sgf/SgfNode.cpp
+ ../libboardgame_sgf/SgfTree.cpp
+ ../libboardgame_sgf/TreeReader.cpp
+ ../libpentobi_base/CallistoGeometry.cpp
+ ../libpentobi_base/NexosGeometry.cpp
+ ../libpentobi_base/NodeUtil.cpp
+ ../libpentobi_base/StartingPoints.cpp
+ ../libpentobi_base/TrigonGeometry.cpp
+ ../libpentobi_base/Variant.cpp
+ ../libpentobi_gui/BoardPainter.cpp
+ ../libpentobi_gui/Util.cpp
+ ../libpentobi_thumbnail/CreateThumbnail.cpp
+)
+
+target_link_libraries(pentobi_kde_thumbnailer Qt5::Widgets)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/AnalyzeGame.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "AnalyzeGame.h"
+
+#include "Search.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/WallTimeSource.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_sgf::InvalidTree;
+using libboardgame_sgf::SgfNode;
+using libboardgame_util::clear_abort;
+using libboardgame_util::get_abort;
+using libboardgame_util::WallTimeSource;
+using libpentobi_base::BoardUpdater;
+
+//-----------------------------------------------------------------------------
+
+void AnalyzeGame::run(const Game& game, Search& search, size_t nu_simulations,
+ function<void(unsigned,unsigned)> progress_callback)
+{
+ m_variant = game.get_variant();
+ m_moves.clear();
+ m_values.clear();
+ auto& tree = game.get_tree();
+ unique_ptr<Board> bd(new Board(m_variant));
+ BoardUpdater updater;
+ auto& root = game.get_root();
+ auto node = &root;
+ unsigned total_moves = 0;
+ try {
+ while (node)
+ {
+ if (tree.has_move(*node))
+ ++total_moves;
+ node = node->get_first_child_or_null();
+ }
+ }
+ catch (const InvalidTree&)
+ {
+ // PentobiTree::has_move() can throw on invalid SGF tree read from
+ // external file. We simply abort the analysis.
+ return;
+ }
+ WallTimeSource time_source;
+ clear_abort();
+ node = &root;
+ unsigned move_number = 0;
+ auto tie_value = Search::SearchParamConst::tie_value;
+ while (node)
+ {
+ auto mv = tree.get_move(*node);
+ if (! mv.is_null())
+ {
+ if (! node->has_parent())
+ {
+ // Root shouldn't contain moves in SGF files
+ m_moves.push_back(mv);
+ m_values.push_back(tie_value);
+ }
+ else
+ {
+ progress_callback(move_number, total_moves);
+ try
+ {
+ updater.update(*bd, tree, node->get_parent());
+ LIBBOARDGAME_LOG("Analyzing move ", bd->get_nu_moves());
+ const Float max_count = Float(nu_simulations);
+ double max_time = 0;
+ // Set min_simulations to a reasonable value because
+ // nu_simulations can be reached without having that many
+ // value updates if a subtree from a previous search is
+ // reused (which re-initializes the value and value count
+ // of the new root from the best child)
+ size_t min_simulations = min(size_t(100), nu_simulations);
+ Move computer_mv;
+ search.search(computer_mv, *bd, mv.color, max_count,
+ min_simulations, max_time, time_source);
+ if (get_abort())
+ break;
+ m_moves.push_back(mv);
+ m_values.push_back(search.get_root_val().get_mean());
+ }
+ catch (const InvalidTree&)
+ {
+ // BoardUpdater::update() can throw on invalid SGF tree
+ // read from external file. We simply abort the analysis.
+ break;
+ }
+ }
+ ++move_number;
+ }
+ node = node->get_first_child_or_null();
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/AnalyzeGame.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_ANALYZE_GAME_H
+#define LIBPENTOBI_MCTS_ANALYZE_GAME_H
+
+#include <functional>
+#include <vector>
+#include "libpentobi_base/Game.h"
+
+namespace libpentobi_mcts {
+
+class Search;
+
+using namespace std;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Game;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Evaluate each position in the main variation of a game. */
+class AnalyzeGame
+{
+public:
+ /** Run the analysis.
+ The analysis can be aborted from a different thread with
+ libboardgame_util::set_abort().
+ @param game
+ @param search
+ @param nu_simulations
+ @param progress_callback Function that will be called at the beginning
+ of the analysis of a position. Arguments: number moves analyzed so far,
+ total number of moves. */
+ void run(const Game& game, Search& search, size_t nu_simulations,
+ function<void(unsigned,unsigned)> progress_callback);
+
+ Variant get_variant() const;
+
+ unsigned get_nu_moves() const;
+
+ ColorMove get_move(unsigned i) const;
+
+ double get_value(unsigned i) const;
+
+private:
+ Variant m_variant;
+
+ vector<ColorMove> m_moves;
+
+ vector<double> m_values;
+};
+
+inline ColorMove AnalyzeGame::get_move(unsigned i) const
+{
+ LIBBOARDGAME_ASSERT(i < m_moves.size());
+ return m_moves[i];
+}
+
+inline unsigned AnalyzeGame::get_nu_moves() const
+{
+ return static_cast<unsigned>(m_moves.size());
+}
+
+inline double AnalyzeGame::get_value(unsigned i) const
+{
+ LIBBOARDGAME_ASSERT(i < m_values.size());
+ return m_values[i];
+}
+
+inline Variant AnalyzeGame::get_variant() const
+{
+ return m_variant;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_ANALYZE_GAME_H
--- /dev/null
+add_library(pentobi_mcts STATIC
+ AnalyzeGame.h
+ AnalyzeGame.cpp
+ Float.h
+ History.h
+ History.cpp
+ Player.h
+ Player.cpp
+ PlayoutFeatures.h
+ PlayoutFeatures.cpp
+ PriorKnowledge.h
+ PriorKnowledge.cpp
+ SearchParamConst.h
+ SharedConst.h
+ SharedConst.cpp
+ Search.h
+ Search.cpp
+ State.h
+ State.cpp
+ StateUtil.h
+ StateUtil.cpp
+ Util.h
+ Util.cpp
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Float.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_FLOAT_H
+#define LIBPENTOBI_MCTS_FLOAT_H
+
+#include <type_traits>
+
+namespace libpentobi_mcts {
+
+//-----------------------------------------------------------------------------
+
+#ifdef LIBPENTOBI_MCTS_FLOAT_TYPE
+typedef LIBPENTOBI_MCTS_FLOAT_TYPE Float;
+#else
+typedef float Float;
+#endif
+
+static_assert(std::is_floating_point<Float>::value, "");
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_FLOAT_H
--- /dev/null
+//----------------------------------------------------------------------------
+/** @file libpentobi_mcts/History.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "History.h"
+
+#include "libpentobi_base/BoardUtil.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libpentobi_base::boardutil::get_current_position_as_setup;
+
+//----------------------------------------------------------------------------
+
+void History::get_as_setup(Variant& variant, Setup& setup) const
+{
+ LIBBOARDGAME_ASSERT(is_valid());
+ variant = m_variant;
+ unique_ptr<Board> bd(new Board(variant));
+ for (ColorMove mv : m_moves)
+ bd->play(mv);
+ get_current_position_as_setup(*bd, setup);
+}
+
+void History::init(const Board& bd, Color to_play)
+{
+ m_is_valid = true;
+ m_variant = bd.get_variant();
+ m_nu_colors = bd.get_nu_colors();
+ m_moves.clear();
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ m_moves.push_back(bd.get_move(i));
+ m_to_play = to_play;
+}
+
+bool History::is_followup(
+ const History& other,
+ ArrayList<Move, SearchParamConst::max_moves>& sequence) const
+{
+ if (! m_is_valid || ! other.m_is_valid || m_variant != other.m_variant
+ || m_moves.size() < other.m_moves.size())
+ return false;
+ unsigned i = 0;
+ for ( ; i < other.m_moves.size(); ++i)
+ if (m_moves[i] != other.m_moves[i])
+ return false;
+ sequence.clear();
+ Color to_play = other.m_to_play;
+ for ( ; i < m_moves.size(); ++i)
+ {
+ auto mv = m_moves[i];
+ while (mv.color != to_play)
+ {
+ sequence.push_back(Move::null());
+ to_play = to_play.get_next(m_nu_colors);
+ }
+ sequence.push_back(mv.move);
+ to_play = to_play.get_next(m_nu_colors);
+ }
+ while (m_to_play != to_play)
+ {
+ sequence.push_back(Move::null());
+ to_play = to_play.get_next(m_nu_colors);
+ }
+ return true;
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//----------------------------------------------------------------------------
+/** @file libpentobi_mcts/History.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_HISTORY_H
+#define LIBPENTOBI_MCTS_HISTORY_H
+
+#include "SearchParamConst.h"
+#include "libpentobi_base/Board.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_util::ArrayList;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Move;
+using libpentobi_base::Setup;
+using libpentobi_base::Variant;
+
+//----------------------------------------------------------------------------
+
+/** Identifier for board state including history.
+ This class can be used, for instance, to uniquely remember a board
+ position for reusing parts of previous computations. The state includes:
+ - the game variant
+ - the history of moves
+ - the color to play */
+class History
+{
+public:
+ /** Constructor.
+ The initial state is that the history does not correspond to any
+ valid position. */
+ History();
+
+ /** Initialize from a current board position and explicit color to play. */
+ void init(const Board& bd, Color to_play);
+
+ /** Clear the state.
+ A cleared state does not correspond to any valid position. */
+ void clear();
+
+ /** Check if the state corresponds to any valid position. */
+ bool is_valid() const;
+
+ /** Check if this position is a alternate-play followup to another one.
+ @param other The other position
+ @param[out] sequence The sequence leading from the other position to
+ this one. Pass (=null) moves are inserted to ensure alternating colors
+ (as required by libpentobi_mcts::Search.)
+ @return @c true If the position is a followup
+ */
+ bool is_followup(
+ const History& other,
+ ArrayList<Move, SearchParamConst::max_moves>& sequence) const;
+
+ /** Get the position of the board state as setup.
+ @pre is_valid()
+ @param[out] variant
+ @param[out] setup */
+ void get_as_setup(Variant& variant, Setup& setup) const;
+
+ Color get_to_play() const;
+
+private:
+ bool m_is_valid;
+
+ Color::IntType m_nu_colors;
+
+ Variant m_variant;
+
+ ArrayList<ColorMove, Board::max_game_moves> m_moves;
+
+ Color m_to_play;
+};
+
+inline History::History()
+{
+ clear();
+}
+
+inline void History::clear()
+{
+ m_is_valid = false;
+}
+
+inline Color History::get_to_play() const
+{
+ LIBBOARDGAME_ASSERT(m_is_valid);
+ return m_to_play;
+}
+
+inline bool History::is_valid() const
+{
+ return m_is_valid;
+}
+
+//----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_HISTORY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Player.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Player.h"
+
+#include <fstream>
+#include <iomanip>
+#include "libboardgame_util/CpuTimeSource.h"
+#include "libboardgame_util/WallTimeSource.h"
+#include "libboardgame_sys/Memory.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_util::CpuTimeSource;
+using libboardgame_util::WallTimeSource;
+using libpentobi_base::BoardType;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+// Rationale for choosing the number of simulations:
+// * Level 9, the highest in the desktop version, should be as strong as
+// possible on a mid-range PC with reasonable thinking times. The average
+// time per game and player is targeted at 2-3 min for the 2-color game
+// variants and 5-6 min for the others.
+// * Level 7, the highest in the Android version, should be as strong as
+// possible on typical mobile hardware. It is set to 4% of level 9.
+// * Level 8 is set to 20% of level 9, the middle (on a log scale) between
+// level 7 and 9. Since most parameter tuning is done at level 7 or 8, it is
+// better for development purposes to define level 8 in terms of time, even
+// if it doesn't necessarily correspond to the middle wrt. playing strength.
+// * The numbers for level 1 are set to a value that is weak enough for
+// beginners without playing silly moves. They are currently chosen depending
+// on how strong we estimate Pentobi is in a game variant. It is also taken
+// into consideration how much the Elo difference level 1-9 is in self-play
+// experiments. After applying the scale factor (see comment in
+// Player::get_rating()), we want a range of about 1000 Elo (difference
+// between beginner and lower master level).
+// * The numbers for level 1-6 are chosen such that they correspond to roughly
+// equidistant Elo differences measured in self-play experiments.
+
+static const float counts_classic[Player::max_supported_level] =
+ { 3, 18, 75, 311, 1260, 8949, 66179, 330894, 1654470 };
+
+static const float counts_duo[Player::max_supported_level] =
+ { 3, 14, 63, 253, 2203, 13614, 202994, 1014969, 5074843 };
+
+static const float counts_trigon[Player::max_supported_level] =
+ { 228, 376, 733, 1214, 2606, 6802, 18428, 92138, 460691 };
+
+static const float counts_nexos[Player::max_supported_level] =
+ { 250, 347, 625, 1223, 3117, 8270, 22626, 113130, 565651 };
+
+static const float counts_callisto_2[Player::max_supported_level] =
+ { 100, 192, 405, 1079, 3323, 12258, 94104, 470522, 2352609 };
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+Player::Player(Variant initial_variant, unsigned max_level, string books_dir,
+ unsigned nu_threads)
+ : m_is_book_loaded(false),
+ m_use_book(true),
+ m_resign(false),
+ m_books_dir(move(books_dir)),
+ m_max_level(max_level),
+ m_level(4),
+ m_fixed_simulations(0),
+ m_resign_threshold(0.09f),
+ m_resign_min_simulations(500),
+ m_search(initial_variant, nu_threads, get_memory()),
+ m_book(initial_variant),
+ m_time_source(new WallTimeSource)
+{
+ for (unsigned i = 0; i < Board::max_player_moves; ++i)
+ {
+ // Hand-tuned such that time per move is more evenly spread among all
+ // moves than with a fixed number of simulations (because the
+ // simulations per second increase rapidly with the move number) but
+ // the average time per game is roughly the same.
+ m_weight_max_count_duo[i] = 0.7f * exp(0.1f * static_cast<float>(i));
+ m_weight_max_count_classic[i] = m_weight_max_count_duo[i];
+ m_weight_max_count_trigon[i] = m_weight_max_count_duo[i];
+ m_weight_max_count_callisto[i] = m_weight_max_count_duo[i];
+ m_weight_max_count_callisto_2[i] = m_weight_max_count_duo[i];
+ // Less weight for the first move(s) because number of legal moves
+ // is lower and the search applies some pruning rules to reduce the
+ // branching factor in early moves
+ if (i == 0)
+ {
+ m_weight_max_count_classic[i] *= 0.2f;
+ m_weight_max_count_trigon[i] *= 0.2f;
+ m_weight_max_count_duo[i] *= 0.6f;
+ m_weight_max_count_callisto[i] *= 0.2f;
+ m_weight_max_count_callisto_2[i] *= 0.2f;
+ }
+ else if (i == 1)
+ {
+ m_weight_max_count_classic[i] *= 0.2f;
+ m_weight_max_count_trigon[i] *= 0.5f;
+ m_weight_max_count_callisto[i] *= 0.6f;
+ m_weight_max_count_callisto_2[i] *= 0.2f;
+ }
+ else if (i == 2)
+ {
+ m_weight_max_count_classic[i] *= 0.3f;
+ m_weight_max_count_trigon[i] *= 0.6f;
+ }
+ else if (i == 3)
+ {
+ m_weight_max_count_trigon[i] *= 0.8f;
+ }
+ }
+}
+
+Player::~Player() = default;
+
+Move Player::genmove(const Board& bd, Color c)
+{
+ m_resign = false;
+ if (! bd.has_moves(c))
+ return Move::null();
+ Move mv;
+ auto variant = bd.get_variant();
+ auto board_type = bd.get_board_type();
+ auto level = min(max(m_level, 1u), m_max_level);
+ // Don't use more than 2 moves per color from opening book in lower levels
+ if (m_use_book
+ && (level >= 4 || bd.get_nu_moves() < 2u * bd.get_nu_colors()))
+ {
+ if (! is_book_loaded(variant))
+ load_book(m_books_dir
+ + "/book_" + to_string_id(variant) + ".blksgf");
+ if (m_is_book_loaded)
+ {
+ mv = m_book.genmove(bd, c);
+ if (! mv.is_null())
+ return mv;
+ }
+ }
+ Float max_count = 0;
+ double max_time = 0;
+ if (m_fixed_simulations > 0)
+ max_count = m_fixed_simulations;
+ else if (m_fixed_time > 0)
+ max_time = m_fixed_time;
+ else
+ {
+ switch (board_type)
+ {
+ case BoardType::classic:
+ max_count = counts_classic[level - 1];
+ break;
+ case BoardType::duo:
+ max_count = counts_duo[level - 1];
+ break;
+ case BoardType::trigon:
+ case BoardType::trigon_3:
+ case BoardType::callisto:
+ case BoardType::callisto_3:
+ max_count = counts_trigon[level - 1];
+ break;
+ case BoardType::nexos:
+ max_count = counts_nexos[level - 1];
+ break;
+ case BoardType::callisto_2:
+ max_count = counts_callisto_2[level - 1];
+ break;
+ }
+ // Don't weight max_count in low levels, otherwise it is still too
+ // strong for beginners (later in the game, the weight becomes much
+ // greater than 1 because the simulations become very fast)
+ bool weight_max_count = (level >= 4);
+ if (weight_max_count)
+ {
+ auto player_move = bd.get_nu_onboard_pieces(c);
+ float weight = 1; // Init to avoid compiler warning
+ switch (board_type)
+ {
+ case BoardType::classic:
+ weight = m_weight_max_count_classic[player_move];
+ break;
+ case BoardType::duo:
+ weight = m_weight_max_count_duo[player_move];
+ break;
+ case BoardType::callisto:
+ case BoardType::callisto_3:
+ weight = m_weight_max_count_callisto[player_move];
+ break;
+ case BoardType::callisto_2:
+ weight = m_weight_max_count_callisto_2[player_move];
+ break;
+ case BoardType::trigon:
+ case BoardType::trigon_3:
+ case BoardType::nexos:
+ weight = m_weight_max_count_trigon[player_move];
+ break;
+ }
+ max_count = ceil(max_count * weight);
+ }
+ }
+ if (max_count != 0)
+ LIBBOARDGAME_LOG("MaxCnt ", fixed, setprecision(0), max_count);
+ else
+ LIBBOARDGAME_LOG("MaxTime ", max_time);
+ if (! m_search.search(mv, bd, c, max_count, 0, max_time, *m_time_source))
+ return Move::null();
+ // Resign only in two-player game variants
+ if (get_nu_players(variant) == 2)
+ if (m_search.get_root_visit_count() > m_resign_min_simulations
+ && m_search.get_root_val().get_mean() < m_resign_threshold)
+ m_resign = true;
+ return mv;
+}
+
+/** Suggest how much memory to use for the trees depending on the maximum
+ level used. */
+size_t Player::get_memory()
+{
+ size_t available = libboardgame_sys::get_memory();
+ if (available == 0)
+ {
+ LIBBOARDGAME_LOG("WARNING: could not determine system memory"
+ " (assuming 512MB)");
+ available = 512000000;
+ }
+ // Don't use all of the available memory
+#if PENTOBI_LOW_RESOURCES
+ size_t reasonable = available / 4;
+#else
+ size_t reasonable = available / 3;
+#endif
+ size_t wanted = 2000000000;
+ if (m_max_level < max_supported_level)
+ {
+ // We don't need so much memory if m_max_level is smaller than
+ // max_supported_level. Trigon has the highest relative number of
+ // simulations on lower levels compared to the highest level. The
+ // memory used in a search is not proportional to the number of
+ // simulations (e.g. because the expand threshold increases with the
+ // depth). We approximate this by adding an exponent to the ratio
+ // and not taking into account if m_max_level is very small.
+ LIBBOARDGAME_ASSERT(max_supported_level >= 5);
+ auto factor = pow(counts_trigon[max_supported_level - 1]
+ / counts_trigon[max(m_max_level, 5u) - 1], 0.8);
+ wanted = static_cast<size_t>(double(wanted) / factor);
+ }
+ size_t memory = min(wanted, reasonable);
+ LIBBOARDGAME_LOG("Using ", memory / 1000000, " MB of ",
+ available / 1000000, " MB");
+ return memory;
+}
+
+Rating Player::get_rating(Variant variant, unsigned level)
+{
+ // The ratings are roughly based on Elo differences measured in self-play
+ // experiments. The measured values are scaled with a factor smaller than 1
+ // to take into account that self-play usually overestimates the strength
+ // against humans. The anchor is set to about 1000 (beginner level) for
+ // level 1. The exact value for anchor and scale is chosen according to our
+ // estimate how strong Pentobi plays at level 1 and level 9 in each game
+ // variant (2000 Elo would be lower expert level). Currently, only 2-player
+ // variants are tested and the ratings are used for multi-player variants
+ // on the same board.
+ auto max_supported_level = Player::max_supported_level;
+ level = min(max(level, 1u), max_supported_level);
+ Rating result;
+ switch (get_board_type(variant))
+ {
+ case BoardType::classic:
+ {
+ // Anchor 1000, scale 0.63
+ static float elo[Player::max_supported_level] =
+ { 1000, 1145, 1290, 1435, 1580, 1725, 1870, 1957, 2021 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::duo:
+ {
+ // Anchor 1100, scale 0.7
+ static float elo[Player::max_supported_level] =
+ { 1100, 1269, 1438, 1607, 1776, 1945, 2114, 2165, 2209 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::callisto_2:
+ {
+ // Anchor 1000, scale 0.63
+ static float elo[Player::max_supported_level] =
+ { 1000, 1101, 1203, 1304, 1405, 1507, 1608, 1673, 1756 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::trigon:
+ case BoardType::trigon_3:
+ {
+ // Anchor 1000, scale 0.60
+ static float elo[Player::max_supported_level] =
+ { 1000, 1103, 1206, 1308, 1411, 1514, 1617, 1757, 1856 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::nexos:
+ case BoardType::callisto: // Not measured
+ case BoardType::callisto_3: // Not measured
+ {
+ // Anchor 1000, scale 0.60
+ static float elo[Player::max_supported_level] =
+ { 1000, 1101, 1202, 1304, 1406, 1507, 1608, 1698, 1799 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ }
+ return result;
+}
+
+bool Player::is_book_loaded(Variant variant) const
+{
+ return m_is_book_loaded && m_book.get_tree().get_variant() == variant;
+}
+
+void Player::load_book(istream& in)
+{
+ m_book.load(in);
+ m_is_book_loaded = true;
+}
+
+bool Player::load_book(const string& filepath)
+{
+ ifstream in(filepath);
+ if (! in)
+ {
+ LIBBOARDGAME_LOG("Could not load book ", filepath);
+ return false;
+ }
+ m_book.load(in);
+ m_is_book_loaded = true;
+ LIBBOARDGAME_LOG("Loaded book ", filepath);
+ return true;
+}
+
+bool Player::resign() const
+{
+ return m_resign;
+}
+
+void Player::use_cpu_time(bool enable)
+{
+ if (enable)
+ m_time_source.reset(new CpuTimeSource);
+ else
+ m_time_source.reset(new WallTimeSource);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Player.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_PLAYER_H
+#define LIBPENTOBI_MCTS_PLAYER_H
+
+#include "Search.h"
+#include "libboardgame_base/Rating.h"
+#include "libpentobi_base/Book.h"
+#include "libpentobi_base/PlayerBase.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_base::Rating;
+using libpentobi_base::Book;
+using libpentobi_base::PlayerBase;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class Player final
+ : public PlayerBase
+{
+public:
+ static const unsigned max_supported_level = 9;
+
+ /** Constructor.
+ @param initial_variant Game variant to initialize the internal
+ board with (may avoid unnecessary BoardConst creation for game variant
+ that is never used)
+ @param max_level The maximum level used
+ @param books_dir Directory containing opening books.
+ @param nu_threads The number of threads to use in the search (0 means
+ to select a reasonable default value) */
+ Player(Variant initial_variant, unsigned max_level, string books_dir, unsigned nu_threads = 0);
+
+ ~Player();
+
+ Move genmove(const Board& bd, Color c) override;
+
+ bool resign() const override;
+
+ Float get_fixed_simulations() const;
+
+ double get_fixed_time() const;
+
+ /** Use a fixed number of simulations in the search.
+ If set to a value greater than zero, this value will enforce a
+ fixed number of simulations per search independent of the playing
+ level. */
+ void set_fixed_simulations(Float n);
+
+ /** Use a fixed time limit per move.
+ If set to a value greater than zero, this value will set a fixed
+ (maximum) time per search independent of the playing level. */
+ void set_fixed_time(double seconds);
+
+ bool get_use_book() const;
+
+ void set_use_book(bool enable);
+
+ unsigned get_level() const;
+
+ void set_level(unsigned level);
+
+ /** Use CPU time instead of Wall time to measure time. */
+ void use_cpu_time(bool enable);
+
+ Search& get_search();
+
+ void load_book(istream& in);
+
+ /** Is a book loaded and compatible with a given game variant? */
+ bool is_book_loaded(Variant variant) const;
+
+ /** Get an estimated Elo-rating of a level.
+ This rating is an estimated rating when playing vs. humans. Although
+ it is based on computer vs. computer experiments, the ratings were
+ modified and rescaled to take into account that self-play experiments
+ usually overestimate the rating differences when playing against
+ humans. */
+ static Rating get_rating(Variant variant, unsigned level);
+
+ /** Get an estimated Elo-rating of the current level. */
+ Rating get_rating(Variant variant) const;
+
+private:
+ bool m_is_book_loaded;
+
+ bool m_use_book;
+
+ bool m_resign;
+
+ string m_books_dir;
+
+ unsigned m_max_level;
+
+ unsigned m_level;
+
+ array<float, Board::max_player_moves> m_weight_max_count_classic;
+
+ array<float, Board::max_player_moves> m_weight_max_count_trigon;
+
+ array<float, Board::max_player_moves> m_weight_max_count_duo;
+
+ array<float, Board::max_player_moves> m_weight_max_count_callisto;
+
+ array<float, Board::max_player_moves> m_weight_max_count_callisto_2;
+
+ Float m_fixed_simulations;
+
+ Float m_resign_threshold;
+
+ Float m_resign_min_simulations;
+
+ double m_fixed_time;
+
+ Search m_search;
+
+ Book m_book;
+
+ unique_ptr<TimeSource> m_time_source;
+
+
+ size_t get_memory();
+
+ void init_settings();
+
+ bool load_book(const string& filepath);
+};
+
+inline Float Player::get_fixed_simulations() const
+{
+ return m_fixed_simulations;
+}
+
+inline double Player::get_fixed_time() const
+{
+ return m_fixed_time;
+}
+
+inline unsigned Player::get_level() const
+{
+ return m_level;
+}
+
+inline Rating Player::get_rating(Variant variant) const
+{
+ return get_rating(variant, m_level);
+}
+
+inline Search& Player::get_search()
+{
+ return m_search;
+}
+
+inline bool Player::get_use_book() const
+{
+ return m_use_book;
+}
+
+inline void Player::set_fixed_simulations(Float n)
+{
+ m_fixed_simulations = n;
+ m_fixed_time = 0;
+}
+
+inline void Player::set_fixed_time(double seconds)
+{
+ m_fixed_time = seconds;
+ m_fixed_simulations = 0;
+}
+
+inline void Player::set_level(unsigned level)
+{
+ m_level = level;
+ m_fixed_simulations = 0;
+ m_fixed_time = 0;
+}
+
+inline void Player::set_use_book(bool enable)
+{
+ m_use_book = enable;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_PLAYER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/PlayoutFeatures.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PlayoutFeatures.h"
+
+namespace libpentobi_mcts {
+
+//-----------------------------------------------------------------------------
+
+PlayoutFeatures::PlayoutFeatures() = default;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/PlayoutFeatures.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H
+#define LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H
+
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/PointList.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libboardgame_base::ArrayList;
+using libboardgame_util::Range;
+using libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Geometry;
+using libpentobi_base::Grid;
+using libpentobi_base::GridExt;
+using libpentobi_base::Move;
+using libpentobi_base::MoveInfo;
+using libpentobi_base::MoveInfoExt;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::Point;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Compute move features for the playout policy.
+ This class encodes features that correspond to points on the board in bit
+ ranges of an integer, such that the sum of the features values for all
+ points of a move can be quickly computed in the playout move generation.
+ Currently, there are only two features: the forbidden status and whether
+ the point is a local point. Local points are attach points of recent
+ opponent moves or points that are adjacent to them. Local points that
+ are attach points of the color to play count double.
+ During a simulation, some of the features are updated incrementally
+ (forbidden status) and some non-incrementally (local points). */
+class PlayoutFeatures
+{
+public:
+ typedef unsigned IntType;
+
+ /** The maximum number of local points for a move.
+ The number can be higher than PieceInfo::max_size (see class
+ description). */
+ static const unsigned max_local = 2 * PieceInfo::max_size;
+
+ /** Compute the sum of the feature values for a move. */
+ class Compute
+ {
+ public:
+ /** Constructor.
+ @param p The first point of the move
+ @param playout_features */
+ Compute(Point p, const PlayoutFeatures& playout_features)
+ : m_value(playout_features.m_point_value[p])
+ { }
+
+ /** Add a point of the move. */
+ void add(Point p, const PlayoutFeatures& playout_features)
+ {
+ m_value += playout_features.m_point_value[p];
+ }
+
+ bool is_forbidden() const
+ {
+ return (m_value & 0xf000u) != 0;
+ }
+
+ /** Get the number of local points for this move.
+ @pre ! is_forbidden()
+ @return The number of local points in [0..max_local] */
+ IntType get_nu_local() const
+ {
+ LIBBOARDGAME_ASSERT(! is_forbidden());
+ return m_value;
+ }
+
+ private:
+ IntType m_value;
+ };
+
+ friend class Compute;
+
+ PlayoutFeatures();
+
+ /** Initialize snapshot with forbidden state. */
+ void init_snapshot(const Board& bd, Color c);
+
+ void restore_snapshot(const Board& bd);
+
+ /** Set points of move to forbidden. */
+ template<unsigned MAX_SIZE>
+ void set_forbidden(const MoveInfo<MAX_SIZE>& info);
+
+ /** Set adjacent points of move to forbidden. */
+ template<unsigned MAX_ADJ_ATTACH>
+ void set_forbidden(const MoveInfoExt<MAX_ADJ_ATTACH>& info_ext);
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void set_local(const Board& bd);
+
+private:
+ GridExt<IntType> m_point_value;
+
+ Grid<IntType> m_snapshot;
+
+ /** Points with non-zero local value. */
+ PointList m_local_points;
+};
+
+inline void PlayoutFeatures::init_snapshot(const Board& bd, Color c)
+{
+ m_point_value[Point::null()] = 0;
+ auto& is_forbidden = bd.is_forbidden(c);
+ for (Point p : bd)
+ m_snapshot[p] = (is_forbidden[p] ? 0x1000u : 0);
+}
+
+
+inline void PlayoutFeatures::restore_snapshot(const Board& bd)
+{
+ m_point_value.copy_from(m_snapshot, bd.get_geometry());
+}
+
+template<unsigned MAX_SIZE>
+inline void PlayoutFeatures::set_forbidden(const MoveInfo<MAX_SIZE>& info)
+{
+ auto p = info.begin();
+ for (unsigned i = 0; i < MAX_SIZE; ++i, ++p)
+ m_point_value[*p] = 0x1000u;
+ m_point_value[Point::null()] = 0;
+}
+
+template<unsigned MAX_ADJ_ATTACH>
+inline void PlayoutFeatures::set_forbidden(
+ const MoveInfoExt<MAX_ADJ_ATTACH>& info_ext)
+{
+ for (auto i = info_ext.begin_adj(), end = info_ext.end_adj(); i != end;
+ ++i)
+ m_point_value[*i] = 0x1000u;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void PlayoutFeatures::set_local(const Board& bd)
+{
+ // Clear old info about local points
+ for (Point p : m_local_points)
+ m_point_value[p] &= 0xf000u;
+ unsigned nu_local = 0;
+
+ Color to_play = bd.get_to_play();
+ Color second_color;
+ if (bd.get_variant() == Variant::classic_3 && to_play.to_int() == 3)
+ second_color = Color(bd.get_alt_player());
+ else
+ second_color = bd.get_second_color(to_play);
+ auto& geo = bd.get_geometry();
+ auto& moves = bd.get_moves();
+ auto move_info_ext_array = bd.get_board_const().get_move_info_ext_array();
+ // Consider last 3 moves for local points (i.e. last 2 opponent moves in
+ // two-color variants)
+ auto end = moves.end();
+ auto begin = (end - moves.begin() < 3 ? moves.begin() : end - 3);
+ for (auto i = begin; i != end; ++i)
+ {
+ Color c = i->color;
+ if (c == to_play || c == second_color)
+ continue;
+ Move mv = i->move;
+ auto& is_forbidden = bd.is_forbidden(c);
+ auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+ mv, move_info_ext_array);
+ auto j = info_ext.begin_attach();
+ auto end = info_ext.end_attach();
+ do
+ {
+ if (is_forbidden[*j])
+ continue;
+ if (m_point_value[*j] == 0)
+ {
+ m_local_points.get_unchecked(nu_local++) = *j;
+ m_point_value[*j] =
+ 1 + static_cast<IntType>(
+ bd.is_attach_point(*j, to_play));
+ }
+ if (MAX_SIZE == 7) // Nexos
+ LIBBOARDGAME_ASSERT(geo.get_adj(*j).empty());
+ else
+ for (Point k : geo.get_adj(*j))
+ if (! is_forbidden[k] && m_point_value[k] == 0)
+ {
+ m_local_points.get_unchecked(nu_local++) = k;
+ m_point_value[k] =
+ 1 + static_cast<IntType>(
+ bd.is_attach_point(k, to_play));
+ }
+ }
+ while (++j != end);
+ }
+ m_local_points.resize(nu_local);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_PLAYOUT_FEATURES_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/PriorKnowledge.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "PriorKnowledge.h"
+
+#include <cmath>
+#include "libboardgame_util/MathUtil.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_util::fast_exp;
+using libpentobi_base::BoardType;
+using libpentobi_base::Color;
+using libpentobi_base::PointState;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::PieceSet;
+
+//-----------------------------------------------------------------------------
+
+PriorKnowledge::PriorKnowledge()
+{
+ m_is_local.fill_all(false);
+}
+
+void PriorKnowledge::start_search(const Board& bd)
+{
+ auto& geo = bd.get_geometry();
+ auto board_type = bd.get_board_type();
+ auto piece_set = bd.get_piece_set();
+
+ // Init m_dist_to_center
+ float width = static_cast<float>(geo.get_width());
+ float height = static_cast<float>(geo.get_height());
+ float center_x = 0.5f * width - 0.5f;
+ float center_y = 0.5f * height - 0.5f;
+ bool is_trigon = (piece_set == PieceSet::trigon);
+ float ratio = (is_trigon ? 1.732f : 1);
+ for (Point p : geo)
+ {
+ float x = static_cast<float>(geo.get_x(p));
+ float y = static_cast<float>(geo.get_y(p));
+ float dx = x - center_x;
+ float dy = ratio * (y - center_y);
+ float d = sqrt(dx * dx + dy * dy);
+ if (board_type == BoardType::classic)
+ // Don't make a distinction between moves close enough to the
+ // center in game variant Classic/Classic2
+ d = max(d, 2.f);
+ m_dist_to_center[p] = d;
+ }
+ m_dist_to_center[Point::null()] = numeric_limits<float>::max();
+
+ // Init m_check_dist_to_center
+ switch(bd.get_variant())
+ {
+ case Variant::classic:
+ case Variant::classic_2:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 12;
+ m_max_dist_diff = 0.3f;
+ break;
+ case Variant::classic_3:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 10;
+ m_max_dist_diff = 0.3f;
+ break;
+ case Variant::trigon:
+ case Variant::trigon_2:
+ case Variant::trigon_3:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 3;
+ m_max_dist_diff = 0.5f;
+ break;
+ case Variant::duo:
+ case Variant::junior:
+ m_check_dist_to_center.fill(false);
+ break;
+ case Variant::callisto:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 4;
+ m_max_dist_diff = 0;
+ break;
+ case Variant::callisto_2:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 4;
+ m_max_dist_diff = 0;
+ break;
+ case Variant::callisto_3:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 3;
+ m_max_dist_diff = 0;
+ break;
+ case Variant::nexos:
+ case Variant::nexos_2:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 7;
+ m_max_dist_diff = 0.3f;
+ break;
+ }
+
+ if (piece_set != PieceSet::callisto)
+ // Don't check dist to center if the position was setup in a way that
+ // placed pieces but did not cover the starting point(s), otherwise the
+ // search might not generate any moves (if no moves meet the
+ // dist-to-center condition). Even if such positions cannot occur in
+ // legal games, we still don't want the move generation to fail.
+ for (Color c : bd.get_colors())
+ {
+ if (bd.get_nu_onboard_pieces(c) == 0)
+ continue;
+ bool is_starting_point_covered = false;
+ for (Point p : bd.get_starting_points(c))
+ if (bd.get_point_state(p) == PointState(c))
+ {
+ is_starting_point_covered = true;
+ break;
+ }
+ if (! is_starting_point_covered)
+ m_check_dist_to_center[c] = false;
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/PriorKnowledge.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H
+#define LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H
+
+#include "Float.h"
+#include "SearchParamConst.h"
+#include "libboardgame_mcts/Tree.h"
+#include "libboardgame_util/MathUtil.h"
+#include "libpentobi_base/Board.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libboardgame_util::fast_exp;
+using libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::ColorMap;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Grid;
+using libpentobi_base::GridExt;
+using libpentobi_base::Move;
+using libpentobi_base::MoveList;
+using libpentobi_base::Point;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Initializes newly created nodes with heuristic prior count and value. */
+class PriorKnowledge
+{
+public:
+ typedef libboardgame_mcts::Node<Move, Float, SearchParamConst::multithread>
+ Node;
+
+ typedef libboardgame_mcts::Tree<Node> Tree;
+
+ PriorKnowledge();
+
+ void start_search(const Board& bd);
+
+ /** Generate children nodes initialized with prior knowledge.
+ @return false If the tree has not enough capacity for the children. */
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ bool gen_children(const Board& bd, const MoveList& moves,
+ bool is_symmetry_broken, Tree::NodeExpander& expander,
+ Float root_val);
+
+private:
+ struct MoveFeatures
+ {
+ /** Heuristic value of the move expressed in score points. */
+ Float heuristic;
+
+ bool is_local;
+
+ /** Does the move touch a piece of the same player? */
+ bool connect;
+
+ /** Only used on Classic and Trigon boards. */
+ float dist_to_center;
+ };
+
+
+ array<MoveFeatures, Move::range> m_features;
+
+ /** Maximum of Features::heuristic for all moves. */
+ Float m_max_heuristic;
+
+ bool m_has_connect_move;
+
+ ColorMap<bool> m_check_dist_to_center;
+
+ unsigned m_dist_to_center_max_pieces;
+
+ float m_min_dist_to_center;
+
+ float m_max_dist_diff;
+
+ /** Marker for attach points of recent opponent moves. */
+ GridExt<bool> m_is_local;
+
+ /** Points in m_is_local with value greater zero. */
+ PointList m_local_points;
+
+ /** Distance to center heuristic. */
+ GridExt<float> m_dist_to_center;
+
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void compute_features(const Board& bd, const MoveList& moves,
+ bool check_dist_to_center, bool check_connect);
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void init_local(const Board& bd);
+};
+
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+void PriorKnowledge::compute_features(const Board& bd, const MoveList& moves,
+ bool check_dist_to_center,
+ bool check_connect)
+{
+ auto to_play = bd.get_to_play();
+ auto variant = bd.get_variant();
+ Color second_color;
+ // connect_color is the 2nd color of the player in game variants with 2
+ // colors per player (connecting to_play and connect_color is good) and
+ // to_play in other game variants (which disables the bonus without
+ // needing an extra check below because adj_point_value is not used for
+ // pieces of to_play because it is illegal for to_play to play there).
+ Color connect_color;
+ if (variant == Variant::classic_3 && to_play.to_int() == 3)
+ {
+ second_color = Color(bd.get_alt_player());
+ connect_color = to_play;
+ }
+ else
+ {
+ second_color = bd.get_second_color(to_play);
+ connect_color = second_color;
+ }
+ auto& bc = bd.get_board_const();
+ auto& geo = bc.get_geometry();
+ auto move_info_array = bc.get_move_info_array();
+ auto move_info_ext_array = bc.get_move_info_ext_array();
+ auto& is_forbidden = bd.is_forbidden(to_play);
+ GridExt<Float> point_value;
+ point_value[Point::null()] = 0;
+ Grid<Float> attach_point_value;
+ Grid<Float> adj_point_value;
+ for (Point p : geo)
+ {
+ auto s = bd.get_point_state(p);
+ if (is_forbidden[p])
+ {
+ if (s != to_play)
+ attach_point_value[p] = -2.5;
+ else
+ attach_point_value[p] = 0.5;
+ if (s == connect_color)
+ // Connecting own colors is good
+ adj_point_value[p] = 1;
+ else if (! s.is_empty())
+ // Touching opponent is better than playing elsewhere (no need to
+ // check if s == to_play, such moves are illegal)
+ adj_point_value[p] = 0.4f;
+ else
+ adj_point_value[p] = 0;
+ }
+ else
+ {
+ point_value[p] = 1;
+ attach_point_value[p] = 0.5;
+ if (bd.is_attach_point(p, to_play))
+ // Making own attach point forbidden is especially bad
+ adj_point_value[p] = -1;
+ else
+ // Creating new forbidden points is a bad thing
+ adj_point_value[p] = -0.1f;
+ }
+ }
+ for (Color c : bd.get_colors())
+ {
+ if (c == to_play || c == second_color)
+ continue;
+ auto& is_forbidden = bd.is_forbidden(c);
+ for (Point p : bd.get_attach_points(c))
+ if (! is_forbidden[p])
+ {
+ // Occupying opponent attach points or points adjacent to them
+ // is very good
+ point_value[p] = 3.f;
+ for (Point j : geo.get_adj(p))
+ if (! is_forbidden[j])
+ point_value[j] = 3.f;
+ }
+ }
+ if (variant == Variant::classic_2
+ || (variant == Variant::classic_3 && second_color != to_play))
+ {
+ auto& is_forbidden_second_color = bd.is_forbidden(second_color);
+ for (Point p : bd.get_attach_points(second_color))
+ if (! is_forbidden_second_color[p])
+ {
+ // Occupying attach points of second color is bad
+ point_value[p] -= 3.f;
+ if (! is_forbidden[p])
+ // Sharing an attach point with second color is bad
+ attach_point_value[p] -= 1.f;
+ }
+ }
+ m_max_heuristic = -numeric_limits<Float>::max();
+ m_min_dist_to_center = numeric_limits<unsigned short>::max();
+ m_has_connect_move = false;
+ for (unsigned i = 0; i < moves.size(); ++i)
+ {
+ auto mv = moves[i];
+ auto info = BoardConst::get_move_info<MAX_SIZE>(mv, move_info_array);
+ auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+ mv, move_info_ext_array);
+ auto& features = m_features[i];
+ auto j = info.begin();
+ Float heuristic = point_value[*j];
+ bool local = m_is_local[*j];
+ if (! check_dist_to_center)
+ for (unsigned k = 1; k < MAX_SIZE; ++k)
+ {
+ ++j;
+ heuristic += point_value[*j];
+ // Logically, we mean: local = local || m_is_local[*j]
+ // But this generates branches, which are bad for performance
+ // in this tight loop (unrolled by the compiler). So we use a
+ // bitwise OR, which works because C++ guarantees that
+ // true/false converts to 1/0.
+ local |= m_is_local[*j];
+ }
+ else
+ {
+ features.dist_to_center = m_dist_to_center[*j];
+ for (unsigned k = 1; k < MAX_SIZE; ++k)
+ {
+ ++j;
+ heuristic += point_value[*j];
+ // See comment above about bitwise OR on bool
+ local |= m_is_local[*j];
+ features.dist_to_center =
+ min(features.dist_to_center, m_dist_to_center[*j]);
+ }
+ m_min_dist_to_center =
+ min(m_min_dist_to_center, features.dist_to_center);
+ }
+ j = info_ext.begin_attach();
+ auto end = info_ext.end_attach();
+ heuristic += attach_point_value[*j];
+ while (++j != end)
+ heuristic += attach_point_value[*j];
+ if (MAX_SIZE == 7) // Nexos
+ {
+ LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0);
+ LIBBOARDGAME_ASSERT(! check_connect);
+ }
+ else
+ {
+ j = info_ext.begin_adj();
+ end = info_ext.end_adj();
+ if (! check_connect)
+ {
+ for ( ; j != end; ++j)
+ heuristic += adj_point_value[*j];
+ }
+ else
+ {
+ features.connect = (bd.get_point_state(*j) == second_color);
+ for ( ; j != end; ++j)
+ {
+ heuristic += adj_point_value[*j];
+ if (bd.get_point_state(*j) == second_color)
+ features.connect = true;
+ }
+ if (features.connect)
+ m_has_connect_move = true;
+ }
+ }
+ if (heuristic > m_max_heuristic)
+ m_max_heuristic = heuristic;
+ features.heuristic = heuristic;
+ features.is_local = local;
+ }
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+bool PriorKnowledge::gen_children(const Board& bd, const MoveList& moves,
+ bool is_symmetry_broken,
+ Tree::NodeExpander& expander, Float root_val)
+{
+ if (moves.empty())
+ {
+ // Add a pass move. The initialization value does not matter for a
+ // single child, but we need to use SearchParamConst::child_min_count
+ // for the count to avoid an assertion.
+ if (! expander.check_capacity(1))
+ return false;
+ expander.add_child(Move::null(), root_val, 3);
+ return true;
+ }
+ init_local<MAX_SIZE, MAX_ADJ_ATTACH>(bd);
+ auto to_play = bd.get_to_play();
+ auto nu_onboard_pieces = bd.get_nu_onboard_pieces();
+ bool check_dist_to_center =
+ (m_check_dist_to_center[to_play]
+ && nu_onboard_pieces <= m_dist_to_center_max_pieces);
+ bool check_connect =
+ (bd.get_variant() == Variant::classic_2 && nu_onboard_pieces < 14);
+ compute_features<MAX_SIZE, MAX_ADJ_ATTACH>(bd, moves, check_dist_to_center,
+ check_connect);
+ if (! m_has_connect_move)
+ check_connect = false;
+ Move symmetric_mv = Move::null();
+ bool has_symmetry_breaker = false;
+ if (! is_symmetry_broken)
+ {
+ unsigned nu_moves = bd.get_nu_moves();
+ if (to_play == Color(1) || to_play == Color(3))
+ {
+ if (nu_moves > 0)
+ {
+ ColorMove last = bd.get_move(nu_moves - 1);
+ symmetric_mv =
+ bd.get_move_info_ext_2(last.move).symmetric_move;
+ }
+ }
+ else if (nu_moves > 0)
+ for (Move mv : moves)
+ if (bd.get_move_info_ext_2(mv).breaks_symmetry)
+ {
+ has_symmetry_breaker = true;
+ break;
+ }
+ }
+ m_min_dist_to_center += m_max_dist_diff;
+ if (! expander.check_capacity(static_cast<unsigned short>(moves.size())))
+ return false;
+ for (unsigned i = 0; i < moves.size(); ++i)
+ {
+ const auto& features = m_features[i];
+
+ // Depending on the game variant, prune early moves that don't minimize
+ // dist to center and moves that don't connect in the middle if
+ // connection is possible
+ if ((check_dist_to_center
+ && features.dist_to_center > m_min_dist_to_center)
+ || (check_connect && ! features.connect))
+ continue;
+
+ auto mv = moves[i];
+
+ // Convert the heuristic, which is so far estimated in score points,
+ // into a win/loss value in [0..1] by making it relative to the
+ // heuristic of the best move and let it decrease exponentially with a
+ // certain width. We could use exp(-c*x) here, but we use
+ // 0.1+0.9*exp(-c*x) instead to avoid that the value is too close to
+ // 0, because then it might never get explored in practice if the bias
+ // term constant is small.
+ Float heuristic = m_max_heuristic - features.heuristic;
+ heuristic = 0.1f + 0.9f * fast_exp(-0.6f * heuristic);
+
+ // Initialize value from heuristic and root_val, each with a count
+ // of 1.5. If this is changed, SearchParamConst::child_min_count
+ // should be updated.
+ Float value = 1.5f * (heuristic + root_val);
+ Float count = 3;
+
+ // If a symmetric draw is still possible, encourage exploring a move
+ // that keeps or breaks the symmetry by adding 5 wins or 5 losses
+ // See also the comment in evaluate_playout()
+ if (! symmetric_mv.is_null())
+ {
+ if (mv == symmetric_mv)
+ value += 5;
+ count += 5;
+ }
+ else if (has_symmetry_breaker
+ && ! bd.get_move_info_ext_2(mv).breaks_symmetry)
+ continue;
+
+ // Add 1 win for moves that are local responses to recent opponent
+ // moves
+ if (features.is_local)
+ {
+ value += 1;
+ count += 1;
+ }
+
+ LIBBOARDGAME_ASSERT(bd.is_legal(to_play, mv));
+ expander.add_child(mv, value / count, count);
+ }
+ return true;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void PriorKnowledge::init_local(const Board& bd)
+{
+ for (Point p : m_local_points)
+ m_is_local[p] = false;
+ unsigned nu_local = 0;
+ Color to_play = bd.get_to_play();
+ Color second_color;
+ if (bd.get_variant() == Variant::classic_3 && to_play.to_int() == 3)
+ second_color = Color(bd.get_alt_player());
+ else
+ second_color = bd.get_second_color(to_play);
+ auto& moves = bd.get_moves();
+ auto move_info_ext_array = bd.get_board_const().get_move_info_ext_array();
+ // Consider last 3 moves for local points (i.e. last 2 opponent moves in
+ // two-color variants)
+ auto end = moves.end();
+ auto begin = (end - moves.begin() < 3 ? moves.begin() : end - 3);
+ for (auto i = begin; i != end; ++i)
+ {
+ Color c = i->color;
+ if (c == to_play || c == second_color)
+ continue;
+ auto mv = i->move;
+ auto& is_forbidden = bd.is_forbidden(c);
+ auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+ mv, move_info_ext_array);
+ auto j = info_ext.begin_attach();
+ auto end = info_ext.end_attach();
+ do
+ {
+ if (is_forbidden[*j])
+ continue;
+ if (! m_is_local[*j])
+ m_local_points.get_unchecked(nu_local++) = *j;
+ m_is_local[*j] = true;
+ }
+ while (++j != end);
+ }
+ m_local_points.resize(nu_local);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_PRIOR_KNOWLEDGE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Search.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Search.h"
+
+#include "Util.h"
+
+namespace libpentobi_mcts {
+
+//-----------------------------------------------------------------------------
+
+Search::Search(Variant initial_variant, unsigned nu_threads, size_t memory)
+ : SearchBase(nu_threads == 0 ? util::get_nu_threads() : nu_threads,
+ memory),
+ m_auto_param(true),
+ m_variant(initial_variant),
+ m_shared_const(m_to_play)
+{
+ set_default_param(m_variant);
+ create_threads();
+}
+
+Search::~Search() = default;
+
+bool Search::check_followup(ArrayList<Move, max_moves>& sequence)
+{
+ auto& bd = get_board();
+ m_history.init(bd, m_to_play);
+ bool is_followup = m_history.is_followup(m_last_history, sequence);
+
+ // If avoid_symmetric_draw is enabled, class State uses a different
+ // evaluation function depending on which player is to play in the root
+ // position (the first player knows about symmetric draws to be able to
+ // play a symmetry breaker but the second player pretends not to know about
+ // symmetric draws to avoid going for such a draw). In this case, we cannot
+ // reuse parts of the old search tree if the computer plays both colors.
+ if (m_shared_const.avoid_symmetric_draw
+ && is_followup && m_to_play != m_last_history.get_to_play()
+ && has_central_symmetry(bd.get_variant())
+ && ! check_symmetry_broken(bd))
+ is_followup = false;
+
+ m_last_history = m_history;
+ return is_followup;
+}
+
+unique_ptr<State> Search::create_state()
+{
+ return unique_ptr<State>(new State(m_variant, m_shared_const));
+}
+
+void Search::get_root_position(Variant& variant, Setup& setup) const
+{
+ m_last_history.get_as_setup(variant, setup);
+ setup.to_play = m_to_play;
+}
+
+void Search::on_start_search(bool is_followup)
+{
+ m_shared_const.init(is_followup);
+}
+
+bool Search::search(Move& mv, const Board& bd, Color to_play,
+ Float max_count, size_t min_simulations,
+ double max_time, TimeSource& time_source)
+{
+ m_shared_const.board = &bd;
+ m_to_play = to_play;
+ auto variant = bd.get_variant();
+ if (m_auto_param && variant != m_variant)
+ set_default_param(variant);
+ m_variant = variant;
+ bool result = SearchBase::search(mv, max_count, min_simulations, max_time,
+ time_source);
+ // Search doesn't generate all useless one-piece moves in Callisto
+ if (result && mv.is_null() && bd.get_piece_set() == PieceSet::callisto
+ && bd.is_piece_left(to_play, bd.get_one_piece()))
+ {
+ for (Point p : bd)
+ if (! bd.is_forbidden(p, to_play) && ! bd.is_center_section(p))
+ {
+ auto moves = bd.get_board_const().get_moves(bd.get_one_piece(),
+ p, 0);
+ LIBBOARDGAME_ASSERT(moves.size() == 1);
+ mv = *moves.begin();
+ result = true;
+ break;
+ }
+ }
+ return result;
+}
+
+void Search::set_default_param(Variant variant)
+{
+ LIBBOARDGAME_LOG("Setting default parameters for ", to_string(variant));
+ set_expand_threshold(1);
+ set_expand_threshold_inc(0.5f);
+ set_rave_weight(0.7f);
+ set_rave_child_max(2000);
+ // The following parameters are currently tuned for duo, classic_2 and
+ // trigon_2 and used for all other game variants with the same board type
+ switch (variant)
+ {
+ case Variant::classic:
+ case Variant::classic_2:
+ case Variant::classic_3:
+ set_exploration_constant(0.021f);
+ set_rave_parent_max(50000);
+ break;
+ case Variant::duo:
+ case Variant::junior:
+ set_exploration_constant(0.020f);
+ set_rave_parent_max(25000);
+ break;
+ case Variant::trigon:
+ case Variant::trigon_2:
+ case Variant::trigon_3:
+ case Variant::callisto:
+ case Variant::callisto_3:
+ set_exploration_constant(0.014f);
+ set_rave_parent_max(50000);
+ break;
+ case Variant::nexos:
+ case Variant::nexos_2:
+ set_exploration_constant(0.008f);
+ set_rave_parent_max(50000);
+ break;
+ case Variant::callisto_2:
+ set_exploration_constant(0.011f);
+ set_rave_parent_max(25000);
+ break;
+ }
+}
+
+string Search::get_info() const
+{
+ if (get_nu_simulations() == 0)
+ return string();
+ auto& root = get_tree().get_root();
+ if (! root.has_children())
+ return string();
+ ostringstream s;
+ s << SearchBase::get_info()
+ << "Mov: " << root.get_nu_children() << ", ";
+ if (libpentobi_base::get_nu_players(m_variant) > 2)
+ {
+ s << "All:";
+ for (PlayerInt i = 0; i < libpentobi_base::get_nu_colors(m_variant);
+ ++i)
+ {
+ if (get_root_val(i).get_count() == 0)
+ s << " -";
+ else
+ s << " " << setprecision(2) << get_root_val(i).get_mean();
+ }
+ s << ", ";
+ }
+ s << get_state(0).get_info();
+ return s.str();
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Search.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_SEARCH_H
+#define LIBPENTOBI_MCTS_SEARCH_H
+
+#include "History.h"
+#include "SearchParamConst.h"
+#include "State.h"
+#include "libboardgame_mcts/SearchBase.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libboardgame_mcts::PlayerInt;
+using libboardgame_util::TimeSource;
+using libpentobi_base::Setup;
+
+//-----------------------------------------------------------------------------
+
+/** Monte-Carlo tree search implementation for Blokus.
+ Multiple colors per player (e.g. in Classic 2) are handled by using the
+ same game result for each color of a player.
+ Multiple players of a color (the 4th color in Classic 3) are handled by
+ adding additional players for each player of this color that share the
+ game result with the main color of the player.
+ The maximum number of players is 6, which occurs in Classic 3 with 3
+ real players and 3 pseudo-players for the 4th color.
+ @note @ref libboardgame_avoid_stack_allocation */
+class Search final
+ : public libboardgame_mcts::SearchBase<State, Move, SearchParamConst>
+{
+public:
+ Search(Variant initial_variant, unsigned nu_threads, size_t memory);
+
+ ~Search();
+
+ unique_ptr<State> create_state() override;
+
+ PlayerInt get_nu_players() const override;
+
+ PlayerInt get_player() const override;
+
+ bool check_followup(ArrayList<Move, max_moves>& sequence) override;
+
+ string get_info() const override;
+
+
+ /** @name Parameters */
+ /** @{ */
+
+ bool get_avoid_symmetric_draw() const;
+
+ void set_avoid_symmetric_draw(bool enable);
+
+ /** Automatically set some user-changeable parameters that have different
+ optimal values for different game variants whenever the game variant
+ changes.
+ Default is true. */
+ bool get_auto_param() const;
+
+ void set_auto_param(bool enable);
+
+ /** @} */ // @name
+
+
+ bool search(Move& mv, const Board& bd, Color to_play, Float max_count,
+ size_t min_simulations, double max_time,
+ TimeSource& time_source);
+
+ /** Get color to play at root node of the last search. */
+ Color get_to_play() const;
+
+ const History& get_last_history() const;
+
+ /** Get board position of last search at root node as setup.
+ @param[out] variant
+ @param[out] setup */
+ void get_root_position(Variant& variant, Setup& setup) const;
+
+protected:
+ void on_start_search(bool is_followup) override;
+
+private:
+ /** Automatically set default parameters for the game variant if
+ the game variant changes. */
+ bool m_auto_param;
+
+ /** Game variant of last search. */
+ Variant m_variant;
+
+ Color m_to_play;
+
+ SharedConst m_shared_const;
+
+ /** Local variable reused for efficiency. */
+ History m_history;
+
+ History m_last_history;
+
+ const Board& get_board() const;
+
+ void set_default_param(Variant variant);
+};
+
+inline bool Search::get_auto_param() const
+{
+ return m_auto_param;
+}
+
+inline bool Search::get_avoid_symmetric_draw() const
+{
+ return m_shared_const.avoid_symmetric_draw;
+}
+
+inline const Board& Search::get_board() const
+{
+ return *m_shared_const.board;
+}
+
+inline const History& Search::get_last_history() const
+{
+ return m_last_history;
+}
+
+inline PlayerInt Search::get_nu_players() const
+{
+ return m_variant != Variant::classic_3 ? get_board().get_nu_colors() : 6;
+}
+
+inline PlayerInt Search::get_player() const
+{
+ auto to_play = m_to_play.to_int();
+ if ( m_variant == Variant::classic_3 && to_play == 3)
+ return static_cast<PlayerInt>(to_play + get_board().get_alt_player());
+ else
+ return to_play;
+}
+
+inline Color Search::get_to_play() const
+{
+ return m_to_play;
+}
+
+inline void Search::set_auto_param(bool enable)
+{
+ m_auto_param = enable;
+}
+
+inline void Search::set_avoid_symmetric_draw(bool enable)
+{
+ m_shared_const.avoid_symmetric_draw = enable;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_SEARCH_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/SearchParamConst.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H
+#define LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H
+
+#include "Float.h"
+#include "libpentobi_base/Board.h"
+#include "libboardgame_mcts/PlayerMove.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_mcts::PlayerInt;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+
+//-----------------------------------------------------------------------------
+
+/** Optional compile-time parameters for libboardgame_mcts::Search.
+ See libboardgame_mcts::SearchParamConstDefault for the meaning of the
+ members. */
+struct SearchParamConst
+{
+ typedef libpentobi_mcts::Float Float;
+
+ static const PlayerInt max_players = 6;
+
+ /** The maximum number of moves in a simulation.
+ This needs to include pass moves because in the in-tree phase pass
+ moves (Move::null()) are used. The game ends after all colors have
+ passed in a row. Therefore, the maximum number of moves is reached in
+ case that a piece move is followed by (Color::range-1) pass moves and
+ an extra Color::range pass moves at the end. */
+ static const unsigned max_moves =
+ Color::range * (Color::range * Board::max_pieces + 1);
+
+#ifdef LIBBOARDGAME_MCTS_SINGLE_THREAD
+ static const bool multithread = false;
+#else
+ static const bool multithread = true;
+#endif
+
+ static const bool rave = true;
+
+ static const bool rave_dist_weighting = true;
+
+ static const bool use_lgr = true;
+
+#if PENTOBI_LOW_RESOURCES
+ static const size_t lgr_hash_table_size = (1 << 20);
+#else
+ static const size_t lgr_hash_table_size = (1 << 21);
+#endif
+
+ static const bool virtual_loss = true;
+
+ static const bool use_unlikely_change = true;
+
+ static constexpr Float child_min_count = 3;
+
+ static constexpr Float tie_value = 0.5f;
+
+ static constexpr Float prune_count_start = 16;
+
+ static constexpr double expected_sim_per_sec = 100;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_SEARCH_PARAM_CONST_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/SharedConst.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "SharedConst.h"
+
+namespace libpentobi_mcts {
+
+using libpentobi_base::BoardConst;
+using libpentobi_base::BoardType;
+using libpentobi_base::Piece;
+using libpentobi_base::PieceSet;
+using libpentobi_base::ScoreType;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void filter_min_size(const BoardConst& bc, ScoreType min_size,
+ PieceMap<bool>& is_piece_considered)
+{
+ for (Piece::IntType i = 0; i < bc.get_nu_pieces(); ++i)
+ {
+ Piece piece(i);
+ auto& piece_info = bc.get_piece_info(piece);
+ if (piece_info.get_score_points() < min_size)
+ is_piece_considered[piece] = false;
+ }
+}
+
+/** Check if an adjacent status is a possible follow-up status for another
+ one. */
+inline bool is_followup_adj_status(unsigned status_new, unsigned status_old)
+{
+ return (status_new & status_old) == status_old;
+}
+
+void set_piece_considered(const BoardConst& bc, const char* name,
+ PieceMap<bool>& is_piece_considered,
+ bool is_considered = true)
+{
+ Piece piece;
+ bool found = bc.get_piece_by_name(name, piece);
+ LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(found);
+ LIBBOARDGAME_ASSERT(found);
+ is_piece_considered[piece] = is_considered;
+}
+
+void set_pieces_considered(const Board& bd, unsigned nu_moves,
+ PieceMap<bool>& is_piece_considered)
+{
+ auto& bc = bd.get_board_const();
+ unsigned nu_colors = bd.get_nu_colors();
+ is_piece_considered.fill(true);
+ switch (bc.get_board_type())
+ {
+ case BoardType::duo:
+ if (nu_moves < 2 * nu_colors)
+ filter_min_size(bc, 5, is_piece_considered);
+ else if (nu_moves < 3 * nu_colors)
+ filter_min_size(bc, 4, is_piece_considered);
+ else if (nu_moves < 5 * nu_colors)
+ filter_min_size(bc, 3, is_piece_considered);
+ break;
+ case BoardType::classic:
+ if (nu_moves < nu_colors)
+ {
+ is_piece_considered.fill(false);
+ set_piece_considered(bc, "V5", is_piece_considered);
+ set_piece_considered(bc, "Z5", is_piece_considered);
+ }
+ else if (nu_moves < 2 * nu_colors)
+ {
+ filter_min_size(bc, 5, is_piece_considered);
+ set_piece_considered(bc, "F", is_piece_considered, false);
+ set_piece_considered(bc, "P", is_piece_considered, false);
+ set_piece_considered(bc, "T5", is_piece_considered, false);
+ set_piece_considered(bc, "U", is_piece_considered, false);
+ set_piece_considered(bc, "X", is_piece_considered, false);
+ }
+ else if (nu_moves < 3 * nu_colors)
+ {
+ filter_min_size(bc, 5, is_piece_considered);
+ set_piece_considered(bc, "P", is_piece_considered, false);
+ set_piece_considered(bc, "U", is_piece_considered, false);
+ }
+ else if (nu_moves < 5 * nu_colors)
+ filter_min_size(bc, 4, is_piece_considered);
+ else if (nu_moves < 7 * nu_colors)
+ filter_min_size(bc, 3, is_piece_considered);
+ break;
+ case BoardType::trigon:
+ case BoardType::trigon_3:
+ if (nu_moves < nu_colors)
+ {
+ is_piece_considered.fill(false);
+ set_piece_considered(bc, "V", is_piece_considered);
+ set_piece_considered(bc, "I6", is_piece_considered);
+ }
+ if (nu_moves < 4 * nu_colors)
+ {
+ filter_min_size(bc, 6, is_piece_considered);
+ // O is a bad early move, it neither extends, nor blocks well
+ set_piece_considered(bc, "O", is_piece_considered, false);
+ }
+ else if (nu_moves < 5 * nu_colors)
+ filter_min_size(bc, 5, is_piece_considered);
+ else if (nu_moves < 7 * nu_colors)
+ filter_min_size(bc, 4, is_piece_considered);
+ else if (nu_moves < 9 * nu_colors)
+ filter_min_size(bc, 3, is_piece_considered);
+ break;
+ case BoardType::nexos:
+ if (nu_moves < 3 * nu_colors)
+ filter_min_size(bc, 4, is_piece_considered);
+ else if (nu_moves < 5 * nu_colors)
+ filter_min_size(bc, 3, is_piece_considered);
+ break;
+ case BoardType::callisto:
+ case BoardType::callisto_2:
+ case BoardType::callisto_3:
+ is_piece_considered[bd.get_one_piece()] = false;
+ if (nu_moves < 3 * nu_colors)
+ filter_min_size(bc, 5, is_piece_considered);
+ else if (nu_moves < 8 * nu_colors)
+ filter_min_size(bc, 4, is_piece_considered);
+ else if (nu_moves < 12 * nu_colors)
+ filter_min_size(bc, 3, is_piece_considered);
+ break;
+ }
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+SharedConst::SharedConst(const Color& to_play)
+ : board(nullptr),
+ to_play(to_play),
+ avoid_symmetric_draw(true)
+{ }
+
+void SharedConst::init(bool is_followup)
+{
+ auto& bd = *board;
+ auto& bc = bd.get_board_const();
+
+ // Initialize precomp_moves
+ for (Color c : bd.get_colors())
+ {
+ auto& precomp = precomp_moves[c];
+ auto& old_precomp = (is_followup ? precomp : bc.get_precomp_moves());
+
+ m_is_forbidden.set();
+ for (Point p : bd)
+ if (! bd.is_forbidden(p, c))
+ {
+ auto adj_status = bd.get_adj_status(p, c);
+ for (Piece piece : bd.get_pieces_left(c))
+ {
+ if (! old_precomp.has_moves(piece, p, adj_status))
+ continue;
+ for (Move mv : old_precomp.get_moves(piece, p, adj_status))
+ if (m_is_forbidden[mv] && ! bd.is_forbidden(c, mv))
+ m_is_forbidden.clear(mv);
+ }
+ }
+
+ // Don't use bd.get_pieces_left() because its ordering is not preserved
+ // during a game. The in-place construction requires that the loop
+ // iterates in the same order as during the last construction such that
+ // it doesn't overwrite elements it still needs to read.
+ Board::PiecesLeftList pieces;
+ for (Piece::IntType i = 0; i < bc.get_nu_pieces(); ++i)
+ if (bd.is_piece_left(c, Piece(i)))
+ pieces.push_back(Piece(i));
+ if (! is_followup)
+ for (Point p : bd)
+ if (! bd.is_forbidden(p, c))
+ {
+ auto adj_status = bd.get_adj_status(p, c);
+ for (unsigned i = 0; i < PrecompMoves::nu_adj_status; ++i)
+ if (is_followup_adj_status(i, adj_status))
+ for (auto piece : pieces)
+ precomp.set_list_range(p, i, piece, 0, 0);
+ }
+ unsigned n = 0;
+ for (Point p : bd)
+ {
+ if (bd.is_forbidden(p, c))
+ continue;
+ auto adj_status = bd.get_adj_status(p, c);
+ for (unsigned i = 0; i < PrecompMoves::nu_adj_status; ++i)
+ {
+ if (! is_followup_adj_status(i, adj_status))
+ continue;
+ for (auto piece : pieces)
+ {
+ if (! old_precomp.has_moves(piece, p, i))
+ continue;
+ auto begin = n;
+ for (auto& mv : old_precomp.get_moves(piece, p, i))
+ if (! m_is_forbidden[mv])
+ precomp.set_move(n++, mv);
+ precomp.set_list_range(p, i, piece, begin, n - begin);
+ }
+ }
+ }
+ }
+
+ if (! is_followup)
+ init_pieces_considered();
+ if (bd.get_piece_set() == PieceSet::callisto)
+ init_one_piece_callisto(is_followup);
+}
+
+void SharedConst::init_one_piece_callisto(bool is_followup)
+{
+ auto& bd = *board;
+ auto& bc = bd.get_board_const();
+ Piece one_piece = bd.get_one_piece();
+ unsigned n = 0;
+ if (! is_followup)
+ {
+ for (Point p : bd)
+ if (! bd.is_center_section(p) && bd.get_point_state(p).is_empty())
+ {
+ auto moves = bc.get_moves(one_piece, p, 0);
+ LIBBOARDGAME_ASSERT(moves.size() == 1);
+ Move mv = *moves.begin();
+ if (! is_useless_one_piece_point(p))
+ {
+ one_piece_points_callisto.get_unchecked(n) = p;
+ one_piece_moves_callisto.get_unchecked(n) = mv;
+ ++n;
+ }
+ }
+ }
+ else
+ for (unsigned i = 0; i < one_piece_points_callisto.size(); ++i)
+ {
+ Point p = one_piece_points_callisto[i];
+ Move mv = one_piece_moves_callisto[i];
+ if (bd.get_point_state(p).is_empty()
+ && ! is_useless_one_piece_point(p))
+ {
+ one_piece_points_callisto.get_unchecked(n) = p;
+ one_piece_moves_callisto.get_unchecked(n) = mv;
+ ++n;
+ }
+ }
+ one_piece_points_callisto.resize(n);
+ one_piece_moves_callisto.resize(n);
+}
+
+void SharedConst::init_pieces_considered()
+{
+ auto& bd = *board;
+ auto& bc = bd.get_board_const();
+ is_piece_considered_list.clear();
+ bool is_callisto = (bd.get_piece_set() == PieceSet::callisto);
+ for (auto i = bd.get_nu_onboard_pieces(); i < Board::max_game_moves; ++i)
+ {
+ PieceMap<bool> is_piece_considered;
+ set_pieces_considered(bd, i, is_piece_considered);
+ bool are_all_considered = true;
+ for (Piece::IntType j = 0; j < bc.get_nu_pieces(); ++j)
+ if (! is_piece_considered[Piece(j)]
+ && ! (is_callisto && Piece(j) == bd.get_one_piece()))
+ {
+ are_all_considered = false;
+ break;
+ }
+ if (are_all_considered)
+ {
+ min_move_all_considered = i;
+ break;
+ }
+ auto pos = find(is_piece_considered_list.begin(),
+ is_piece_considered_list.end(),
+ is_piece_considered);
+ if (pos != is_piece_considered_list.end())
+ this->is_piece_considered[i] = &(*pos);
+ else
+ {
+ is_piece_considered_list.push_back(is_piece_considered);
+ this->is_piece_considered[i] = &is_piece_considered_list.back();
+ }
+ }
+ is_piece_considered_all.fill(true);
+ if (is_callisto)
+ is_piece_considered_all[bd.get_one_piece()] = false;
+ is_piece_considered_none.fill(false);
+}
+
+/** Check if a point is a useless move for the 1-piece.
+ @return true if all neighbors are occupied, because the 1-piece doesn't
+ contribute to the score and playing there neither enables own moves
+ nor prevents opponent moves with larger pieces. */
+bool SharedConst::is_useless_one_piece_point(Point p) const
+{
+ auto& bd = *board;
+ for (Point pp: bd.get_geometry().get_diag(p))
+ if (bd.get_point_state(pp).is_empty())
+ return false;
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/SharedConst.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_SHARED_CONST_H
+#define LIBPENTOBI_MCTS_SHARED_CONST_H
+
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/MoveMarker.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libboardgame_util::ArrayList;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMap;
+using libpentobi_base::Move;
+using libpentobi_base::MoveMarker;
+using libpentobi_base::PieceMap;
+using libpentobi_base::Point;
+using libpentobi_base::PointList;
+using libpentobi_base::PrecompMoves;
+
+//-----------------------------------------------------------------------------
+
+/** Constant data shared between the search states. */
+class SharedConst
+{
+public:
+ /** Precomputed moves additionally constrained by moves that are
+ non-forbidden at root position. */
+ ColorMap<PrecompMoves> precomp_moves;
+
+ /** The game board.
+ Contains the current position. */
+ const Board* board;
+
+ /** The color to play at the root of the search. */
+ const Color& to_play;
+
+ bool avoid_symmetric_draw;
+
+ /** Minimum total number of pieces on the board where all pieces are
+ considered until the rest of the simulation. */
+ unsigned min_move_all_considered;
+
+ /** Precomputed lists of considered pieces depending on the total number
+ of pieces on the board.
+ Only initialized for numbers greater than or equal to the number in the
+ root position and less than min_move_all_considered.
+ Contains pointers to unique values such that the comparison of the
+ lists can be done by comparing the pointers to the lists. */
+ array<const PieceMap<bool>*, Board::max_game_moves> is_piece_considered;
+
+ /** List of unique values for is_piece_considered. */
+ ArrayList<PieceMap<bool>, Board::max_game_moves> is_piece_considered_list;
+
+ /** Precomputed lists of considered pieces if all pieces are enforced to be
+ considered (because using the restricted set of pieces would generate
+ no moves). */
+ PieceMap<bool> is_piece_considered_all;
+
+ PieceMap<bool> is_piece_considered_none;
+
+ /** List of legal points in the root position for the 1x1-piece in
+ Callisto. */
+ PointList one_piece_points_callisto;
+
+ /** Moves corresponding to one_piece_points_callisto. */
+ ArrayList<Move, Point::max_onboard> one_piece_moves_callisto;
+
+
+ explicit SharedConst(const Color& to_play);
+
+ void init(bool is_followup);
+
+private:
+ /** Temporary variable used in init().
+ Reused for efficiency. */
+ MoveMarker m_is_forbidden;
+
+ void init_one_piece_callisto(bool is_followup);
+
+ void init_pieces_considered();
+
+ bool is_useless_one_piece_point(Point p) const;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_SHARED_CONST_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/State.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "State.h"
+
+#include "libboardgame_util/MathUtil.h"
+#include "libpentobi_base/ScoreUtil.h"
+#if LIBBOARDGAME_DEBUG
+#include "libpentobi_base/BoardUtil.h"
+#endif
+
+namespace libpentobi_mcts {
+
+using libboardgame_util::fast_exp;
+using libpentobi_base::get_multiplayer_result;
+using libpentobi_base::BoardType;
+using libpentobi_base::PointState;
+using libpentobi_base::ScoreType;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** Gamma value for PlayoutFeatures::get_nu_local().
+ The value of nu_local dominates all other features, so we use a high
+ gamma. Above some limit, we don't care about the exact value. */
+float gamma_local[PlayoutFeatures::max_local + 1] =
+ { 1, 1e6f, 1e12f, 1e18f, 1e24f, 1e25f, 1e25f, 1e25f, 1e25f, 1e25f, 1e25f,
+ 1e25f, 1e25f, 1e25f, 1e25f };
+
+inline Float sigmoid(Float steepness, Float x)
+{
+ return -1.f + 2.f / (1.f + fast_exp(-steepness * x));
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+State::State(Variant initial_variant, const SharedConst& shared_const)
+ : m_shared_const(shared_const),
+ m_bd(initial_variant)
+{
+}
+
+template<unsigned MAX_SIZE, bool IS_CALLISTO>
+inline void State::add_moves(Point p, Color c,
+ const Board::PiecesLeftList& pieces,
+ float& total_gamma, MoveList& moves,
+ unsigned& nu_moves)
+{
+ auto& marker = m_marker[c];
+ auto& playout_features = m_playout_features[c];
+ auto adj_status = m_bd.get_adj_status(p, c);
+ for (Piece piece : pieces)
+ {
+ if (! has_moves(c, piece, p, adj_status))
+ continue;
+ auto gamma_piece = m_gamma_piece[piece];
+ for (Move mv : get_moves(c, piece, p, adj_status))
+ if (! marker[mv]
+ && check_move<MAX_SIZE, IS_CALLISTO>(
+ mv, get_move_info<MAX_SIZE>(mv), gamma_piece, moves,
+ nu_moves, playout_features, total_gamma))
+ marker.set(mv);
+ }
+}
+
+template<unsigned MAX_SIZE>
+void State::add_one_piece_moves(Color c, bool with_gamma, float& total_gamma,
+ MoveList& moves, unsigned& nu_moves)
+{
+ Piece one_piece = m_bd.get_one_piece();
+ auto nu_left = m_bd.get_nu_left_piece(c, one_piece);
+ if (nu_left == 0)
+ return;
+ for (unsigned i = 0; i < m_shared_const.one_piece_points_callisto.size();
+ ++i)
+ {
+ Point p = m_shared_const.one_piece_points_callisto[i];
+ if (m_bd.is_forbidden(p, c))
+ continue;
+ Move mv = m_shared_const.one_piece_moves_callisto[i];
+ LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size);
+ moves.get_unchecked(nu_moves) = mv;
+ ++nu_moves;
+ LIBBOARDGAME_ASSERT(! m_marker[c][mv]);
+ m_marker[c].set(mv);
+ if (with_gamma)
+ {
+ total_gamma += m_gamma_piece[one_piece];
+ m_cumulative_gamma[nu_moves - 1] = total_gamma;
+ }
+ }
+}
+
+template<unsigned MAX_SIZE>
+void State::add_starting_moves(Color c, const Board::PiecesLeftList& pieces,
+ bool with_gamma, MoveList& moves)
+{
+ // Using only one starting point (if game variant has more than one) not
+ // only reduces the branching factor but is also necessary because
+ // update_moves() assumes that a move stays legal if the forbidden
+ // status for all of its points does not change.
+ Point p = find_best_starting_point(c);
+ if (p.is_null())
+ return;
+ unsigned nu_moves = 0;
+ auto& marker = m_marker[c];
+ auto& is_forbidden = m_bd.is_forbidden(c);
+ float total_gamma = 0;
+ for (Piece piece : pieces)
+ for (Move mv : get_moves(c, piece, p, 0))
+ {
+ LIBBOARDGAME_ASSERT(! marker[mv]);
+ if (check_forbidden<MAX_SIZE>(is_forbidden, mv, moves, nu_moves))
+ {
+ marker.set(mv);
+ if (with_gamma)
+ {
+ total_gamma += m_gamma_piece[piece];
+ m_cumulative_gamma[nu_moves - 1] = total_gamma;
+ }
+ }
+ }
+ moves.resize(nu_moves);
+}
+
+template<unsigned MAX_SIZE>
+bool State::check_forbidden(const GridExt<bool>& is_forbidden, Move mv,
+ MoveList& moves, unsigned& nu_moves)
+{
+ auto p = get_move_info<MAX_SIZE>(mv).begin();
+ unsigned forbidden = is_forbidden[*p];
+ for (unsigned i = 1; i < MAX_SIZE; ++i)
+ // Logically, forbidden is a bool and the next line should be
+ // forbidden = forbidden || is_forbidden[*(++p)]
+ // But this generates branches, which are bad for performance in this
+ // tight loop (unrolled by the compiler). So we use a bitwise OR, which
+ // works because C++ guarantees that true/false converts to 1/0.
+ forbidden |= static_cast<unsigned>(is_forbidden[*(++p)]);
+ if (forbidden != 0)
+ return false;
+ LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size);
+ moves.get_unchecked(nu_moves) = mv;
+ ++nu_moves;
+ return true;
+}
+
+template<unsigned MAX_SIZE, bool IS_CALLISTO>
+bool State::check_move(Move mv, const MoveInfo<MAX_SIZE>& info,
+ float gamma_piece, MoveList& moves, unsigned& nu_moves,
+ const PlayoutFeatures& playout_features,
+ float& total_gamma)
+{
+ LIBBOARDGAME_ASSERT(IS_CALLISTO == m_is_callisto);
+ auto p = info.begin();
+ PlayoutFeatures::Compute features(*p, playout_features);
+ for (unsigned i = 1; i < MAX_SIZE; ++i)
+ features.add(*(++p), playout_features);
+ if (features.is_forbidden())
+ return false;
+ auto gamma = gamma_piece;
+ if (! (IS_CALLISTO && info.get_size() == 1))
+ gamma *= gamma_local[features.get_nu_local()];
+ total_gamma += gamma;
+ m_cumulative_gamma[nu_moves] = total_gamma;
+ LIBBOARDGAME_ASSERT(nu_moves < MoveList::max_size);
+ moves.get_unchecked(nu_moves) = mv;
+ ++nu_moves;
+ return true;
+}
+
+template<unsigned MAX_SIZE, bool IS_CALLISTO>
+inline bool State::check_move(Move mv, const MoveInfo<MAX_SIZE>& info,
+ MoveList& moves, unsigned& nu_moves,
+ const PlayoutFeatures& playout_features,
+ float& total_gamma)
+{
+ return check_move<MAX_SIZE, IS_CALLISTO>(
+ mv, info, m_gamma_piece[info.get_piece()], moves, nu_moves,
+ playout_features, total_gamma);
+}
+
+#if LIBBOARDGAME_DEBUG
+string State::dump() const
+{
+ ostringstream s;
+ s << "pentobi_mcts::State:\n" << libpentobi_base::boardutil::dump(m_bd);
+ return s.str();
+}
+#endif
+
+/** Evaluation function for game variants with 2 players and 2 colors per
+ player. */
+void State::evaluate_multicolor(array<Float, 6>& result)
+{
+ LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2);
+ LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 4);
+ // Always evaluate symmetric positions in trigon_2 as a draw in the
+ // playouts. See comment in evaluate_playout_duo.
+ // m_is_symmetry_broken is always true in classic_2, no need to check for
+ // game variant.
+ if (! m_is_symmetry_broken
+ && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces)
+ {
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Result: 0.5 (symmetry)");
+ result[0] = result[1] = result[2] = result[3] = 0.5;
+ return;
+ }
+
+ auto s = m_bd.get_score_multicolor(Color(0));
+ Float res;
+ if (s > 0)
+ res = 1;
+ else if (s < 0)
+ res = 0;
+ else
+ res = 0.5;
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Result color 0: sco=", s, " game_res=", res);
+ res += get_quality_bonus(Color(0), res, s)
+ + get_quality_bonus_attach_multicolor();
+ if (log_simulations)
+ LIBBOARDGAME_LOG("res=", res);
+ result[0] = result[2] = res;
+ result[1] = result[3] = 1.f - res;
+}
+
+/** Evaluation function for game variants with more than 2 players.
+ The result is 0,0.5,1 for loss/tie/win in 2-player variants. For n \> 2
+ players, this is generalized in the following way: The scores are sorted in
+ ascending order. Each rank r_i (i in 0..n-1) is assigned a result value of
+ r_i/(n-1). If multiple players have the same score, the result value is the
+ average of all ranks with this score. So being the single winner still
+ gives the result 1 and having the lowest score gives the result 0. Being
+ the single winner is better than sharing the best place, which is better
+ than getting the second place, etc. */
+void State::evaluate_multiplayer(array<Float, 6>& result)
+{
+ auto nu_players = m_bd.get_nu_players();
+ LIBBOARDGAME_ASSERT(nu_players > 2);
+ array<ScoreType, Color::range> points;
+ for (Color::IntType i = 0; i < nu_players; ++i)
+ points[i] = m_bd.get_points(Color(i));
+ array<Float, Color::range> game_result;
+ get_multiplayer_result(nu_players, points, game_result, m_is_callisto);
+ for (Color::IntType i = 0; i < nu_players; ++i)
+ {
+ Color c(i);
+ auto s = m_bd.get_score_multiplayer(c);
+ result[i] = game_result[i] + get_quality_bonus(c, game_result[i], s);
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Result sco=", s, " game_res=", game_result[i],
+ " res=", result[i]);
+ }
+ if (m_bd.get_variant() == Variant::classic_3)
+ {
+ result[3] = result[0];
+ result[4] = result[1];
+ result[5] = result[2];
+ }
+}
+
+/** Evaluation function for Duo, Junior and Callisto Two-Player. */
+void State::evaluate_twocolor(array<Float, 6>& result)
+{
+ LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2);
+ LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 2);
+ ScoreType s;
+ if (! m_is_symmetry_broken
+ && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces)
+ {
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Symmetry not broken");
+ s = 0;
+ }
+ else
+ s = m_bd.get_score_twocolor(Color(0));
+ Float res;
+ if (s > 0)
+ res = 1;
+ else if (s < 0 || (m_is_callisto && s == 0))
+ res = 0;
+ else
+ res = 0.5;
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Result sco=", s, " game_res=", res);
+ res += get_quality_bonus(Color(0), res, s);
+ if (m_is_callisto)
+ res += get_quality_bonus_attach_twocolor();
+ if (log_simulations)
+ LIBBOARDGAME_LOG("res=", res);
+ result[0] = res;
+ result[1] = 1.f - res;
+}
+
+Point State::find_best_starting_point(Color c) const
+{
+ // We use the starting point that maximizes the distance to occupied
+ // starting points, especially to the ones occupied by the player (their
+ // distance is weighted with a factor of 2).
+ Point best = Point::null();
+ float max_distance = -1;
+ auto board_type = m_bd.get_board_type();
+ bool is_trigon = (board_type == BoardType::trigon
+ || board_type == BoardType::trigon_3);
+ bool is_nexos = board_type == BoardType::nexos;
+ float ratio = (is_trigon ? 1.732f : 1);
+ auto& geo = m_bd.get_geometry();
+ for (Point p : m_bd.get_starting_points(c))
+ {
+ if (m_bd.is_forbidden(p, c))
+ continue;
+ if (is_nexos)
+ {
+ // Don't use the starting segments towards the edge of the board
+ auto x = geo.get_x(p);
+ if (x <= 3 || x >= geo.get_width() - 3 - 1)
+ continue;
+ auto y = geo.get_y(p);
+ if (y <= 3 || y >= geo.get_height() - 3 - 1)
+ continue;
+ }
+ float px = static_cast<float>(geo.get_x(p));
+ float py = static_cast<float>(geo.get_y(p));
+ float d = 0;
+ for (Color i : Color::Range(m_nu_colors))
+ for (Point pp : m_bd.get_starting_points(i))
+ {
+ PointState s = m_bd.get_point_state(pp);
+ if (! s.is_empty())
+ {
+ float ppx = static_cast<float>(geo.get_x(pp));
+ float ppy = static_cast<float>(geo.get_y(pp));
+ float dx = ppx - px;
+ float dy = ratio * (ppy - py);
+ float weight = 1;
+ if (s == c || s == m_bd.get_second_color(c))
+ weight = 2;
+ d += weight * sqrt(dx * dx + dy * dy);
+ }
+ }
+ if (d > max_distance)
+ {
+ best = p;
+ max_distance = d;
+ }
+ }
+ return best;
+}
+
+bool State::gen_children(Tree::NodeExpander& expander, Float root_val)
+{
+ if (m_nu_passes == m_nu_colors)
+ return true;
+ Color to_play = m_bd.get_to_play();
+ if (m_max_piece_size == 5)
+ {
+ init_moves_without_gamma<5>(to_play);
+ return m_prior_knowledge.gen_children<5, 16>(m_bd, m_moves[to_play],
+ m_is_symmetry_broken,
+ expander, root_val);
+ }
+ else if (m_max_piece_size == 6)
+ {
+ init_moves_without_gamma<6>(to_play);
+ return m_prior_knowledge.gen_children<6, 22>(m_bd, m_moves[to_play],
+ m_is_symmetry_broken,
+ expander, root_val);
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 7);
+ init_moves_without_gamma<7>(to_play);
+ return m_prior_knowledge.gen_children<7, 12>(m_bd, m_moves[to_play],
+ m_is_symmetry_broken,
+ expander, root_val);
+ }
+}
+
+bool State::gen_playout_move_full(PlayerMove<Move>& mv)
+{
+ Color to_play = m_bd.get_to_play();
+ while (true)
+ {
+ if (! m_is_move_list_initialized[to_play])
+ {
+ if (m_max_piece_size == 5)
+ {
+ if (m_is_callisto)
+ init_moves_with_gamma<5, 16, true>(to_play);
+ else
+ init_moves_with_gamma<5, 16, false>(to_play);
+ }
+ else if (m_max_piece_size == 6)
+ init_moves_with_gamma<6, 22, false>(to_play);
+ else
+ init_moves_with_gamma<7, 12, false>(to_play);
+ }
+ else if (m_has_moves[to_play])
+ {
+ if (m_max_piece_size == 5)
+ {
+ if (m_is_callisto)
+ update_moves<5, 16, true>(to_play);
+ else
+ update_moves<5, 16, false>(to_play);
+ }
+ else if (m_max_piece_size == 6)
+ update_moves<6, 22, false>(to_play);
+ else
+ update_moves<7, 12, false>(to_play);
+ }
+ if ((m_has_moves[to_play] = ! m_moves[to_play].empty()))
+ break;
+ if (++m_nu_passes == m_nu_colors)
+ return false;
+ if (m_check_terminate_early && m_bd.get_score_twoplayer(to_play) < 0
+ && ! m_has_moves[m_bd.get_second_color(to_play)])
+ {
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Terminate early (no moves and neg. score)");
+ return false;
+ }
+ to_play = to_play.get_next(m_nu_colors);
+ m_bd.set_to_play(to_play);
+ // Don't try to handle symmetry after pass moves
+ m_is_symmetry_broken = true;
+ }
+
+ auto& moves = m_moves[to_play];
+ LIBBOARDGAME_ASSERT(! moves.empty());
+ auto total_gamma = m_cumulative_gamma[moves.size() - 1];
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Moves: ", moves.size(), ", total_gamma: ",
+ total_gamma);
+ auto begin = m_cumulative_gamma.begin();
+ auto end = begin + moves.size();
+ auto random = m_random.generate_float(0, total_gamma);
+ auto pos = lower_bound(begin, end, random);
+ LIBBOARDGAME_ASSERT(pos != end);
+ mv = PlayerMove<Move>(get_player(),
+ moves[static_cast<unsigned>(pos - begin)]);
+ return true;
+}
+
+string State::get_info() const
+{
+ ostringstream s;
+ if (m_bd.get_nu_players() == 2)
+ {
+ s << "Sco: ";
+ m_stat_score[Color(0)].write(s, true, 1);
+ }
+ s << '\n';
+ return s.str();
+}
+
+inline const PieceMap<bool>& State::get_is_piece_considered(Color c) const
+{
+ if (m_is_callisto
+ && m_bd.get_nu_left_piece(c, m_bd.get_one_piece()) > 1)
+ return m_shared_const.is_piece_considered_none;
+ // Use number of on-board pieces for move number to handle the case where
+ // there are more pieces on the board than moves (setup positions)
+ unsigned nu_moves = m_bd.get_nu_onboard_pieces();
+ if (nu_moves >= m_shared_const.min_move_all_considered
+ || m_force_consider_all_pieces)
+ return m_shared_const.is_piece_considered_all;
+ return *m_shared_const.is_piece_considered[nu_moves];
+}
+
+/** Initializes and returns m_pieces_considered if not all pieces are
+ considered, otherwise m_bd.get_pieces_left(c) is returned. */
+inline const Board::PiecesLeftList& State::get_pieces_considered(Color c)
+{
+ auto is_piece_considered = m_is_piece_considered[c];
+ auto& pieces_left = m_bd.get_pieces_left(c);
+ if (is_piece_considered == &m_shared_const.is_piece_considered_all
+ && ! m_is_callisto)
+ return pieces_left;
+ unsigned n = 0;
+ for (Piece piece : pieces_left)
+ if ((*is_piece_considered)[piece])
+ m_pieces_considered.get_unchecked(n++) = piece;
+ m_pieces_considered.resize(n);
+ return m_pieces_considered;
+}
+
+/** Basic bonus added to the result for quality-based rewards.
+ See also: Pepels et al.: Quality-based Rewards for Monte-Carlo Tree Search
+ Simulations. ECAI 2014. */
+inline Float State::get_quality_bonus(Color c, Float result, Float score)
+{
+ Float bonus = 0;
+
+ // Game length
+ Float l = static_cast<Float>(m_bd.get_nu_moves());
+ m_stat_len.add(l);
+ Float var = m_stat_len.get_variance();
+ if (var > 0)
+ bonus += -0.12f * (result - 0.5f)
+ * sigmoid(2.f, (l - m_stat_len.get_mean()) / sqrt(var));
+
+ // Game score
+ auto& stat = m_stat_score[c];
+ stat.add(score);
+ var = stat.get_variance();
+ if (var > 0)
+ bonus += 0.3f * sigmoid(2.f, (score - stat.get_mean()) / sqrt(var));
+ return bonus;
+}
+
+/** Additional quality-based rewards based on number of attach points.
+ The number of non-forbidden attach points is another feature of a superior
+ final position. Only used in some two-player variants, mainly helps in
+ Trigon. */
+inline Float State::get_quality_bonus_attach_twocolor()
+{
+ LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2);
+ int n = m_bd.get_attach_points(Color(0)).size()
+ - m_bd.get_attach_points(Color(1)).size();
+ for (Point p : m_bd.get_attach_points(Color(0)))
+ n -= m_bd.is_forbidden(p, Color(0));
+ for (Point p : m_bd.get_attach_points(Color(1)))
+ n += m_bd.is_forbidden(p, Color(1));
+ Float attach = static_cast<Float>(n);
+ m_stat_attach.add(attach);
+ auto var = m_stat_attach.get_variance();
+ if (var > 0)
+ return 0.1f * sigmoid(2.f,
+ (attach - m_stat_attach.get_mean()) / sqrt(var));
+ return 0;
+}
+
+/** Like get_quality_bonus_attach_twocolor() but for 2 colors per player. */
+inline Float State::get_quality_bonus_attach_multicolor()
+{
+ LIBBOARDGAME_ASSERT(m_bd.get_nu_players() == 2);
+ LIBBOARDGAME_ASSERT(m_bd.get_nu_colors() == 4);
+ int n = m_bd.get_attach_points(Color(0)).size()
+ + m_bd.get_attach_points(Color(2)).size()
+ - m_bd.get_attach_points(Color(1)).size()
+ - m_bd.get_attach_points(Color(3)).size();
+ for (Point p : m_bd.get_attach_points(Color(0)))
+ n -= m_bd.is_forbidden(p, Color(0));
+ for (Point p : m_bd.get_attach_points(Color(2)))
+ n -= m_bd.is_forbidden(p, Color(2));
+ for (Point p : m_bd.get_attach_points(Color(1)))
+ n += m_bd.is_forbidden(p, Color(1));
+ for (Point p : m_bd.get_attach_points(Color(3)))
+ n += m_bd.is_forbidden(p, Color(3));
+ Float attach = static_cast<Float>(n);
+ m_stat_attach.add(attach);
+ auto var = m_stat_attach.get_variance();
+ if (var > 0)
+ return 0.1f * sigmoid(2.f,
+ (attach - m_stat_attach.get_mean()) / sqrt(var));
+ return 0;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+void State::init_moves_with_gamma(Color c)
+{
+ m_is_piece_considered[c] = &get_is_piece_considered(c);
+ m_playout_features[c].set_local<MAX_SIZE, MAX_ADJ_ATTACH>(m_bd);
+ auto& marker = m_marker[c];
+ auto& moves = m_moves[c];
+ marker.clear(moves);
+ auto& pieces = get_pieces_considered(c);
+ if (m_bd.is_first_piece(c) && ! (MAX_SIZE == 5 && m_is_callisto))
+ add_starting_moves<MAX_SIZE>(c, pieces, true, moves);
+ else
+ {
+ unsigned nu_moves = 0;
+ float total_gamma = 0;
+ if (MAX_SIZE == 5 && m_is_callisto)
+ add_one_piece_moves<MAX_SIZE>(c, true, total_gamma, moves,
+ nu_moves);
+ if (m_is_piece_considered[c]
+ != &m_shared_const.is_piece_considered_none)
+ for (Point p : m_bd.get_attach_points(c))
+ {
+ if (m_bd.is_forbidden(p, c))
+ continue;
+ add_moves<MAX_SIZE, IS_CALLISTO>(p, c, pieces, total_gamma,
+ moves, nu_moves);
+ m_moves_added_at[c][p] = true;
+ }
+ moves.resize(nu_moves);
+ }
+ m_is_move_list_initialized[c] = true;
+ m_nu_new_moves[c] = 0;
+ m_last_attach_points_end[c] = m_bd.get_attach_points(c).end();
+ if (moves.empty() &&
+ m_is_piece_considered[c]
+ != &m_shared_const.is_piece_considered_all)
+ {
+ m_force_consider_all_pieces = true;
+ init_moves_with_gamma<MAX_SIZE, MAX_ADJ_ATTACH, IS_CALLISTO>(c);
+ }
+}
+
+template<unsigned MAX_SIZE>
+void State::init_moves_without_gamma(Color c)
+{
+ m_is_piece_considered[c] = &get_is_piece_considered(c);
+ auto& marker = m_marker[c];
+ auto& moves = m_moves[c];
+ marker.clear(moves);
+ auto& pieces = get_pieces_considered(c);
+ auto& is_forbidden = m_bd.is_forbidden(c);
+ if (m_bd.is_first_piece(c) && ! (MAX_SIZE == 5 && m_is_callisto))
+ add_starting_moves<MAX_SIZE>(c, pieces, false, moves);
+ else
+ {
+ unsigned nu_moves = 0;
+ if (MAX_SIZE == 5 && m_is_callisto)
+ {
+ float total_gamma_dummy;
+ add_one_piece_moves<MAX_SIZE>(c, false, total_gamma_dummy, moves,
+ nu_moves);
+ }
+ if (m_is_piece_considered[c]
+ != &m_shared_const.is_piece_considered_none)
+ for (Point p : m_bd.get_attach_points(c))
+ {
+ if (is_forbidden[p])
+ continue;
+ auto adj_status = m_bd.get_adj_status(p, c);
+ for (Piece piece : pieces)
+ {
+ if (! has_moves(c, piece, p, adj_status))
+ continue;
+ for (Move mv : get_moves(c, piece, p, adj_status))
+ if (! marker[mv]
+ && check_forbidden<MAX_SIZE>(
+ is_forbidden, mv, moves, nu_moves))
+ marker.set(mv);
+ }
+ m_moves_added_at[c][p] = true;
+ }
+ moves.resize(nu_moves);
+ }
+ m_is_move_list_initialized[c] = true;
+ m_nu_new_moves[c] = 0;
+ m_last_attach_points_end[c] = m_bd.get_attach_points(c).end();
+ if (moves.empty() &&
+ m_is_piece_considered[c]
+ != &m_shared_const.is_piece_considered_all)
+ {
+ m_force_consider_all_pieces = true;
+ init_moves_without_gamma<MAX_SIZE>(c);
+ }
+}
+
+void State::play_expanded_child(Move mv)
+{
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Playing expanded child");
+ if (! mv.is_null())
+ play_playout(mv);
+ else
+ {
+ ++m_nu_passes;
+ m_bd.set_to_play(m_bd.get_to_play().get_next(m_nu_colors));
+ // Don't try to handle pass moves: a pass move either breaks symmetry
+ // or both players have passed and it's the end of the game and we need
+ // symmetry detection only as a heuristic (playouts and move value
+ // initialization)
+ m_is_symmetry_broken = true;
+ if (log_simulations)
+ LIBBOARDGAME_LOG(m_bd);
+ }
+}
+
+void State::start_search()
+{
+ auto& bd = *m_shared_const.board;
+ m_bd.copy_from(bd);
+ m_bd.set_to_play(m_shared_const.to_play);
+ m_bd.take_snapshot();
+ m_nu_colors = bd.get_nu_colors();
+ m_is_callisto = (bd.get_piece_set() == PieceSet::callisto);
+ for (Color c : Color::Range(m_nu_colors))
+ m_playout_features[c].init_snapshot(m_bd, c);
+ m_bc = &m_bd.get_board_const();
+ m_max_piece_size = m_bc->get_max_piece_size();
+ m_move_info_array = m_bc->get_move_info_array();
+ m_move_info_ext_array = m_bc->get_move_info_ext_array();
+ m_check_terminate_early =
+ (bd.get_nu_moves() < 10u * m_nu_colors
+ && m_bd.get_nu_players() == 2);
+ auto variant = bd.get_variant();
+ m_check_symmetric_draw =
+ (has_central_symmetry(variant)
+ && ! ((m_shared_const.to_play == Color(1)
+ || m_shared_const.to_play == Color(3))
+ && m_shared_const.avoid_symmetric_draw)
+ && ! check_symmetry_broken(bd));
+ if (! m_check_symmetric_draw)
+ // Pretending that the symmetry is always broken is equivalent to
+ // ignoring symmetric draws
+ m_is_symmetry_broken = true;
+ if (variant == Variant::trigon_2 || variant == Variant::callisto_2)
+ m_symmetry_min_nu_pieces = 5;
+ else
+ {
+ LIBBOARDGAME_ASSERT(! m_check_symmetric_draw || variant == Variant::duo
+ || variant == Variant::junior);
+ m_symmetry_min_nu_pieces = 3;
+ }
+
+ m_prior_knowledge.start_search(bd);
+ m_stat_len.clear();
+ m_stat_attach.clear();
+ for (Color c : Color::Range(m_nu_colors))
+ m_stat_score[c].clear();
+
+ // Init gamma values
+ float gamma_size_factor = 1;
+ float gamma_nu_attach_factor = 1;
+ switch (bd.get_board_type())
+ {
+ case BoardType::classic:
+ gamma_size_factor = 5;
+ break;
+ case BoardType::duo:
+ gamma_size_factor = 3;
+ gamma_nu_attach_factor = 1.8f;
+ break;
+ case BoardType::trigon:
+ case BoardType::trigon_3: // Not tuned
+ gamma_size_factor = 5;
+ break;
+ case BoardType::nexos: // Not tuned
+ gamma_size_factor = 5;
+ gamma_nu_attach_factor = 1.8f;
+ break;
+ case BoardType::callisto_2:
+ case BoardType::callisto: // Not tuned
+ case BoardType::callisto_3: // Not tuned
+ gamma_size_factor = 12;
+ gamma_nu_attach_factor = 1.8f;
+ break;
+ }
+ for (Piece::IntType i = 0; i < m_bc->get_nu_pieces(); ++i)
+ {
+ Piece piece(i);
+ auto score_points = m_bc->get_piece_info(piece).get_score_points();
+ auto piece_nu_attach =
+ static_cast<float>(m_bc->get_nu_attach_points(piece));
+ LIBBOARDGAME_ASSERT(score_points >= 0);
+ LIBBOARDGAME_ASSERT(piece_nu_attach > 0);
+ m_gamma_piece[piece] =
+ pow(gamma_size_factor, score_points)
+ * pow(gamma_nu_attach_factor, piece_nu_attach - 1);
+ }
+}
+
+void State::start_simulation(size_t n)
+{
+#if LIBBOARDGAME_DISABLE_LOG
+ LIBBOARDGAME_UNUSED(n);
+#endif
+ if (log_simulations)
+ LIBBOARDGAME_LOG("=================================================\n",
+ "Simulation ", n, "\n",
+ "=================================================");
+ m_bd.restore_snapshot();
+ m_force_consider_all_pieces = false;
+ auto& geo = m_bd.get_geometry();
+ for (Color c : Color::Range(m_nu_colors))
+ {
+ m_has_moves[c] = true;
+ m_is_move_list_initialized[c] = false;
+ m_playout_features[c].restore_snapshot(m_bd);
+ m_moves_added_at[c].fill(false, geo);
+ }
+ m_nu_passes = 0;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+void State::update_moves(Color c)
+{
+ auto& playout_features = m_playout_features[c];
+ playout_features.set_local<MAX_SIZE, MAX_ADJ_ATTACH>(m_bd);
+
+ auto& marker = m_marker[c];
+
+ // Find old moves that are still legal
+ auto& is_forbidden = m_bd.is_forbidden(c);
+ auto& moves = m_moves[c];
+ unsigned nu_moves = 0;
+ float total_gamma = 0;
+ Piece piece;
+ if (m_nu_new_moves[c] == 1 &&
+ ! m_bd.is_piece_left(
+ c, (piece =
+ get_move_info<MAX_SIZE>(m_last_move[c]).get_piece())))
+ for (Move mv : moves)
+ {
+ auto& info = get_move_info<MAX_SIZE>(mv);
+ if (info.get_piece() == piece
+ || ! check_move<MAX_SIZE, IS_CALLISTO>(
+ mv, info, moves, nu_moves, playout_features,
+ total_gamma))
+ marker.clear(mv);
+ }
+ else
+ for (Move mv : moves)
+ {
+ auto& info = get_move_info<MAX_SIZE>(mv);
+ if (! m_bd.is_piece_left(c, info.get_piece())
+ || ! check_move<MAX_SIZE, IS_CALLISTO>(
+ mv, info, moves, nu_moves, playout_features,
+ total_gamma))
+ marker.clear(mv);
+ }
+
+ // Find new legal moves because of new pieces played by this color
+ auto& pieces = get_pieces_considered(c);
+ auto& attach_points = m_bd.get_attach_points(c);
+ auto begin = m_last_attach_points_end[c];
+ auto end = attach_points.end();
+ for (auto i = begin; i != end; ++i)
+ if (! is_forbidden[*i] && ! m_moves_added_at[c][*i])
+ {
+ m_moves_added_at[c][*i] = true;
+ add_moves<MAX_SIZE, IS_CALLISTO>(*i, c, pieces, total_gamma, moves,
+ nu_moves);
+ }
+ m_nu_new_moves[c] = 0;
+ m_last_attach_points_end[c] = end;
+
+ // Generate moves for pieces not considered in the last position
+ if (m_is_piece_considered[c] != &m_shared_const.is_piece_considered_all)
+ {
+ auto& is_piece_considered = *m_is_piece_considered[c];
+ if (nu_moves == 0)
+ m_force_consider_all_pieces = true;
+ auto& is_piece_considered_new = get_is_piece_considered(c);
+ if (&is_piece_considered != &is_piece_considered_new)
+ {
+ Board::PiecesLeftList new_pieces;
+ unsigned n = 0;
+ for (Piece piece : m_bd.get_pieces_left(c))
+ if (! is_piece_considered[piece]
+ && is_piece_considered_new[piece])
+ new_pieces.get_unchecked(n++) = piece;
+ new_pieces.resize(n);
+ for (Point p : attach_points)
+ if (! is_forbidden[p])
+ add_moves<MAX_SIZE, IS_CALLISTO>(
+ p, c, new_pieces, total_gamma, moves, nu_moves);
+ m_is_piece_considered[c] = &is_piece_considered_new;
+ }
+ }
+ moves.resize(nu_moves);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/State.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_STATE_H
+#define LIBPENTOBI_MCTS_STATE_H
+
+#include "PlayoutFeatures.h"
+#include "PriorKnowledge.h"
+#include "SharedConst.h"
+#include "StateUtil.h"
+#include "libboardgame_mcts/LastGoodReply.h"
+#include "libboardgame_mcts/PlayerMove.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/RandomGenerator.h"
+#include "libboardgame_util/Statistics.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_mcts::LastGoodReply;
+using libboardgame_mcts::PlayerInt;
+using libboardgame_mcts::PlayerMove;
+using libboardgame_util::RandomGenerator;
+using libboardgame_util::Statistics;
+using libpentobi_base::BoardConst;
+using libpentobi_base::MoveInfo;
+using libpentobi_base::MoveInfoExt;
+using libpentobi_base::Piece;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::PieceSet;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** A state of a simulation.
+ This class contains modifiable data used in a simulation. In multi-threaded
+ search (not yet implemented), each thread uses its own instance of this
+ class.
+ This class incrementally keeps track of the legal moves.
+ The randomization in the playouts is done by assigning a heuristically
+ tuned gamma value to each move. The gamma value determines the probabilty
+ that a move is played in the playout phase. */
+class State
+{
+public:
+ typedef libboardgame_mcts::Node<Move, Float, SearchParamConst::multithread>
+ Node;
+
+ typedef libboardgame_mcts::Tree<Node> Tree;
+
+ typedef libboardgame_mcts::LastGoodReply<Move,
+ SearchParamConst::max_players,
+ SearchParamConst::lgr_hash_table_size,
+ SearchParamConst::multithread>
+ LastGoodReply;
+
+ /** Constructor.
+ @param initial_variant Game variant to initialize the internal
+ board with (may avoid unnecessary BoardConst creation for game variant
+ that is never used)
+ @param shared_const (@ref libboardgame_doc_storesref) */
+ State(Variant initial_variant, const SharedConst& shared_const);
+
+ State& operator=(const State&) = delete;
+
+ /** Play a move in the in-tree phase of the search. */
+ void play_in_tree(Move mv);
+
+ /** Handle end of in-tree phase. */
+ void finish_in_tree();
+
+ /** Play a move right after expanding a node. */
+ void play_expanded_child(Move mv);
+
+ /** Finish in-tree phase without expanding a node. */
+ void finish_in_tree_no_expansion();
+
+ /** Get current player to play. */
+ PlayerInt get_player() const;
+
+ void start_search();
+
+ void start_simulation(size_t n);
+
+ bool gen_children(Tree::NodeExpander& expander, Float root_val);
+
+ void start_playout() { }
+
+ /** Generate a playout move.
+ @return @c false if end of game was reached, and no move was
+ generated. */
+ bool gen_playout_move(const LastGoodReply& lgr, Move last,
+ Move second_last, PlayerMove<Move>& move);
+
+ void evaluate_playout(array<Float, 6>& result);
+
+ void play_playout(Move mv);
+
+ /** Do not update RAVE values for n'th move of the current simulation. */
+ bool skip_rave(Move mv) const;
+
+#if LIBBOARDGAME_DEBUG
+ string dump() const;
+#endif
+
+ string get_info() const;
+
+private:
+ static const bool log_simulations = false;
+
+ /** The cumulative gamma value of the moves in m_moves. */
+ array<float, MoveList::max_size> m_cumulative_gamma;
+
+ Color::IntType m_nu_passes;
+
+ const SharedConst& m_shared_const;
+
+ Board m_bd;
+
+ const BoardConst* m_bc;
+
+ Color::IntType m_nu_colors;
+
+ BoardConst::MoveInfoArray m_move_info_array;
+
+ BoardConst::MoveInfoExtArray m_move_info_ext_array;
+
+ /** Incrementally updated lists of legal moves for both colors.
+ Only the move list for the color to play van be used in any given
+ position, the other color is not updated immediately after a move. */
+ ColorMap<MoveList> m_moves;
+
+ ColorMap<const PieceMap<bool>*> m_is_piece_considered;
+
+ /** The list of pieces considered in the current move if not all pieces
+ are considered. */
+ Board::PiecesLeftList m_pieces_considered;
+
+ PriorKnowledge m_prior_knowledge;
+
+ /** Gamma value for a piece. */
+ PieceMap<float> m_gamma_piece;
+
+ /** Number of moves played by a color since the last update of its move
+ list. */
+ ColorMap<unsigned> m_nu_new_moves;
+
+ /** Board::get_attach_points().end() for a color at the last update of
+ its move list. */
+ ColorMap<PointList::const_iterator> m_last_attach_points_end;
+
+ /** Last move played by a color since the last update of its move list. */
+ ColorMap<Move> m_last_move;
+
+ ColorMap<bool> m_is_move_list_initialized;
+
+ ColorMap<bool> m_has_moves;
+
+ /** Marks moves contained in m_moves. */
+ ColorMap<MoveMarker> m_marker;
+
+ ColorMap<PlayoutFeatures> m_playout_features;
+
+ RandomGenerator m_random;
+
+ /** Used in get_quality_bonus(). */
+ ColorMap<Statistics<Float>> m_stat_score;
+
+ /** Used in get_quality_bonus(). */
+ Statistics<Float> m_stat_len;
+
+ /** Used in get_quality_bonus(). */
+ Statistics<Float> m_stat_attach;
+
+ bool m_check_symmetric_draw;
+
+ bool m_check_terminate_early;
+
+ bool m_is_symmetry_broken;
+
+ /** Enforce all pieces to be considered for the rest of the simulation.
+ This applies to all colors, because it is only used if no moves were
+ generated because not all pieces were considered and this case is so
+ rare that it is not worth the cost of setting such a flag for each
+ color individually. */
+ bool m_force_consider_all_pieces;
+
+ bool m_is_callisto;
+
+ /** Minimum number of pieces on board to perform a symmetry check.
+ 3 in Duo/Junior or 5 in Trigon because this is the earliest move number
+ to break the symmetry. The early playout termination that evaluates all
+ symmetric positions as a draw should not be used earlier because it can
+ cause bad move selection in very short searches if all moves are
+ evaluated as draw and the search is not deep enough to find that the
+ symmetry can be broken a few moves later. */
+ unsigned m_symmetry_min_nu_pieces;
+
+ /** Cache of m_bc->get_max_piece_size() */
+ unsigned m_max_piece_size;
+
+ /** Remember attach points that were already used for move generation.
+ Allows the incremental update of the move lists to skip attach points
+ of newly played pieces that were already attach points of previously
+ played pieces. */
+ ColorMap<Grid<bool>> m_moves_added_at;
+
+
+ template<unsigned MAX_SIZE, bool IS_CALLISTO>
+ void add_moves(Point p, Color c, const Board::PiecesLeftList& pieces,
+ float& total_gamma, MoveList& moves, unsigned& nu_moves);
+
+ template<unsigned MAX_SIZE>
+ LIBBOARDGAME_NOINLINE
+ void add_starting_moves(Color c, const Board::PiecesLeftList& pieces,
+ bool with_gamma, MoveList& moves);
+
+ template<unsigned MAX_SIZE>
+ LIBBOARDGAME_NOINLINE
+ void add_one_piece_moves(Color c, bool with_gamma, float& total_gamma,
+ MoveList& moves, unsigned& nu_moves);
+
+ void evaluate_multicolor(array<Float, 6>& result);
+
+ void evaluate_multiplayer(array<Float, 6>& result);
+
+ void evaluate_twocolor(array<Float, 6>& result);
+
+ Point find_best_starting_point(Color c) const;
+
+ Float get_quality_bonus(Color c, Float result, Float score);
+
+ Float get_quality_bonus_attach_twocolor();
+
+ Float get_quality_bonus_attach_multicolor();
+
+ template<unsigned MAX_SIZE>
+ const MoveInfo<MAX_SIZE>& get_move_info(Move mv) const;
+
+ template<unsigned MAX_ADJ_ATTACH>
+ const MoveInfoExt<MAX_ADJ_ATTACH>& get_move_info_ext(Move mv) const;
+
+ PrecompMoves::Range get_moves(Color c, Piece piece, Point p,
+ unsigned adj_status) const;
+
+ bool has_moves(Color c, Piece piece, Point p, unsigned adj_status) const;
+
+ const PieceMap<bool>& get_is_piece_considered(Color c) const;
+
+ const Board::PiecesLeftList& get_pieces_considered(Color c);
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+ void init_moves_with_gamma(Color c);
+
+ template<unsigned MAX_SIZE>
+ void init_moves_without_gamma(Color c);
+
+ template<unsigned MAX_SIZE>
+ bool check_forbidden(const GridExt<bool>& is_forbidden, Move mv,
+ MoveList& moves, unsigned& nu_moves);
+
+ bool check_lgr(Move mv) const;
+
+ template<unsigned MAX_SIZE, bool IS_CALLISTO>
+ bool check_move(Move mv, const MoveInfo<MAX_SIZE>& info, float gamma_piece,
+ MoveList& moves, unsigned& nu_moves,
+ const PlayoutFeatures& playout_features,
+ float& total_gamma);
+
+ template<unsigned MAX_SIZE, bool IS_CALLISTO>
+ bool check_move(Move mv, const MoveInfo<MAX_SIZE>& info, MoveList& moves,
+ unsigned& nu_moves,
+ const PlayoutFeatures& playout_features,
+ float& total_gamma);
+
+ bool gen_playout_move_full(PlayerMove<Move>& mv);
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+ void update_moves(Color c);
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void update_playout_features(Color c, Move mv);
+
+ template<unsigned MAX_SIZE>
+ LIBBOARDGAME_NOINLINE void update_symmetry_broken(Move mv);
+};
+
+/** Check if last-good-reply move is applicable.
+ To be faster, it doesn't check for starting moves because such moves rarely
+ occur in the playout phase and doesn't check if a 1-piece move is in the
+ center in Callisto because such moves are not generated in the search. */
+inline bool State::check_lgr(Move mv) const
+{
+ if (mv.is_null())
+ return false;
+ Color c = m_bd.get_to_play();
+ auto piece = m_bd.get_move_piece(mv);
+ if (! m_bd.is_piece_left(c, piece))
+ return false;
+ auto points = m_bd.get_move_points(mv);
+ auto i = points.begin();
+ auto end = points.end();
+ bool has_attach_point = false;
+ do
+ {
+ if (m_bd.is_forbidden(*i, c))
+ return false;
+ // Logically, we mean:
+ // has_attach_point = has_attach_point || is_attach_point(*i, c)
+ // But this generates branches, which are bad for performance in this
+ // tight loop (unrolled by the compiler). So we use a bitwise OR, which
+ // works because C++ guarantees that true/false converts to 1/0.
+ has_attach_point |= m_bd.is_attach_point(*i, c);
+ }
+ while (++i != end);
+ if (m_is_callisto)
+ {
+ Piece one_piece = m_bd.get_one_piece();
+ if (piece == one_piece)
+ return true;
+ if (m_bd.get_nu_left_piece(c, one_piece) > 1 && piece != one_piece)
+ return false;
+ }
+ return has_attach_point;
+}
+
+inline void State::evaluate_playout(array<Float, 6>& result)
+{
+ auto nu_players = m_bd.get_nu_players();
+ if (nu_players == 2)
+ {
+ if (m_nu_colors == 2)
+ evaluate_twocolor(result);
+ else
+ evaluate_multicolor(result);
+ }
+ else
+ evaluate_multiplayer(result);
+}
+
+inline void State::finish_in_tree()
+{
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Finish in-tree");
+ if (m_check_symmetric_draw)
+ m_is_symmetry_broken = check_symmetry_broken(m_bd);
+}
+
+inline bool State::gen_playout_move(const LastGoodReply& lgr, Move last,
+ Move second_last, PlayerMove<Move>& mv)
+{
+ if (m_nu_passes == m_nu_colors)
+ return false;
+ if (! m_is_symmetry_broken
+ && m_bd.get_nu_onboard_pieces() >= m_symmetry_min_nu_pieces)
+ {
+ // See also the comment in evaluate_playout()
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Terminate playout. Symmetry not broken.");
+ return false;
+ }
+ PlayerInt player = get_player();
+ Move lgr2 = lgr.get_lgr2(player, last, second_last);
+ if (check_lgr(lgr2))
+ {
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Playing last good reply 2");
+ mv = PlayerMove<Move>(player, lgr2);
+ return true;
+ }
+ Move lgr1 = lgr.get_lgr1(player, last);
+ if (check_lgr(lgr1))
+ {
+ if (log_simulations)
+ LIBBOARDGAME_LOG("Playing last good reply 1");
+ mv = PlayerMove<Move>(player, lgr1);
+ return true;
+ }
+ return gen_playout_move_full(mv);
+}
+
+template<unsigned MAX_SIZE>
+inline const MoveInfo<MAX_SIZE>& State::get_move_info(Move mv) const
+{
+ LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves());
+ return BoardConst::get_move_info<MAX_SIZE>(mv, m_move_info_array);
+}
+
+template<unsigned MAX_ADJ_ATTACH>
+inline const MoveInfoExt<MAX_ADJ_ATTACH>& State::get_move_info_ext(
+ Move mv) const
+{
+ LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_nu_moves());
+ return BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+ mv, m_move_info_ext_array);
+}
+
+inline PrecompMoves::Range State::get_moves(Color c, Piece piece, Point p,
+ unsigned adj_status) const
+{
+ return m_shared_const.precomp_moves[c].get_moves(piece, p, adj_status);
+}
+
+inline PlayerInt State::get_player() const
+{
+ unsigned player = m_bd.get_to_play().to_int();
+ if ( m_bd.get_variant() == Variant::classic_3 && player == 3)
+ player += m_bd.get_alt_player();
+ return static_cast<PlayerInt>(player);
+}
+
+inline bool State::has_moves(Color c, Piece piece, Point p,
+ unsigned adj_status) const
+{
+ return m_shared_const.precomp_moves[c].has_moves(piece, p, adj_status);
+}
+
+inline void State::play_in_tree(Move mv)
+{
+ Color to_play = m_bd.get_to_play();
+ if (! mv.is_null())
+ {
+ LIBBOARDGAME_ASSERT(m_bd.is_legal(to_play, mv));
+ m_nu_passes = 0;
+ if (m_max_piece_size == 5)
+ {
+ m_bd.play<5, 16>(to_play, mv);
+ update_playout_features<5, 16>(to_play, mv);
+ }
+ else if (m_max_piece_size == 6)
+ {
+ m_bd.play<6, 22>(to_play, mv);
+ update_playout_features<6, 22>(to_play, mv);
+ }
+ else
+ {
+ m_bd.play<7, 12>(to_play, mv);
+ update_playout_features<7, 12>(to_play, mv);
+ }
+ }
+ else
+ {
+ ++m_nu_passes;
+ m_bd.set_to_play(to_play.get_next(m_nu_colors));
+ }
+ if (log_simulations)
+ LIBBOARDGAME_LOG(m_bd);
+}
+
+inline void State::play_playout(Move mv)
+{
+ auto to_play = m_bd.get_to_play();
+ LIBBOARDGAME_ASSERT(m_bd.is_legal(to_play, mv));
+ if (m_max_piece_size == 5)
+ {
+ m_bd.play<5, 16>(to_play, mv);
+ update_playout_features<5, 16>(to_play, mv);
+ if (! m_is_symmetry_broken)
+ update_symmetry_broken<5>(mv);
+ }
+ else if (m_max_piece_size == 6)
+ {
+ m_bd.play<6, 22>(to_play, mv);
+ update_playout_features<6, 22>(to_play, mv);
+ if (! m_is_symmetry_broken)
+ update_symmetry_broken<6>(mv);
+ }
+ else
+ {
+ m_bd.play<7, 12>(to_play, mv);
+ update_playout_features<7, 12>(to_play, mv);
+ // No game variant with piece size 7 uses m_is_symmetry_broken
+ }
+ ++m_nu_new_moves[to_play];
+ m_last_move[to_play] = mv;
+ m_nu_passes = 0;
+ if (log_simulations)
+ LIBBOARDGAME_LOG(m_bd);
+}
+
+inline bool State::skip_rave(Move mv) const
+{
+ LIBBOARDGAME_UNUSED(mv);
+ return false;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void State::update_playout_features(Color c, Move mv)
+{
+ auto& info = get_move_info<MAX_SIZE>(mv);
+ for (Color i : Color::Range(m_nu_colors))
+ m_playout_features[i].set_forbidden(info);
+ m_playout_features[c].set_forbidden<MAX_ADJ_ATTACH>(
+ get_move_info_ext<MAX_ADJ_ATTACH>(mv));
+}
+
+template<unsigned MAX_SIZE>
+void State::update_symmetry_broken(Move mv)
+{
+ Color to_play = m_bd.get_to_play();
+ Color second_color = m_bd.get_second_color(to_play);
+ auto& symmetric_points = m_bc->get_symmetrc_points();
+ auto& info = get_move_info<MAX_SIZE>(mv);
+ auto i = info.begin();
+ auto end = info.end();
+ if (to_play == Color(0) || to_play == Color(2))
+ {
+ // First player to play: Check that all symmetric points of the last
+ // move of the second player are occupied by the first player
+ do
+ {
+ Point symm_p = symmetric_points[*i];
+ if (m_bd.get_point_state(symm_p) != second_color)
+ {
+ m_is_symmetry_broken = true;
+ return;
+ }
+ }
+ while (++i != end);
+ }
+ else
+ {
+ // Second player to play: Check that all symmetric points of the last
+ // move of the first player are empty (i.e. the second player can play
+ // there to preserve the symmetry)
+ do
+ {
+ Point symm_p = symmetric_points[*i];
+ if (! m_bd.get_point_state(symm_p).is_empty())
+ {
+ m_is_symmetry_broken = true;
+ return;
+ }
+ }
+ while (++i != end);
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_STATE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/StateUtil.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "StateUtil.h"
+
+namespace libpentobi_mcts {
+
+using libpentobi_base::Color;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Geometry;
+using libpentobi_base::Point;
+using libpentobi_base::PointState;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+array<Color, Color::range> symmetric_state =
+ { Color(1), Color(0), Color(3), Color(2) };
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+bool check_symmetry_broken(const Board& bd)
+{
+ LIBBOARDGAME_ASSERT(has_central_symmetry(bd.get_variant()));
+ auto& symmetric_points = bd.get_board_const().get_symmetrc_points();
+ Color to_play = bd.get_to_play();
+ auto& geo = bd.get_geometry();
+ // No need to iterator over the whole board when checking symmetry (this
+ // makes the assumption that the symmetric points of the points in the
+ // first half of the integer range are in the second half).
+ Geometry::Iterator begin = geo.begin();
+ LIBBOARDGAME_ASSERT(geo.get_range() % 2 == 0);
+ Geometry::Iterator end(static_cast<Point::IntType>(geo.get_range() / 2));
+#if LIBBOARDGAME_DEBUG
+ for (auto p = begin; p != end; ++p)
+ LIBBOARDGAME_ASSERT(symmetric_points[*p].to_int() >= (*end).to_int());
+#endif
+ if (to_play == Color(0) || to_play == Color(2))
+ {
+ // First player to play: the symmetry is broken if the position is
+ // not symmetric.
+ for (auto p = begin; p != end; ++p)
+ {
+ PointState s1 = bd.get_point_state(*p);
+ if (! s1.is_empty())
+ {
+ Point symm_p = symmetric_points[*p];
+ PointState s2 = bd.get_point_state(symm_p);
+ if (s2 != symmetric_state[s1.to_int()])
+ return true;
+ }
+ }
+ }
+ else
+ {
+ // Second player to play: the symmetry is broken if the second player
+ // cannot copy the first player's last move to make the position
+ // symmetric again.
+ unsigned nu_moves = bd.get_nu_moves();
+ if (nu_moves == 0)
+ // Don't try to handle the case if the second player has to play as
+ // first move (e.g. in setup positions)
+ return true;
+ Color previous_color = bd.get_previous(to_play);
+ ColorMove last_mv = bd.get_move(nu_moves - 1);
+ if (last_mv.color != previous_color)
+ // Don't try to handle non-alternating moves in board history
+ return true;
+ auto points = bd.get_move_points(last_mv.move);
+ for (Point p : points)
+ if (! bd.get_point_state(symmetric_points[p]).is_empty())
+ return true;
+ for (auto p = begin; p != end; ++p)
+ {
+ PointState s1 = bd.get_point_state(*p);
+ if (! s1.is_empty())
+ {
+ PointState s2 = bd.get_point_state(symmetric_points[*p]);
+ if (s2 != symmetric_state[s1.to_int()]
+ && ! points.contains(*p))
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/StateUtil.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_STATE_UTIL_H
+#define LIBPENTOBI_MCTS_STATE_UTIL_H
+
+#include "libpentobi_base/Board.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libpentobi_base::Board;
+
+//-----------------------------------------------------------------------------
+
+bool check_symmetry_broken(const Board& bd);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_STATE_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Util.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Util.h"
+
+#include <thread>
+#include "libboardgame_sgf/Writer.h"
+#include "libboardgame_util/Log.h"
+#include "libpentobi_base/BoardUtil.h"
+#include "libpentobi_base/PentobiSgfUtil.h"
+
+namespace libpentobi_mcts {
+namespace util {
+
+using libboardgame_mcts::Node;
+using libboardgame_mcts::Tree;
+using libboardgame_sgf::Writer;
+using libpentobi_base::boardutil::write_setup;
+using libpentobi_base::sgf_util::get_color_id;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void dump_tree_recurse(Writer& writer, Variant variant,
+ const Search::Tree& tree, const Search::Node& node,
+ Color to_play)
+{
+ ostringstream comment;
+ comment << "Visits: " << node.get_visit_count()
+ << "\nVal: " << node.get_value()
+ << "\nCnt: " << node.get_value_count();
+ writer.write_property("C", comment.str());
+ writer.end_node();
+ Color next_to_play = to_play.get_next(get_nu_colors(variant));
+ vector<const Search::Node*> children;
+ for (auto& i : tree.get_children(node))
+ children.push_back(&i);
+ sort(children.begin(), children.end(), compare_node);
+ for (const auto i : children)
+ {
+ writer.begin_tree();
+ writer.begin_node();
+ auto mv = i->get_move();
+ if (! mv.is_null())
+ {
+ auto& board_const = BoardConst::get(variant);
+ auto id = get_color_id(variant, to_play);
+ if (! mv.is_null())
+ writer.write_property(id, board_const.to_string(mv, false));
+ }
+ dump_tree_recurse(writer, variant, tree, *i, next_to_play);
+ writer.end_tree();
+ }
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+bool compare_node(const Search::Node* n1, const Search::Node* n2)
+{
+ Float count1 = n1->get_visit_count();
+ Float count2 = n2->get_visit_count();
+ if (count1 != count2)
+ return count1 > count2;
+ return n1->get_value() > n2->get_value();
+}
+
+void dump_tree(ostream& out, const Search& search)
+{
+ Variant variant;
+ Setup setup;
+ search.get_root_position(variant, setup);
+ Writer writer(out);
+ writer.begin_tree();
+ writer.begin_node();
+ writer.write_property("GM", to_string(variant));
+ write_setup(writer, variant, setup);
+ writer.write_property("PL", get_color_id(variant, setup.to_play));
+ auto& tree = search.get_tree();
+ dump_tree_recurse(writer, variant, tree, tree.get_root(), setup.to_play);
+ writer.end_tree();
+}
+
+unsigned get_nu_threads()
+{
+ unsigned nu_threads = thread::hardware_concurrency();
+ if (nu_threads == 0)
+ {
+ LIBBOARDGAME_LOG("Could not determine the number of hardware threads");
+ nu_threads = 1;
+ }
+ // The lock-free search probably scales up to 16-32 threads, but we
+ // haven't tested more than 4 threads, we still use single precision
+ // float for LIBBOARDGAME_MCTS_FLOAT_TYPE (which limits the maximum number
+ // of simulations per search) and CPUs with more than 4 cores are
+ // currently not very common anyway. Also, the loss of playing strength
+ // of a multi-threaded search with the same count as a single-threaded
+ // search will become larger with many threads, so there would need to be
+ // a correction factor in the number of simulations per level to take this
+ // into account.
+ if (nu_threads > 4)
+ nu_threads = 4;
+ return nu_threads;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace util
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Util.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_UTIL_H
+#define LIBPENTOBI_MCTS_UTIL_H
+
+#include "Search.h"
+
+namespace libpentobi_mcts {
+namespace util {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Comparison function for sorting children of a node by count.
+ Prefers nodes with higher counts. Uses the node value as a tie breaker. */
+bool compare_node(const Search::Node* n1, const Search::Node* n2);
+
+/** Dump the search tree in SGF format. */
+void dump_tree(ostream& out, const Search& search);
+
+/** Suggest how many threads to use in the search depending on the current
+ system. */
+unsigned get_nu_threads();
+
+//-----------------------------------------------------------------------------
+
+} // namespace util
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_UTIL_H
--- /dev/null
+add_library(pentobi_thumbnail STATIC
+ CreateThumbnail.h
+ CreateThumbnail.cpp
+)
+
+target_link_libraries(pentobi_thumbnail Qt5::Widgets)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_thumbnail/CreateThumbnail.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CreateThumbnail.h"
+
+#include <iostream>
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_util/StringUtil.h"
+#include "libpentobi_base/NodeUtil.h"
+#include "libpentobi_gui/BoardPainter.h"
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libboardgame_sgf::TreeReader;
+using libboardgame_util::split;
+using libboardgame_util::trim;
+using libpentobi_base::get_board_type;
+using libpentobi_base::Geometry;
+using libpentobi_base::Grid;
+using libpentobi_base::PieceSet;
+using libpentobi_base::PointState;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** Helper function for getFinalPosition() */
+void handleSetup(const char* id, Color c, const SgfNode& node,
+ const Geometry& geo, Grid<PointState>& pointState,
+ Grid<unsigned>& pieceId, unsigned& currentPieceId)
+{
+ vector<string> values = node.get_multi_property(id);
+ for (const string& s : values)
+ {
+ if (trim(s).empty())
+ continue;
+ vector<string> v = split(s, ',');
+ ++currentPieceId;
+ for (const string& p_str : v)
+ {
+ Point p;
+ if (geo.from_string(p_str, p))
+ {
+ pointState[p] = PointState(c);
+ pieceId[p] = currentPieceId;
+ }
+ }
+ }
+}
+
+/** Helper function for getFinalPosition() */
+void handleSetupEmpty(const SgfNode& node, const Geometry& geo,
+ Grid<PointState>& pointState, Grid<unsigned>& pieceId)
+{
+ vector<string> values = node.get_multi_property("AE");
+ for (const auto& s : values)
+ {
+ if (trim(s).empty())
+ continue;
+ vector<string> v = split(s, ',');
+ for (const auto& p_str : v)
+ {
+ Point p;
+ if (geo.from_string(p_str, p))
+ {
+ pointState[p] = PointState::empty();
+ pieceId[p] = 0;
+ }
+ }
+ }
+}
+
+/** Get the board state of the final position of the main variation.
+ Avoids constructing an instance of a Tree or Game, which would do a costly
+ initialization of BoardConst and slow down the thumbnailer
+ unnecessarily. */
+bool getFinalPosition(const SgfNode& root, Variant& variant,
+ const Geometry*& geo, Grid<PointState>& pointState,
+ Grid<unsigned>& pieceId)
+{
+ if (! parse_variant(root.get_property("GM", ""), variant))
+ return false;
+ geo = &get_geometry(variant);
+ pointState.fill(PointState::empty(), *geo);
+ auto pieceSet = get_piece_set(variant);
+ if (pieceSet == PieceSet::nexos || pieceSet == PieceSet::callisto)
+ pieceId.fill(0, *geo);
+ auto node = &root;
+ unsigned id = 0;
+ while (node)
+ {
+ if (libpentobi_base::node_util::has_setup(*node))
+ {
+ handleSetup("AB", Color(0), *node, *geo, pointState, pieceId, id);
+ handleSetup("AW", Color(1), *node, *geo, pointState, pieceId, id);
+ handleSetup("A1", Color(0), *node, *geo, pointState, pieceId, id);
+ handleSetup("A2", Color(1), *node, *geo, pointState, pieceId, id);
+ handleSetup("A3", Color(2), *node, *geo, pointState, pieceId, id);
+ handleSetup("A4", Color(3), *node, *geo, pointState, pieceId, id);
+ handleSetupEmpty(*node, *geo, pointState, pieceId);
+ if (node == &root)
+ // If the file starts with a setup (e.g. a puzzle), we use this
+ // position for the thumbnail.
+ break;
+ }
+ Color c;
+ MovePoints points;
+ if (libpentobi_base::node_util::get_move(*node, variant, c, points))
+ {
+ ++id;
+ for (Point p : points)
+ {
+ pointState[p] = PointState(c);
+ pieceId[p] = id;
+ }
+ }
+ node = node->get_first_child_or_null();
+ }
+ return true;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+bool createThumbnail(const QString& path, int width, int height,
+ QImage& image)
+{
+ TreeReader reader;
+ reader.set_read_only_main_variation(true);
+ reader.read(path.toLocal8Bit().constData());
+ auto variant =
+ Variant::classic; // Initialize to avoid compiler warning
+ const Geometry* geo;
+ Grid<PointState> pointState;
+ Grid<unsigned> pieceId;
+ if (! getFinalPosition(reader.get_tree(), variant, geo, pointState,
+ pieceId))
+ {
+ cerr << "Not a valid Blokus SGF file\n";
+ return false;
+ }
+ QPainter painter;
+ if (! painter.begin(&image))
+ return false;
+ BoardPainter boardPainter;
+ boardPainter.paintEmptyBoard(painter, width, height, variant, *geo);
+ boardPainter.paintPieces(painter, pointState, pieceId);
+ painter.end();
+ return true;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_thumbnail/CreateThumbnail.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H
+#define LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H
+
+class QImage;
+class QString;
+
+//-----------------------------------------------------------------------------
+
+bool createThumbnail(const QString& path, int width, int height,
+ QImage& image);
+
+//-----------------------------------------------------------------------------
+
+#endif // LIBPENTOBI_THUMBNAIL_CREATE_THUMBNAIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeGameWidget.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "AnalyzeGameWidget.h"
+
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QLabel>
+#include <QMouseEvent>
+#include <QProgressDialog>
+#include <QtConcurrentRun>
+#include "Util.h"
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_util/Abort.h"
+#include "libpentobi_gui/Util.h"
+
+using libboardgame_sgf::util::find_root;
+using libboardgame_sgf::util::is_main_variation;
+using libboardgame_util::set_abort;
+using libboardgame_util::ArrayList;
+using libpentobi_base::Board;
+using libpentobi_base::PentobiTree;
+
+//-----------------------------------------------------------------------------
+
+AnalyzeGameWidget::AnalyzeGameWidget(QWidget* parent)
+ : QWidget(parent)
+{
+ setMinimumSize(240, 120);
+ m_isInitialized = false;
+ m_currentPosition = -1;
+}
+
+void AnalyzeGameWidget::cancel()
+{
+ if (! m_isRunning)
+ return;
+ set_abort();
+ m_future.waitForFinished();
+}
+
+void AnalyzeGameWidget::initSize()
+{
+ m_borderX = width() / 50;
+ m_borderY = height() / 20;
+ m_maxX = width() - 2 * m_borderX;
+ m_dX = qreal(m_maxX) / Board::max_game_moves;
+ m_maxY = height() - 2 * m_borderY;
+}
+
+void AnalyzeGameWidget::mousePressEvent(QMouseEvent* event)
+{
+ if (! m_isInitialized && m_isRunning)
+ return;
+ unsigned moveNumber =
+ static_cast<unsigned>((event->x() - m_borderX) / m_dX);
+ if (moveNumber >= m_analyzeGame.get_nu_moves())
+ return;
+ vector<ColorMove> moves;
+ for (unsigned i = 0; i < moveNumber; ++i)
+ moves.push_back(m_analyzeGame.get_move(i));
+ emit gotoPosition(m_analyzeGame.get_variant(), moves);
+}
+
+void AnalyzeGameWidget::paintEvent(QPaintEvent*)
+{
+ if (! m_isInitialized)
+ return;
+ QPainter painter(this);
+ QFont font;
+ font.setStyleStrategy(QFont::PreferOutline);
+ // QFont::setPixelSize(0) prints a warning even if it works and the docs
+ // of Qt 5.3 don't forbid it (unlike QFont::setPointSize(0)).
+ font.setPixelSize(max(1, static_cast<int>(0.06 * height())));
+ QFontMetrics metrics(font);
+ painter.translate(m_borderX, m_borderY);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(QColor(240, 240, 240));
+ painter.drawRect(0, 0, m_maxX, m_maxY);
+ unsigned nu_moves = m_analyzeGame.get_nu_moves();
+ if (m_currentPosition >= 0
+ && static_cast<unsigned>(m_currentPosition) < nu_moves)
+ {
+ QPen pen(QColor(96, 96, 96));
+ pen.setStyle(Qt::DotLine);
+ painter.setPen(pen);
+ int x = static_cast<int>(m_currentPosition * m_dX + 0.5 * m_dX);
+ painter.drawLine(x, 0, x, m_maxY);
+ }
+ painter.setPen(QColor(32, 32, 32));
+ painter.drawLine(0, 0, m_maxX, 0);
+ painter.drawLine(0, m_maxY, m_maxX, m_maxY);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ QString labelWin = tr("Win");
+ QRect boundingRectWin = metrics.boundingRect(labelWin);
+ painter.drawText(QRect(0, 0, boundingRectWin.width(),
+ boundingRectWin.height()),
+ Qt::AlignLeft | Qt::AlignTop | Qt::TextDontClip,
+ labelWin);
+ QString labelLoss = tr("Loss");
+ QRect boundingRectLoss = metrics.boundingRect(labelLoss);
+ painter.drawText(QRect(0, m_maxY - boundingRectLoss.height(),
+ boundingRectLoss.width(), boundingRectLoss.height()),
+ Qt::AlignLeft | Qt::AlignBottom | Qt::TextDontClip,
+ labelLoss);
+ painter.setRenderHint(QPainter::Antialiasing, false);
+ painter.setPen(QColor(128, 128, 128));
+ painter.drawLine(0, m_maxY / 2, m_maxX, m_maxY / 2);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ for (unsigned i = 0; i < nu_moves; ++i)
+ {
+ double value = m_analyzeGame.get_value(i);
+ // Values can be outside [0..1] due to score/length bonuses
+ if (value < 0)
+ value = 0;
+ else if (value > 1)
+ value = 1;
+ auto color = Util::getPaintColor(m_analyzeGame.get_variant(),
+ m_analyzeGame.get_move(i).color);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(color);
+ painter.drawEllipse(QPointF((i + 0.5) * m_dX, (1 - value) * m_maxY),
+ 0.5 * m_dX, 0.5 * m_dX);
+ }
+}
+
+void AnalyzeGameWidget::resizeEvent(QResizeEvent*)
+{
+ if (! m_isInitialized)
+ return;
+ initSize();
+}
+
+void AnalyzeGameWidget::setCurrentPosition(const Game& game,
+ const SgfNode& node)
+{
+ update();
+ m_currentPosition = -1;
+ if (is_main_variation(node))
+ {
+ ArrayList<ColorMove,Board::max_game_moves> moves;
+ auto& tree = game.get_tree();
+ auto current = &find_root(node);
+ while (current)
+ {
+ auto mv = tree.get_move(*current);
+ if (! mv.is_null() && moves.size() < Board::max_game_moves)
+ moves.push_back(mv);
+ if (current == &node)
+ break;
+ current = current->get_first_child_or_null();
+ }
+ if (moves.size() <= m_analyzeGame.get_nu_moves())
+ {
+ for (unsigned i = 0; i < moves.size(); ++i)
+ if (moves[i] != m_analyzeGame.get_move(i))
+ return;
+ m_currentPosition = moves.size();
+ }
+ }
+}
+
+void AnalyzeGameWidget::showProgress(int progress)
+{
+ // m_progressDialog might already be closed if cancel was pressed and
+ // setValue makes it visible again (only with some Qt versions/platforms?)
+ if (m_progressDialog->isVisible())
+ m_progressDialog->setValue(progress);
+ // Repaint the window with the current status of the analysis
+ update();
+}
+
+QSize AnalyzeGameWidget::sizeHint() const
+{
+ auto geo = QApplication::desktop()->screenGeometry();
+ return QSize(geo.width() / 2, geo.height() / 3);
+}
+
+void AnalyzeGameWidget::start(const Game& game, Search& search,
+ size_t nuSimulations)
+{
+ m_isInitialized = true;
+ m_game = &game;
+ m_search = &search;
+ m_nuSimulations = nuSimulations;
+ initSize();
+ if (! m_progressDialog)
+ {
+ m_progressDialog = new QProgressDialog(this);
+ m_progressDialog->setWindowModality(Qt::WindowModal);
+ m_progressDialog->setWindowFlags(m_progressDialog->windowFlags()
+ & ~Qt::WindowContextHelpButtonHint);
+ m_progressDialog->setLabel(new QLabel(tr("Running game analysis..."),
+ this));
+ Util::setNoTitle(*m_progressDialog);
+ m_progressDialog->setMinimumDuration(0);
+ connect(m_progressDialog, SIGNAL(canceled()), SLOT(cancel()));
+ }
+ m_progressDialog->show();
+ m_isRunning = true;
+ m_future = QtConcurrent::run(this, &AnalyzeGameWidget::threadFunction);
+}
+
+void AnalyzeGameWidget::threadFunction()
+{
+ // This function and the progress callback are not called from the GUI
+ // thread. So we need to invoke showProgress() with invokeMethod().
+ auto progressCallback =
+ [&](unsigned movesAnalyzed, unsigned totalMoves)
+ {
+ if (totalMoves == 0)
+ return;
+ int progress = 100 * movesAnalyzed / totalMoves;
+ QMetaObject::invokeMethod(this, "showProgress",
+ Qt::BlockingQueuedConnection,
+ Q_ARG(int, progress));
+ };
+ m_analyzeGame.run(*m_game, *m_search, m_nuSimulations, progressCallback);
+ QMetaObject::invokeMethod(m_progressDialog, "hide", Qt::QueuedConnection);
+ m_isRunning = false;
+ emit finished();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeGameWidget.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_ANALYZE_GAME_WIDGET_H
+#define PENTOBI_ANALYZE_GAME_WIDGET_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <vector>
+#include <QFuture>
+#include <QWidget>
+#include "libpentobi_mcts/AnalyzeGame.h"
+
+class QProgressDialog;
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Game;
+using libpentobi_base::Variant;
+using libpentobi_mcts::AnalyzeGame;
+using libpentobi_mcts::Search;
+
+//-----------------------------------------------------------------------------
+
+class AnalyzeGameWidget
+ : public QWidget
+{
+ Q_OBJECT
+
+public slots:
+ /** Cancel a running analysis.
+ The function waits for the analysis to finish. The finished() signal
+ will still be invoked. */
+ void cancel();
+
+public:
+ explicit AnalyzeGameWidget(QWidget* parent);
+
+ /** Start an analysis.
+ This function will return after the analysis has started but the
+ window will be protected by a modal cancelable progress dialog.
+ Don't modify the game or use the search from a different thread until
+ the signal finished() was emitted. This will walk through every game
+ position in the main variation and use the search to evaluate
+ positions. During the analysis, the parent window is protected with a
+ modal progress dialog. */
+ void start(const Game& game, Search& search, size_t nuSimulations);
+
+ /** Mark the current position.
+ Will clear the current position if the target node is not in the
+ main variation or does not correspond to a move in the move
+ sequence when the analysis was done. */
+ void setCurrentPosition(const Game& game, const SgfNode& node);
+
+ QSize sizeHint() const override;
+
+signals:
+ /** Tells that the analysis has finished. */
+ void finished();
+
+ void gotoPosition(Variant variant, const vector<ColorMove>& moves);
+
+protected:
+ void mousePressEvent(QMouseEvent* event) override;
+
+ void paintEvent(QPaintEvent* event) override;
+
+ void resizeEvent(QResizeEvent* event) override;
+
+private slots:
+ void showProgress(int progress);
+
+private:
+ bool m_isInitialized;
+
+ bool m_isRunning;
+
+ const Game* m_game;
+
+ Search* m_search;
+
+ size_t m_nuSimulations;
+
+ AnalyzeGame m_analyzeGame;
+
+ QProgressDialog* m_progressDialog = nullptr;
+
+ QFuture<void> m_future;
+
+ int m_borderX;
+
+ int m_borderY;
+
+ qreal m_dX;
+
+ int m_maxX;
+
+ int m_maxY;
+
+ /** Current position that will be marked or -1 if no position is marked. */
+ int m_currentPosition;
+
+ void initSize();
+
+ void threadFunction();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANALYZE_GAME_WIDGET_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeGameWindow.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "AnalyzeGameWindow.h"
+
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+
+//-----------------------------------------------------------------------------
+
+AnalyzeGameWindow::AnalyzeGameWindow(QWidget* parent)
+ : QDialog(parent)
+{
+ setWindowTitle(tr("Game Analysis"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ auto layout = new QVBoxLayout;
+ setLayout(layout);
+ analyzeGameWidget = new AnalyzeGameWidget(this);
+ layout->addWidget(analyzeGameWidget);
+ auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
+ layout->addWidget(buttonBox);
+ connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
+ buttonBox->setFocus();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeGameWindow.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_ANALYZE_GAME_WINDOW_H
+#define PENTOBI_ANALYZE_GAME_WINDOW_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QDialog>
+#include "AnalyzeGameWidget.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class AnalyzeGameWindow final
+ : public QDialog
+{
+ Q_OBJECT
+
+public:
+ AnalyzeGameWidget* analyzeGameWidget;
+
+
+ explicit AnalyzeGameWindow(QWidget* parent);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANALYZE_GAME_WINDOW_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeSpeedDialog.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "AnalyzeSpeedDialog.h"
+
+//-----------------------------------------------------------------------------
+
+AnalyzeSpeedDialog::AnalyzeSpeedDialog(QWidget* parent, const QString& title)
+ : QInputDialog(parent)
+{
+ m_items << tr("Fast") << tr("Normal") << tr("Slow");
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ setWindowTitle(title);
+ setLabelText(tr("Analysis speed:"));
+ setInputMode(QInputDialog::TextInput);
+ setComboBoxItems(m_items);
+ setComboBoxEditable(false);
+}
+
+AnalyzeSpeedDialog::~AnalyzeSpeedDialog()
+{
+}
+
+void AnalyzeSpeedDialog::accept()
+{
+ m_speedValue = m_items.indexOf(textValue());
+ QDialog::accept();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeSpeedDialog.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_ANALYZE_SPEED_DIALOG_H
+#define PENTOBI_ANALYZE_SPEED_DIALOG_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QInputDialog>
+
+//-----------------------------------------------------------------------------
+
+class AnalyzeSpeedDialog final
+ : public QInputDialog
+{
+ Q_OBJECT
+
+public:
+ AnalyzeSpeedDialog(QWidget* parent, const QString& title);
+
+ ~AnalyzeSpeedDialog();
+
+ /** Get return value if dialog was accepted.
+ 0 = fast, 1 = normal, 2 = slow */
+ int getSpeedValue() { return m_speedValue; }
+
+public slots:
+ void accept() override;
+
+private:
+ int m_speedValue = 0;
+
+ QStringList m_items;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANALYZE_SPEED_DIALOG_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/Application.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Application.h"
+
+#include "ShowMessage.h"
+#include "libboardgame_sys/Compiler.h"
+
+using namespace std;
+using libboardgame_sys::get_type_name;
+
+//-----------------------------------------------------------------------------
+
+bool Application::notify(QObject* receiver, QEvent* event)
+{
+ try
+ {
+ return QApplication::notify(receiver, event);
+ }
+ catch (const exception& e)
+ {
+ string detailedText = get_type_name(e) + ": " + e.what();
+ showFatal(QString::fromLocal8Bit(detailedText.c_str()));
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/Application.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_APPLICATION_H
+#define PENTOBI_APPLICATION_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QApplication>
+
+//-----------------------------------------------------------------------------
+
+class Application
+ : public QApplication
+{
+ Q_OBJECT
+
+public:
+ using QApplication::QApplication;
+
+ /** Reimplemented from QApplication::notify().
+ Catches exceptions and shows an error message. */
+ bool notify(QObject* receiver, QEvent* event) override;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_APPLICATION_H
--- /dev/null
+set(CMAKE_AUTOMOC TRUE)
+
+set(pentobi_SRCS
+ AnalyzeGameWidget.h
+ AnalyzeGameWidget.cpp
+ AnalyzeGameWindow.h
+ AnalyzeGameWindow.cpp
+ AnalyzeSpeedDialog.h
+ AnalyzeSpeedDialog.cpp
+ Application.h
+ Application.cpp
+ Main.cpp
+ MainWindow.h
+ MainWindow.cpp
+ RatedGamesList.h
+ RatedGamesList.cpp
+ RatingDialog.h
+ RatingDialog.cpp
+ RatingGraph.h
+ RatingGraph.cpp
+ RatingHistory.h
+ RatingHistory.cpp
+ ShowMessage.h
+ ShowMessage.cpp
+ Util.h
+ Util.cpp
+ pentobi.rc
+)
+
+set(pentobi_ICNS
+ pentobi.png
+ pentobi-16.png
+ pentobi-32.png
+ pentobi-backward.png
+ pentobi-backward-16.png
+ pentobi-beginning.png
+ pentobi-beginning-16.png
+ pentobi-computer-colors.png
+ pentobi-computer-colors-16.png
+ pentobi-end.png
+ pentobi-end-16.png
+ pentobi-flip-horizontal.png
+ pentobi-flip-vertical.png
+ pentobi-forward.png
+ pentobi-forward-16.png
+ pentobi-newgame.png
+ pentobi-newgame-16.png
+ pentobi-next-piece.png
+ pentobi-next-variation.png
+ pentobi-next-variation-16.png
+ pentobi-piece-clear.png
+ pentobi-play.png
+ pentobi-play-16.png
+ pentobi-previous-piece.png
+ pentobi-previous-variation.png
+ pentobi-previous-variation-16.png
+ pentobi-rated-game.png
+ pentobi-rated-game-16.png
+ pentobi-rotate-left.png
+ pentobi-rotate-right.png
+ pentobi-undo.png
+ pentobi-undo-16.png
+ )
+
+set(pentobi_TS
+ translations/pentobi.ts
+ translations/pentobi_de.ts
+ )
+
+# Create PNG icons from SVG icons using the helper program src/convert
+file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/icons)
+file(COPY resources.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
+foreach(icon ${pentobi_ICNS})
+ string(REPLACE ".png" ".svg" svgicon ${icon})
+ add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}"
+ COMMAND convert ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+ ${CMAKE_CURRENT_BINARY_DIR}/icons/${icon}
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+ )
+endforeach()
+qt5_add_resources(pentobi_RC_SRCS ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc
+ OPTIONS -no-compress)
+file(COPY resources_2x.qrc DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
+foreach(icon ${pentobi_ICNS})
+string(REPLACE ".png" ".svg" svgicon ${icon})
+string(REPLACE ".png" "@2x.png" hdpiicon ${icon})
+add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}"
+ COMMAND convert --hdpi ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+ ${CMAKE_CURRENT_BINARY_DIR}/icons/${hdpiicon}
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/icons/${svgicon}
+)
+endforeach()
+qt5_add_resources(pentobi_RC_SRCS
+ ${CMAKE_CURRENT_BINARY_DIR}/resources_2x.qrc OPTIONS -no-compress)
+
+qt5_add_translation(pentobi_QM_SRCS ${pentobi_TS})
+
+add_executable(pentobi WIN32
+ ${pentobi_SRCS}
+ ${pentobi_QM_SRCS}
+ ${pentobi_RC_SRCS}
+ )
+
+
+if(MINGW AND (CMAKE_SIZEOF_VOID_P EQUAL "4"))
+ set_target_properties(pentobi PROPERTIES LINK_FLAGS -Wl,--large-address-aware)
+endif()
+
+target_link_libraries(pentobi
+ pentobi_gui
+ pentobi_mcts
+ pentobi_base
+ boardgame_base
+ boardgame_sgf
+ boardgame_util
+ boardgame_sys
+ )
+
+target_link_libraries(pentobi Qt5::Widgets Qt5::Concurrent)
+
+if(CMAKE_THREAD_LIBS_INIT)
+ target_link_libraries(pentobi ${CMAKE_THREAD_LIBS_INIT})
+endif()
+
+install(TARGETS pentobi DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+# Install translation files. If you change the destination, you need to
+# update the default for PENTOBI_TRANSLATIONS in the main CMakeLists.txt
+install(FILES ${pentobi_QM_SRCS}
+ DESTINATION ${CMAKE_INSTALL_DATADIR}/pentobi/translations)
+
+install(DIRECTORY help DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}
+ FILES_MATCHING PATTERN "*.css" PATTERN "*.html" PATTERN "*.png" PATTERN "*.jpg")
+
+if(MSVC)
+ configure_file(pentobi.conf.in Debug/pentobi.conf @ONLY)
+ configure_file(pentobi.conf.in Release/pentobi.conf @ONLY)
+else()
+ configure_file(pentobi.conf.in pentobi.conf @ONLY)
+endif()
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/Main.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QCommandLineParser>
+#include <QFileInfo>
+#include <QLibraryInfo>
+#include <QMessageBox>
+#include <QSettings>
+#include <QStyle>
+#include <QTranslator>
+#include "Application.h"
+#include "MainWindow.h"
+
+#ifdef Q_OS_WIN
+#include <stdio.h>
+#include <windows.h>
+#include <io.h>
+#include <fcntl.h>
+#endif
+
+using libboardgame_util::LogInitializer;
+using libboardgame_util::RandomGenerator;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void redirectStdErr()
+{
+#ifdef Q_OS_WIN
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ AllocConsole();
+ GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
+ info.dwSize.Y = 500;
+ SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), info.dwSize);
+ auto stdErrHandle = (intptr_t)GetStdHandle(STD_ERROR_HANDLE);
+ int conHandle = _open_osfhandle(stdErrHandle, _O_TEXT);
+ auto f = _fdopen(conHandle, "w");
+ *stderr = *f;
+ setvbuf(stderr, NULL, _IONBF, 0);
+ ios::sync_with_stdio();
+#endif
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+ LogInitializer log_initializer;
+ Q_INIT_RESOURCE(libpentobi_gui_resources);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
+ QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#endif
+ QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+ Application app(argc, argv);
+ app.setOrganizationName("Pentobi");
+ app.setApplicationName("Pentobi");
+ Q_INIT_RESOURCE(libpentobi_gui_resources_2x);
+ try
+ {
+ // For some reason, labels in the status bar have a border on
+ // Windows 7 with Qt 4.8. We don't want that.
+ app.setStyleSheet("QStatusBar::item { border: 0px solid black }");
+
+ // Allow the user to override installation paths with a config file in
+ // the directory of the executable to test it without installation
+ QString helpDir;
+ QString booksDir;
+ QString translationsPentobiDir;
+ QString translationsLibPentobiGuiDir;
+ QString appDir = app.applicationDirPath();
+#ifdef PENTOBI_HELP_DIR
+ helpDir = PENTOBI_HELP_DIR;
+#endif
+ if (helpDir.isEmpty())
+ helpDir = appDir + "/help";
+#ifdef PENTOBI_BOOKS_DIR
+ booksDir = PENTOBI_BOOKS_DIR;
+#endif
+ if (booksDir.isEmpty())
+ booksDir = appDir + "/books";
+#ifdef PENTOBI_TRANSLATIONS
+ translationsPentobiDir = PENTOBI_TRANSLATIONS;
+ translationsLibPentobiGuiDir = PENTOBI_TRANSLATIONS;
+#endif
+ if (translationsPentobiDir.isEmpty())
+ translationsPentobiDir = appDir + "/translations";
+ if (translationsLibPentobiGuiDir.isEmpty())
+ translationsLibPentobiGuiDir = appDir + "/translations";
+ QString overrideConfigFile = appDir + "/pentobi.conf";
+ if (QFileInfo::exists(overrideConfigFile))
+ {
+ QSettings settings(overrideConfigFile, QSettings::IniFormat);
+ helpDir = settings.value("HelpDir", helpDir).toString();
+ booksDir = settings.value("BooksDir", booksDir).toString();
+ translationsPentobiDir =
+ settings.value("TranslationsPentobiDir",
+ translationsPentobiDir).toString();
+ translationsLibPentobiGuiDir =
+ settings.value("TranslationsLibPentobiGuiDir",
+ translationsLibPentobiGuiDir).toString();
+ }
+
+ QTranslator qtTranslator;
+ QString qtTranslationPath =
+ QLibraryInfo::location(QLibraryInfo::TranslationsPath);
+ QString locale = QLocale::system().name();
+ qtTranslator.load("qt_" + locale, qtTranslationPath);
+ app.installTranslator(&qtTranslator);
+ QTranslator libPentobiGuiTranslator;
+ libPentobiGuiTranslator.load("libpentobi_gui_" + locale,
+ translationsLibPentobiGuiDir);
+ app.installTranslator(&libPentobiGuiTranslator);
+ QTranslator pentobiTranslator;
+ pentobiTranslator.load("pentobi_" + locale, translationsPentobiDir);
+ app.installTranslator(&pentobiTranslator);
+
+ QCommandLineParser parser;
+ auto maxSupportedLevel = Player::max_supported_level;
+ QCommandLineOption optionMaxLevel("maxlevel",
+ "Set maximum level to <n>.", "n",
+ QString::number(maxSupportedLevel));
+ parser.addOption(optionMaxLevel);
+ QCommandLineOption optionNoBook("nobook");
+ parser.addOption(optionNoBook);
+ QCommandLineOption optionNoDelay("nodelay");
+ parser.addOption(optionNoDelay);
+ QCommandLineOption optionSeed("seed", "Set random seed to <n>.", "n");
+ parser.addOption(optionSeed);
+ QCommandLineOption optionThreads("threads", "Use <n> threads.", "n");
+ parser.addOption(optionThreads);
+ QCommandLineOption optionVerbose("verbose");
+ parser.addOption(optionVerbose);
+ parser.process(app);
+ bool ok;
+ auto maxLevel = parser.value(optionMaxLevel).toUInt(&ok);
+ if (! ok || maxLevel < 1 || maxLevel > maxSupportedLevel)
+ throw runtime_error("--maxlevel must be between 1 and "
+ + to_string(maxSupportedLevel));
+ unsigned threads = 0;
+ if (parser.isSet(optionThreads))
+ {
+ threads = parser.value(optionThreads).toUInt(&ok);
+ if (! ok || threads == 0)
+ throw runtime_error("--threads must be greater zero.");
+ if (! libpentobi_mcts::SearchParamConst::multithread
+ && threads > 1)
+ throw runtime_error("This version of Pentobi was compiled"
+ " without support for multi-threading.");
+ }
+ if (! parser.isSet(optionVerbose))
+ libboardgame_util::disable_logging();
+ else
+ redirectStdErr();
+ if (parser.isSet(optionSeed))
+ {
+ RandomGenerator::ResultType seed =
+ parser.value(optionSeed).toUInt(&ok);
+ if (! ok)
+ throw runtime_error("--seed must be a positive number");
+ RandomGenerator::set_global_seed(seed);
+ }
+ bool noBook = parser.isSet(optionNoBook);
+ QString initialFile;
+ auto args = parser.positionalArguments();
+ if (! args.empty())
+ initialFile = args.at(0);
+ QSettings settings;
+ auto variantString = settings.value("variant", "").toString();
+ Variant variant;
+ if (! parse_variant_id(variantString.toLocal8Bit().constData(),
+ variant))
+ variant = Variant::duo;
+ try
+ {
+ MainWindow mainWindow(variant, initialFile, helpDir, maxLevel,
+ booksDir, noBook, threads);
+ if (parser.isSet(optionNoDelay))
+ mainWindow.setNoDelay();
+ mainWindow.show();
+ return app.exec();
+ }
+ catch (bad_alloc&)
+ {
+ // bad_alloc is an expected error because libpentobi_mcts::Player
+ // requires a larger amount of memory and an error message should
+ // be shown to the user. It needs to be handled here because it
+ // needs the translators being installed for the error message.
+ QMessageBox::critical(
+ nullptr, app.translate("main", "Pentobi"),
+ app.translate("main", "Not enough memory."));
+ }
+ }
+ catch (const exception& e)
+ {
+ cerr << "Error: " << e.what() << '\n';
+ return 1;
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/MainWindow.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "MainWindow.h"
+
+#include <algorithm>
+#include <fstream>
+#include <QAction>
+#include <QApplication>
+#include <QDir>
+#include <QDesktopWidget>
+#include <QFileDialog>
+#include <QImageWriter>
+#include <QInputDialog>
+#include <QLabel>
+#include <QMenu>
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QSettings>
+#include <QSplitter>
+#include <QStandardPaths>
+#include <QStatusBar>
+#include <QToolBar>
+#include <QToolButton>
+#include <QtConcurrentRun>
+#include "AnalyzeGameWindow.h"
+#include "AnalyzeSpeedDialog.h"
+#include "RatingDialog.h"
+#include "ShowMessage.h"
+#include "Util.h"
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_util/Assert.h"
+#include "libpentobi_base/TreeUtil.h"
+#include "libpentobi_base/PentobiTreeWriter.h"
+#include "libpentobi_gui/ComputerColorDialog.h"
+#include "libpentobi_gui/GameInfoDialog.h"
+#include "libpentobi_gui/GuiBoard.h"
+#include "libpentobi_gui/GuiBoardUtil.h"
+#include "libpentobi_gui/HelpWindow.h"
+#include "libpentobi_gui/InitialRatingDialog.h"
+#include "libpentobi_gui/LeaveFullscreenButton.h"
+#include "libpentobi_gui/OrientationDisplay.h"
+#include "libpentobi_gui/PieceSelector.h"
+#include "libpentobi_gui/SameHeightLayout.h"
+#include "libpentobi_gui/ScoreDisplay.h"
+#include "libpentobi_gui/Util.h"
+
+using Util::getPlayerString;
+using libboardgame_sgf::InvalidTree;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::util::back_to_main_variation;
+using libboardgame_sgf::util::beginning_of_branch;
+using libboardgame_sgf::util::find_next_comment;
+using libboardgame_sgf::util::get_last_node;
+using libboardgame_sgf::util::get_move_annotation;
+using libboardgame_sgf::util::get_variation_string;
+using libboardgame_sgf::util::has_comment;
+using libboardgame_sgf::util::has_earlier_variation;
+using libboardgame_sgf::util::is_main_variation;
+using libboardgame_util::clear_abort;
+using libboardgame_util::get_abort;
+using libboardgame_util::set_abort;
+using libboardgame_util::trim_right;
+using libboardgame_util::ArrayList;
+using libpentobi_base::BoardType;
+using libpentobi_base::MoveInfo;
+using libpentobi_base::MoveInfoExt;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::PieceSet;
+using libpentobi_base::PentobiTree;
+using libpentobi_base::PentobiTreeWriter;
+using libpentobi_base::ScoreType;
+using libpentobi_base::tree_util::get_move_number;
+using libpentobi_base::tree_util::get_moves_left;
+using libpentobi_base::tree_util::get_position_info;
+using libpentobi_mcts::Search;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** Create a button for manipulating piece orientation. */
+QToolButton* createOBoxToolButton(QAction* action)
+{
+ auto button = new QToolButton;
+ button->setDefaultAction(action);
+ button->setAutoRaise(true);
+ // No focus, there are faster keyboard shortcuts for manipulating pieces
+ button->setFocusPolicy(Qt::NoFocus);
+ // For some reason, toolbuttons are very small in Ubuntu Unity if outside
+ // a toolbar (tested with Ubuntu 15.10)
+ button->setMinimumSize(28, 28);
+ return button;
+}
+
+/** Return auto-save file name as a native path name. */
+QString getAutoSaveFile()
+{
+ return Util::getDataDir() + QDir::separator() + "autosave.blksgf";
+}
+
+bool hasCurrentVariationOtherMoves(const PentobiTree& tree,
+ const SgfNode& current)
+{
+ auto node = current.get_parent_or_null();
+ while (node)
+ {
+ if (! tree.get_move(*node).is_null())
+ return true;
+ node = node->get_parent_or_null();
+ }
+ node = current.get_first_child_or_null();
+ while (node)
+ {
+ if (! tree.get_move(*node).is_null())
+ return true;
+ node = node->get_first_child_or_null();
+ }
+ return false;
+}
+
+void initToolBarText(QToolBar* toolBar)
+{
+ QSettings settings;
+ auto toolBarText = settings.value("toolbar_text", "system").toString();
+ if (toolBarText == "no_text")
+ toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
+ else if (toolBarText == "beside_icons")
+ toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ else if (toolBarText == "below_icons")
+ toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
+ else if (toolBarText == "text_only")
+ toolBar->setToolButtonStyle(Qt::ToolButtonTextOnly);
+ else
+ toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
+}
+
+void setIcon(QAction* action, const QString& name)
+{
+ QIcon icon(QString(":/pentobi/icons/%1.png").arg(name));
+ QString file16 = QString(":/pentobi/icons/%1-16.png").arg(name);
+ if (QFile::exists(file16))
+ icon.addFile(file16, QSize(16, 16));
+ action->setIcon(icon);
+}
+
+/** Simple heuristic that prefers moves with more score points.
+ Used for sorting the list used in Find Move. */
+ScoreType getHeuristic(const Board& bd, Move mv)
+{
+ return bd.get_piece_info(bd.get_move_piece(mv)).get_score_points();
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+MainWindow::MainWindow(Variant variant, const QString& initialFile,
+ const QString& helpDir, unsigned maxLevel,
+ const QString& booksDir, bool noBook,
+ unsigned nuThreads)
+ : m_game(variant),
+ m_bd(m_game.get_board()),
+ m_helpDir(helpDir)
+{
+ if (maxLevel > Player::max_supported_level)
+ maxLevel = Player::max_supported_level;
+ if (maxLevel < 1)
+ maxLevel = 1;
+ m_maxLevel = maxLevel;
+ Util::initDataDir();
+ QSettings settings;
+ m_history.reset(new RatingHistory(variant));
+ createActions();
+ restoreLevel(variant);
+ setCentralWidget(createCentralWidget());
+ initPieceSelectors();
+ m_moveNumber = new QLabel;
+ statusBar()->addPermanentWidget(m_moveNumber);
+ m_setupModeLabel = new QLabel(tr("Setup mode"));
+ statusBar()->addWidget(m_setupModeLabel);
+ m_setupModeLabel->hide();
+ m_ratedGameLabelText = new QLabel(tr("Rated game"));
+ statusBar()->addWidget(m_ratedGameLabelText);
+ m_ratedGameLabelText->hide();
+ initGame();
+ m_player.reset(new Player(variant, maxLevel,
+ booksDir.toLocal8Bit().constData(), nuThreads));
+ m_player->get_search().set_callback(bind(&MainWindow::searchCallback,
+ this, placeholders::_1,
+ placeholders::_2));
+ m_player->set_use_book(! noBook);
+ createToolBar();
+ connect(&m_genMoveWatcher, SIGNAL(finished()),
+ SLOT(genMoveFinished()));
+ connect(m_guiBoard, SIGNAL(play(Color, Move)),
+ SLOT(placePiece(Color, Move)));
+ connect(m_guiBoard, SIGNAL(pointClicked(Point)),
+ SLOT(pointClicked(Point)));
+ connect(m_actionMovePieceLeft, SIGNAL(triggered()),
+ m_guiBoard, SLOT(movePieceLeft()));
+ connect(m_actionMovePieceRight, SIGNAL(triggered()),
+ m_guiBoard, SLOT(movePieceRight()));
+ connect(m_actionMovePieceUp, SIGNAL(triggered()),
+ m_guiBoard, SLOT(movePieceUp()));
+ connect(m_actionMovePieceDown, SIGNAL(triggered()),
+ m_guiBoard, SLOT(movePieceDown()));
+ connect(m_actionPlacePiece, SIGNAL(triggered()),
+ m_guiBoard, SLOT(placePiece()));
+ createMenu();
+ qApp->installEventFilter(this);
+ updateRecentFiles();
+ auto marking = settings.value("move_marking", "last_dot").toString();
+ if (marking == "all_number")
+ m_actionMoveMarkingAllNumber->setChecked(true);
+ else if (marking == "last_dot")
+ m_actionMoveMarkingLastDot->setChecked(true);
+ else if (marking == "last_number")
+ m_actionMoveMarkingLastNumber->setChecked(true);
+ else
+ m_actionMoveMarkingNone->setChecked(true);
+ auto coordinates = settings.value("coordinates", false).toBool();
+ m_guiBoard->setCoordinates(coordinates);
+ m_actionCoordinates->setChecked(coordinates);
+ auto showToolbar = settings.value("toolbar", true).toBool();
+ findChild<QToolBar*>()->setVisible(showToolbar);
+ m_menuToolBarText->setEnabled(showToolbar);
+ m_actionShowToolbar->setChecked(showToolbar);
+ auto showVariations = settings.value("show_variations", true).toBool();
+ m_actionShowVariations->setChecked(showVariations);
+ initVariantActions();
+ QIcon icon;
+ icon.addFile(":/pentobi/icons/pentobi.png");
+ icon.addFile(":/pentobi/icons/pentobi-16.png");
+ icon.addFile(":/pentobi/icons/pentobi-32.png");
+ setWindowIcon(icon);
+
+ bool centerOnScreen = false;
+ QRect screenGeometry = QApplication::desktop()->screenGeometry();
+ if (restoreGeometry(settings.value("geometry").toByteArray()))
+ {
+ if (! screenGeometry.contains(geometry()))
+ {
+ if (width() > screenGeometry.width()
+ || height() > screenGeometry.height())
+ adjustSize();
+ centerOnScreen = true;
+ }
+ }
+ else
+ {
+ adjustSize();
+ centerOnScreen = true;
+ }
+ if (centerOnScreen)
+ {
+ int x = (screenGeometry.width() - width()) / 2;
+ int y = (screenGeometry.height() - height()) / 2;
+ move(x, y);
+ }
+
+ auto showComment = settings.value("show_comment", false).toBool();
+ m_comment->setVisible(showComment);
+ if (showComment)
+ m_splitter->restoreState(
+ settings.value("splitter_state").toByteArray());
+ m_actionShowComment->setChecked(showComment);
+ updateWindow(true);
+ clearFile();
+ if (! initialFile.isEmpty())
+ {
+ if (open(initialFile))
+ rememberFile(initialFile);
+ }
+ else
+ {
+ QString autoSaveFile = getAutoSaveFile();
+ if (QFile(autoSaveFile).exists())
+ {
+ open(autoSaveFile, true);
+ m_isAutoSaveLoaded = true;
+ deleteAutoSaveFile();
+ m_gameFinished = m_bd.is_game_over();
+ updateWindow(true);
+ if (settings.value("autosave_rated", false).toBool())
+ QMetaObject::invokeMethod(this, "continueRatedGame",
+ Qt::QueuedConnection);
+ }
+ }
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::about()
+{
+ QMessageBox::about(this, tr("About Pentobi"),
+ "<style type=\"text/css\">"
+ ":link { text-decoration: none; }"
+ "</style>"
+ "<h2>" + tr("Pentobi") + "</h2>"
+ "<p>" + tr("Version %1").arg(getVersion()) + "</p>"
+ "<p>" +
+ tr("Computer opponent for the board game Blokus.")
+ + "<br>" +
+ tr("© 2011–%1 Markus Enzenberger").arg(2017) +
+ + "<br>" +
+ "<a href=\"http://pentobi.sourceforge.net\">http://pentobi.sourceforge.net</a>"
+ "</p>");
+}
+
+void MainWindow::analyzeGame()
+{
+ if (! is_main_variation(m_game.get_current()))
+ {
+ showInfo(tr("Game analysis is only possible in the main variation."));
+ return;
+ }
+ AnalyzeSpeedDialog dialog(this, tr("Analyze Game"));
+ if (! dialog.exec())
+ return;
+ int speed = dialog.getSpeedValue();
+ cancelThread();
+ if (m_analyzeGameWindow)
+ delete m_analyzeGameWindow;
+ m_analyzeGameWindow = new AnalyzeGameWindow(this);
+ // Make sure all action shortcuts work when the analyze dialog has the
+ // focus apart from m_actionLeaveFullscreen because the Esc key is used
+ // to close the dialog
+ m_analyzeGameWindow->addActions(actions());
+ m_analyzeGameWindow->removeAction(m_actionLeaveFullscreen);
+ m_analyzeGameWindow->show();
+ m_isAnalyzeRunning = true;
+ connect(m_analyzeGameWindow->analyzeGameWidget, SIGNAL(finished()),
+ SLOT(analyzeGameFinished()));
+ connect(m_analyzeGameWindow->analyzeGameWidget,
+ SIGNAL(gotoPosition(Variant,const vector<ColorMove>&)),
+ SLOT(gotoPosition(Variant,const vector<ColorMove>&)));
+ size_t nuSimulations;
+ switch (speed)
+ {
+ case 0:
+ nuSimulations = 6000;
+ break;
+ case 1:
+ nuSimulations = 24000;
+ break;
+ default:
+ nuSimulations = 96000;
+ }
+ m_analyzeGameWindow->analyzeGameWidget->start(
+ m_game, m_player->get_search(), nuSimulations);
+}
+
+void MainWindow::analyzeGameFinished()
+{
+ m_analyzeGameWindow->analyzeGameWidget->setCurrentPosition(
+ m_game, m_game.get_current());
+ m_isAnalyzeRunning = false;
+}
+
+/** Call to Player::genmove() that runs in a different thread. */
+MainWindow::GenMoveResult MainWindow::asyncGenMove(Color c, int genMoveId,
+ bool playSingleMove)
+{
+ QElapsedTimer timer;
+ timer.start();
+ GenMoveResult result;
+ result.playSingleMove = playSingleMove;
+ result.color = c;
+ result.genMoveId = genMoveId;
+ result.move = m_player->genmove(m_bd, c);
+ auto elapsed = timer.elapsed();
+ // Enforce minimum thinking time of 0.8 sec
+ if (elapsed < 800 && ! m_noDelay)
+ QThread::msleep(800 - elapsed);
+ return result;
+}
+
+void MainWindow::badMove(bool checked)
+{
+ if (! checked)
+ return;
+ m_game.set_bad_move();
+ updateWindow(false);
+}
+
+void MainWindow::backward()
+{
+ gotoNode(m_game.get_current().get_parent_or_null());
+}
+
+void MainWindow::backToMainVariation()
+{
+ gotoNode(back_to_main_variation(m_game.get_current()));
+}
+
+void MainWindow::beginning()
+{
+ gotoNode(m_game.get_root());
+}
+
+void MainWindow::beginningOfBranch()
+{
+ gotoNode(beginning_of_branch(m_game.get_current()));
+}
+
+void MainWindow::cancelThread()
+{
+ if (m_isAnalyzeRunning)
+ {
+ // This should never happen because AnalyzeGameWindow protects the
+ // parent with a modal progress dialog while it is running. However,
+ // due to bugs in Unity 2D (tested with Ubuntu 11.04 and 11.10), the
+ // global menu can still trigger menu item events.
+ m_analyzeGameWindow->analyzeGameWidget->cancel();
+ }
+ if (! m_isGenMoveRunning)
+ return;
+ // After waitForFinished() returns, we can be sure that the move generation
+ // is no longer running, but we will still receive the finished event.
+ // Increasing m_genMoveId will make genMoveFinished() ignore the event.
+ ++m_genMoveId;
+ set_abort();
+ m_genMoveWatcher.waitForFinished();
+ m_isGenMoveRunning = false;
+ clearStatus();
+ setCursor(QCursor(Qt::ArrowCursor));
+ m_actionInterrupt->setEnabled(false);
+ m_actionNextPiece->setEnabled(true);
+ m_actionPlay->setEnabled(true);
+ m_actionPlaySingleMove->setEnabled(true);
+ m_actionPreviousPiece->setEnabled(true);
+}
+
+void MainWindow::checkComputerMove()
+{
+ if (m_autoPlay && isComputerToPlay() && ! m_bd.is_game_over()
+ && ! m_isGenMoveRunning)
+ genMove();
+}
+
+bool MainWindow::checkSave()
+{
+ if (! m_file.isEmpty())
+ {
+ if (! m_game.is_modified())
+ return true;
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("The file has been modified."),
+ tr("Do you want to save your changes?"));
+ // Don't use QMessageBox::Discard because on some platforms it uses the
+ // text "Close without saving" which implies that the window would be
+ // closed
+ auto discardButton =
+ msgBox.addButton(tr("&Don't Save"), QMessageBox::DestructiveRole);
+ auto saveButton = msgBox.addButton(QMessageBox::Save);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ auto result = msgBox.clickedButton();
+ if (result == saveButton)
+ {
+ save();
+ return true;
+ }
+ return result == discardButton;
+ }
+ // Don't ask if game should be saved if it was finished because the user
+ // might only want to play and never save games.
+ if (m_game.get_root().has_children() && ! m_gameFinished)
+ {
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("The current game is not finished."),
+ tr("Do you want to abort the game?"));
+ auto abortGameButton =
+ msgBox.addButton(tr("&Abort Game"), QMessageBox::DestructiveRole);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() != abortGameButton)
+ return false;
+ return true;
+ }
+ return true;
+}
+
+bool MainWindow::checkQuit()
+{
+ if (! m_file.isEmpty() && m_game.is_modified())
+ {
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("The file has been modified."),
+ tr("Do you want to save your changes?"));
+ auto discardButton = msgBox.addButton(QMessageBox::Discard);
+ auto saveButton = msgBox.addButton(QMessageBox::Save);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ auto result = msgBox.clickedButton();
+ if (result == saveButton)
+ {
+ save();
+ return true;
+ }
+ return result == discardButton;
+ }
+ cancelThread();
+ QSettings settings;
+ if (m_file.isEmpty() && ! m_gameFinished
+ && (m_game.is_modified() || m_isAutoSaveLoaded))
+ {
+ writeGame(getAutoSaveFile().toLocal8Bit().constData());
+ settings.setValue("autosave_rated", m_isRated);
+ if (m_isRated)
+ settings.setValue("autosave_rated_color",
+ m_ratedGameColor.to_int());
+ }
+ if (! isFullScreen())
+ settings.setValue("geometry", saveGeometry());
+ if (m_comment->isVisible())
+ settings.setValue("splitter_state", m_splitter->saveState());
+ return true;
+}
+
+void MainWindow::clearFile()
+{
+ setFile("");
+}
+
+void MainWindow::clearPiece()
+{
+ m_actionRotateClockwise->setEnabled(false);
+ m_actionRotateAnticlockwise->setEnabled(false);
+ m_actionFlipHorizontally->setEnabled(false);
+ m_actionFlipVertically->setEnabled(false);
+ m_actionClearPiece->setEnabled(false);
+ m_guiBoard->clearPiece();
+ m_orientationDisplay->clearPiece();
+}
+
+void MainWindow::clearStatus()
+{
+ statusBar()->clearMessage();
+}
+
+void MainWindow::closeEvent(QCloseEvent* event)
+{
+ if (checkQuit())
+ event->accept();
+ else
+ event->ignore();
+}
+
+void MainWindow::commentChanged()
+{
+ if (m_ignoreCommentTextChanged)
+ return;
+ QString comment = m_comment->toPlainText();
+ if (comment.isEmpty())
+ m_game.set_comment("");
+ else
+ {
+ string charset = m_game.get_root().get_property("CA", "");
+ string value = Util::convertSgfValueFromQString(comment, charset);
+ value = trim_right(value);
+ m_game.set_comment(value);
+ }
+ updateWindow(false);
+}
+
+void MainWindow::computerColors()
+{
+ ColorMap<bool> oldComputerColors = m_computerColors;
+ ComputerColorDialog dialog(this, m_bd.get_variant(), m_computerColors);
+ dialog.exec();
+ auto nu_colors = m_bd.get_nu_nonalt_colors();
+
+ bool computerNone = true;
+ for (Color c : Color::Range(nu_colors))
+ if (m_computerColors[c])
+ {
+ computerNone = false;
+ break;
+ }
+ QSettings settings;
+ settings.setValue("computer_color_none", computerNone);
+
+ // Enable autoplay only if any color has changed because that means that
+ // the user probably wants to continue playing, otherwise the user could
+ // have only opened the dialog to check the current settings
+ for (Color c : Color::Range(nu_colors))
+ if (m_computerColors[c] != oldComputerColors[c])
+ {
+ m_autoPlay = true;
+ break;
+ }
+
+ checkComputerMove();
+}
+
+bool MainWindow::computerPlaysAll() const
+{
+ for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
+ if (! m_computerColors[c])
+ return false;
+ return true;
+}
+
+void MainWindow::continueRatedGame()
+{
+ auto nuColors = m_bd.get_nu_colors();
+ QSettings settings;
+ auto color =
+ static_cast<Color::IntType>(
+ settings.value("autosave_rated_color", 0).toUInt());
+ if (color >= nuColors)
+ return;
+ m_ratedGameColor = Color(color);
+ m_computerColors.fill(true);
+ for (Color c : Color::Range(nuColors))
+ if (m_bd.is_same_player(c, m_ratedGameColor))
+ m_computerColors[c] = false;
+ setRated(true);
+ updateWindow(false);
+ showInfo(tr("Continuing unfinished rated game."),
+ tr("You play %1 in this game.")
+ .arg(getPlayerString(m_bd.get_variant(), m_ratedGameColor)));
+ m_autoPlay = true;
+ checkComputerMove();
+}
+
+void MainWindow::coordinates(bool checked)
+{
+ m_guiBoard->setCoordinates(checked);
+ QSettings settings;
+ settings.setValue("coordinates", checked);
+}
+
+QAction* MainWindow::createAction(const QString& text)
+{
+ auto action = new QAction(text, this);
+ // Add all actions also to main window. if an action is only added to
+ // the menu bar, shortcuts won't work in fullscreen mode because the menu
+ // is not visible in fullscreen mode
+ addAction(action);
+ return action;
+}
+
+QAction* MainWindow::createActionLevel(unsigned level, const QString& text)
+{
+ auto action = createAction(text);
+ action->setCheckable(true);
+ action->setActionGroup(m_actionGroupLevel);
+ action->setData(level);
+ connect(action, SIGNAL(triggered(bool)), SLOT(levelTriggered(bool)));
+ return action;
+}
+
+void MainWindow::createActions()
+{
+ m_actionGroupVariant = new QActionGroup(this);
+ m_actionGroupLevel = new QActionGroup(this);
+ auto groupMoveMarking = new QActionGroup(this);
+ auto groupMoveAnnotation = new QActionGroup(this);
+ auto groupToolBarText = new QActionGroup(this);
+
+ m_actionAbout = createAction(tr("&About Pentobi"));
+ connect(m_actionAbout, SIGNAL(triggered()), SLOT(about()));
+
+ m_actionAnalyzeGame = createAction(tr("&Analyze Game..."));
+ connect(m_actionAnalyzeGame, SIGNAL(triggered()), SLOT(analyzeGame()));
+
+ m_actionBackward = createAction(tr("B&ackward"));
+ m_actionBackward->setToolTip(tr("Go one move backward"));
+ m_actionBackward->setPriority(QAction::LowPriority);
+ setIcon(m_actionBackward, "pentobi-backward");
+ m_actionBackward->setShortcut(QKeySequence::MoveToPreviousWord);
+ connect(m_actionBackward, SIGNAL(triggered()), SLOT(backward()));
+
+ m_actionBackToMainVariation = createAction(tr("Back to &Main Variation"));
+ m_actionBackToMainVariation->setShortcut(QString("Ctrl+M"));
+ connect(m_actionBackToMainVariation, SIGNAL(triggered()),
+ SLOT(backToMainVariation()));
+
+ m_actionBadMove = createAction(tr("&Bad"));
+ m_actionBadMove->setActionGroup(groupMoveAnnotation);
+ m_actionBadMove->setCheckable(true);
+ connect(m_actionBadMove, SIGNAL(triggered(bool)), SLOT(badMove(bool)));
+
+ m_actionBeginning = createAction(tr("&Beginning"));
+ m_actionBeginning->setToolTip(tr("Go to beginning of game"));
+ m_actionBeginning->setPriority(QAction::LowPriority);
+ setIcon(m_actionBeginning, "pentobi-beginning");
+ m_actionBeginning->setShortcut(QKeySequence::MoveToStartOfDocument);
+ connect(m_actionBeginning, SIGNAL(triggered()), SLOT(beginning()));
+
+ m_actionBeginningOfBranch = createAction(tr("Beginning of Bran&ch"));
+ m_actionBeginningOfBranch->setShortcut(QString("Ctrl+B"));
+ connect(m_actionBeginningOfBranch, SIGNAL(triggered()),
+ SLOT(beginningOfBranch()));
+
+ m_actionClearPiece = createAction(tr("Clear Piece"));
+ setIcon(m_actionClearPiece, "pentobi-piece-clear");
+ m_actionClearPiece->setShortcut(QString("0"));
+ connect(m_actionClearPiece, SIGNAL(triggered()), SLOT(clearPiece()));
+
+ m_actionComputerColors = createAction(tr("&Computer Colors"));
+ m_actionComputerColors->setShortcut(QString("Ctrl+U"));
+ m_actionComputerColors->setToolTip(
+ tr("Set the colors played by the computer"));
+ setIcon(m_actionComputerColors, "pentobi-computer-colors");
+ connect(m_actionComputerColors, SIGNAL(triggered()),
+ SLOT(computerColors()));
+
+ m_actionCoordinates = createAction(tr("C&oordinates"));
+ m_actionCoordinates->setCheckable(true);
+ connect(m_actionCoordinates, SIGNAL(triggered(bool)),
+ SLOT(coordinates(bool)));
+
+ m_actionDeleteAllVariations = createAction(tr("&Delete All Variations"));
+ connect(m_actionDeleteAllVariations, SIGNAL(triggered()),
+ SLOT(deleteAllVariations()));
+
+ m_actionDoubtfulMove = createAction(tr("&Doubtful"));
+ m_actionDoubtfulMove->setActionGroup(groupMoveAnnotation);
+ m_actionDoubtfulMove->setCheckable(true);
+ connect(m_actionDoubtfulMove, SIGNAL(triggered(bool)),
+ SLOT(doubtfulMove(bool)));
+
+ m_actionEnd = createAction(tr("&End"));
+ m_actionEnd->setToolTip(tr("Go to end of moves"));
+ m_actionEnd->setPriority(QAction::LowPriority);
+ m_actionEnd->setShortcut(QKeySequence::MoveToEndOfDocument);
+ setIcon(m_actionEnd, "pentobi-end");
+ connect(m_actionEnd, SIGNAL(triggered()), SLOT(end()));
+
+ m_actionExportAsciiArt = createAction(tr("&ASCII Art"));
+ connect(m_actionExportAsciiArt, SIGNAL(triggered()),
+ SLOT(exportAsciiArt()));
+
+ m_actionExportImage = createAction(tr("I&mage"));
+ connect(m_actionExportImage, SIGNAL(triggered()), SLOT(exportImage()));
+
+ m_actionFindMove = createAction(tr("&Find Move"));
+ m_actionFindMove->setShortcut(QString("F6"));
+ connect(m_actionFindMove, SIGNAL(triggered()), SLOT(findMove()));
+
+ m_actionFindNextComment = createAction(tr("Find Next &Comment"));
+ m_actionFindNextComment->setShortcut(QString("F3"));
+ connect(m_actionFindNextComment, SIGNAL(triggered()),
+ SLOT(findNextComment()));
+
+ m_actionFlipHorizontally = createAction(tr("Flip Horizontally"));
+ setIcon(m_actionFlipHorizontally, "pentobi-flip-horizontal");
+ connect(m_actionFlipHorizontally, SIGNAL(triggered()),
+ SLOT(flipHorizontally()));
+
+ m_actionFlipVertically = createAction(tr("Flip Vertically"));
+ setIcon(m_actionFlipVertically, "pentobi-flip-vertical");
+
+ m_actionForward = createAction(tr("&Forward"));
+ m_actionForward->setToolTip(tr("Go one move forward"));
+ m_actionForward->setPriority(QAction::LowPriority);
+ m_actionForward->setShortcut(QKeySequence::MoveToNextWord);
+ setIcon(m_actionForward, "pentobi-forward");
+ connect(m_actionForward, SIGNAL(triggered()), SLOT(forward()));
+
+ m_actionFullscreen = createAction(tr("&Fullscreen"));
+ // Don't use QKeySequence::Fullscreen, it is Ctrl-F11 on Linux but that
+ // doesn't work in Xfce
+ m_actionFullscreen->setShortcut(QString("F11"));
+ connect(m_actionFullscreen, SIGNAL(triggered()), SLOT(fullscreen()));
+
+ m_actionGameInfo = createAction(tr("Ga&me Info"));
+ m_actionGameInfo->setShortcut(QString("Ctrl+I"));
+ connect(m_actionGameInfo, SIGNAL(triggered()), SLOT(gameInfo()));
+
+ m_actionGoodMove = createAction(tr("&Good"));
+ m_actionGoodMove->setActionGroup(groupMoveAnnotation);
+ m_actionGoodMove->setCheckable(true);
+ connect(m_actionGoodMove, SIGNAL(triggered(bool)), SLOT(goodMove(bool)));
+
+ m_actionGotoMove = createAction(tr("&Go to Move..."));
+ m_actionGotoMove->setShortcut(QString("Ctrl+G"));
+ connect(m_actionGotoMove, SIGNAL(triggered()), SLOT(gotoMove()));
+
+ m_actionHelp = createAction(tr("Pentobi &Help"));
+ m_actionHelp->setShortcut(QKeySequence::HelpContents);
+ connect(m_actionHelp, SIGNAL(triggered()), SLOT(help()));
+
+ m_actionInterestingMove = createAction(tr("I&nteresting"));
+ m_actionInterestingMove->setActionGroup(groupMoveAnnotation);
+ m_actionInterestingMove->setCheckable(true);
+ connect(m_actionInterestingMove, SIGNAL(triggered(bool)),
+ SLOT(interestingMove(bool)));
+
+ m_actionInterrupt = createAction(tr("St&op"));
+ m_actionInterrupt->setEnabled(false);
+ connect(m_actionInterrupt, SIGNAL(triggered()), SLOT(interrupt()));
+
+ m_actionInterruptPlay = createAction();
+ m_actionInterruptPlay->setShortcut(QString("Shift+Esc"));
+ connect(m_actionInterruptPlay, SIGNAL(triggered()), SLOT(interruptPlay()));
+
+ m_actionKeepOnlyPosition = createAction(tr("&Keep Only Position"));
+ connect(m_actionKeepOnlyPosition, SIGNAL(triggered()),
+ SLOT(keepOnlyPosition()));
+
+ m_actionKeepOnlySubtree = createAction(tr("Keep Only &Subtree"));
+ connect(m_actionKeepOnlySubtree, SIGNAL(triggered()),
+ SLOT(keepOnlySubtree()));
+
+ m_actionLeaveFullscreen = createAction(tr("Leave Fullscreen"));
+ m_actionLeaveFullscreen->setShortcut(QString("Esc"));
+ connect(m_actionLeaveFullscreen, SIGNAL(triggered()),
+ SLOT(leaveFullscreen()));
+
+ m_actionMakeMainVariation = createAction(tr("M&ake Main Variation"));
+ connect(m_actionMakeMainVariation, SIGNAL(triggered()),
+ SLOT(makeMainVariation()));
+
+ m_actionMoveDownVariation = createAction(tr("Move Variation D&own"));
+ connect(m_actionMoveDownVariation, SIGNAL(triggered()),
+ SLOT(moveDownVariation()));
+
+ m_actionMoveUpVariation = createAction(tr("Move Variation &Up"));
+ connect(m_actionMoveUpVariation, SIGNAL(triggered()),
+ SLOT(moveUpVariation()));
+
+ static_assert(Player::max_supported_level <= 9, "");
+ QString levelText[Player::max_supported_level] =
+ { tr("&1"), tr("&2"), tr("&3"), tr("&4"), tr("&5"), tr("&6"),
+ tr("&7"), tr("&8"), tr("&9") };
+ for (unsigned i = 0; i < m_maxLevel; ++i)
+ createActionLevel(i + 1, levelText[i]);
+ connect(m_actionFlipVertically, SIGNAL(triggered()),
+ SLOT(flipVertically()));
+
+ m_actionMoveMarkingAllNumber = createAction(tr("&All with Number"));
+ m_actionMoveMarkingAllNumber->setActionGroup(groupMoveMarking);
+ m_actionMoveMarkingAllNumber->setCheckable(true);
+ connect(m_actionMoveMarkingAllNumber, SIGNAL(triggered(bool)),
+ SLOT(setMoveMarkingAllNumber(bool)));
+
+ m_actionMoveMarkingLastDot = createAction(tr("Last with &Dot"));
+ m_actionMoveMarkingLastDot->setActionGroup(groupMoveMarking);
+ m_actionMoveMarkingLastDot->setCheckable(true);
+ m_actionMoveMarkingLastDot->setChecked(true);
+ connect(m_actionMoveMarkingLastDot, SIGNAL(triggered(bool)),
+ SLOT(setMoveMarkingLastDot(bool)));
+
+ m_actionMoveMarkingLastNumber = createAction(tr("&Last with Number"));
+ m_actionMoveMarkingLastNumber->setActionGroup(groupMoveMarking);
+ m_actionMoveMarkingLastNumber->setCheckable(true);
+ m_actionMoveMarkingLastNumber->setChecked(true);
+ connect(m_actionMoveMarkingLastNumber, SIGNAL(triggered(bool)),
+ SLOT(setMoveMarkingLastNumber(bool)));
+
+ m_actionMoveMarkingNone = createAction(tr("&None", "move numbers"));
+ m_actionMoveMarkingNone->setActionGroup(groupMoveMarking);
+ m_actionMoveMarkingNone->setCheckable(true);
+ connect(m_actionMoveMarkingNone, SIGNAL(triggered(bool)),
+ SLOT(setMoveMarkingNone(bool)));
+
+ m_actionMovePieceLeft = createAction();
+ m_actionMovePieceLeft->setShortcut(QKeySequence::MoveToPreviousChar);
+
+ m_actionMovePieceRight = createAction();
+ m_actionMovePieceRight->setShortcut(QKeySequence::MoveToNextChar);
+
+ m_actionMovePieceUp = createAction();
+ m_actionMovePieceUp->setShortcut(QKeySequence::MoveToPreviousLine);
+
+ m_actionMovePieceDown = createAction();
+ m_actionMovePieceDown->setShortcut(QKeySequence::MoveToNextLine);
+
+ m_actionNextPiece = createAction(tr("Next Piece"));
+ setIcon(m_actionNextPiece, "pentobi-next-piece");
+ m_actionNextPiece->setShortcut(QString("+"));
+ connect(m_actionNextPiece, SIGNAL(triggered()), SLOT(nextPiece()));
+
+ m_actionNextTransform = createAction();
+ m_actionNextTransform->setShortcut(QString("Space"));
+ connect(m_actionNextTransform, SIGNAL(triggered()), SLOT(nextTransform()));
+
+ m_actionNextVariation = createAction(tr("&Next Variation"));
+ m_actionNextVariation->setToolTip(tr("Go to next variation"));
+ m_actionNextVariation->setPriority(QAction::LowPriority);
+ m_actionNextVariation->setShortcut(QKeySequence::MoveToNextPage);
+ setIcon(m_actionNextVariation, "pentobi-next-variation");
+ connect(m_actionNextVariation, SIGNAL(triggered()), SLOT(nextVariation()));
+
+ m_actionNew = createAction(tr("&New"));
+ m_actionNew->setShortcut(QKeySequence::New);
+ m_actionNew->setToolTip(tr("Start a new game"));
+ setIcon(m_actionNew, "pentobi-newgame");
+ connect(m_actionNew, SIGNAL(triggered()), SLOT(newGame()));
+
+ m_actionNoMoveAnnotation = createAction(tr("N&one", "move annotation"));
+ m_actionNoMoveAnnotation->setActionGroup(groupMoveAnnotation);
+ m_actionNoMoveAnnotation->setCheckable(true);
+ connect(m_actionNoMoveAnnotation, SIGNAL(triggered(bool)),
+ SLOT(noMoveAnnotation(bool)));
+
+ m_actionOpen = createAction(tr("&Open..."));
+ m_actionOpen->setShortcut(QKeySequence::Open);
+ connect(m_actionOpen, SIGNAL(triggered()), SLOT(open()));
+ m_actionPlacePiece = createAction();
+ m_actionPlacePiece->setShortcut(QString("Return"));
+
+ m_actionPlay = createAction(tr("&Play"));
+ m_actionPlay->setShortcut(QString("Ctrl+L"));
+ setIcon(m_actionPlay, "pentobi-play");
+ connect(m_actionPlay, SIGNAL(triggered()), SLOT(play()));
+
+ m_actionPlaySingleMove = createAction(tr("Play &Single Move"));
+ m_actionPlaySingleMove->setShortcut(QString("Ctrl+Shift+L"));
+ connect(m_actionPlaySingleMove, SIGNAL(triggered()),
+ SLOT(playSingleMove()));
+
+ m_actionPreviousPiece = createAction(tr("Previous Piece"));
+ setIcon(m_actionPreviousPiece, "pentobi-previous-piece");
+ m_actionPreviousPiece->setShortcut(QString("-"));
+ connect(m_actionPreviousPiece, SIGNAL(triggered()),
+ SLOT(previousPiece()));
+
+ m_actionPreviousTransform = createAction();
+ m_actionPreviousTransform->setShortcut(QString("Shift+Space"));
+ connect(m_actionPreviousTransform, SIGNAL(triggered()),
+ SLOT(previousTransform()));
+
+ m_actionPreviousVariation = createAction(tr("&Previous Variation"));
+ m_actionPreviousVariation->setToolTip(tr("Go to previous variation"));
+ m_actionPreviousVariation->setPriority(QAction::LowPriority);
+ m_actionPreviousVariation->setShortcut(QKeySequence::MoveToPreviousPage);
+ setIcon(m_actionPreviousVariation, "pentobi-previous-variation");
+ connect(m_actionPreviousVariation, SIGNAL(triggered()),
+ SLOT(previousVariation()));
+
+ m_actionRatedGame = createAction(tr("&Rated Game"));
+ m_actionRatedGame->setToolTip(tr("Start a rated game"));
+ m_actionRatedGame->setShortcut(QString("Ctrl+Shift+N"));
+ setIcon(m_actionRatedGame, "pentobi-rated-game");
+ connect(m_actionRatedGame, SIGNAL(triggered()), SLOT(ratedGame()));
+
+ for (auto& action : m_actionRecentFile)
+ {
+ action = createAction();
+ action->setVisible(false);
+ connect(action, SIGNAL(triggered()), SLOT(openRecentFile()));
+ }
+
+ m_actionRotateAnticlockwise = createAction(tr("Rotate Anticlockwise"));
+ setIcon(m_actionRotateAnticlockwise, "pentobi-rotate-left");
+ connect(m_actionRotateAnticlockwise, SIGNAL(triggered()),
+ SLOT(rotateAnticlockwise()));
+
+ m_actionRotateClockwise = createAction(tr("Rotate Clockwise"));
+ setIcon(m_actionRotateClockwise, "pentobi-rotate-right");
+ connect(m_actionRotateClockwise, SIGNAL(triggered()),
+ SLOT(rotateClockwise()));
+
+ m_actionQuit = createAction(tr("&Quit"));
+ m_actionQuit->setShortcut(QKeySequence::Quit);
+ connect(m_actionQuit, SIGNAL(triggered()), SLOT(close()));
+
+ m_actionSave = createAction(tr("&Save"));
+ m_actionSave->setShortcut(QKeySequence::Save);
+ connect(m_actionSave, SIGNAL(triggered()), SLOT(save()));
+
+ m_actionSaveAs = createAction(tr("Save &As..."));
+ m_actionSaveAs->setShortcut(QKeySequence::SaveAs);
+ connect(m_actionSaveAs, SIGNAL(triggered()), SLOT(saveAs()));
+
+ m_actionNextColor = createAction(tr("Next &Color"));
+ connect(m_actionNextColor, SIGNAL(triggered()), SLOT(nextColor()));
+
+ for (auto name : { "1", "2", "A", "C", "E", "F", "G", "H", "I", "J", "L",
+ "N", "O", "P", "S", "T", "U", "V", "W", "X", "Y", "Z" })
+ {
+ auto action = createAction();
+ action->setData(name);
+ action->setShortcut(QString(name));
+ connect(action, SIGNAL(triggered()), SLOT(selectNamedPiece()));
+ }
+
+ m_actionSetupMode = createAction(tr("S&etup Mode"));
+ m_actionSetupMode->setCheckable(true);
+ connect(m_actionSetupMode, SIGNAL(triggered(bool)), SLOT(setupMode(bool)));
+
+ m_actionShowComment = createAction(tr("&Comment"));
+ m_actionShowComment->setCheckable(true);
+ m_actionShowComment->setShortcut(QString("Ctrl+T"));
+ connect(m_actionShowComment, SIGNAL(triggered(bool)),
+ SLOT(showComment(bool)));
+
+ m_actionRating = createAction(tr("&Rating"));
+ m_actionRating->setShortcut(QString("F7"));
+ connect(m_actionRating, SIGNAL(triggered()), SLOT(showRating()));
+
+ m_actionToolBarNoText = createAction(tr("&No Text"));
+ m_actionToolBarNoText->setActionGroup(groupToolBarText);
+ m_actionToolBarNoText->setCheckable(true);
+ connect(m_actionToolBarNoText, SIGNAL(triggered(bool)),
+ SLOT(toolBarNoText(bool)));
+
+ m_actionToolBarTextBesideIcons = createAction(tr("Text &Beside Icons"));
+ m_actionToolBarTextBesideIcons->setActionGroup(groupToolBarText);
+ m_actionToolBarTextBesideIcons->setCheckable(true);
+ connect(m_actionToolBarTextBesideIcons, SIGNAL(triggered(bool)),
+ SLOT(toolBarTextBesideIcons(bool)));
+
+ m_actionToolBarTextBelowIcons = createAction(tr("Text Bel&ow Icons"));
+ m_actionToolBarTextBelowIcons->setActionGroup(groupToolBarText);
+ m_actionToolBarTextBelowIcons->setCheckable(true);
+ connect(m_actionToolBarTextBelowIcons, SIGNAL(triggered(bool)),
+ SLOT(toolBarTextBelowIcons(bool)));
+
+ m_actionToolBarTextOnly = createAction(tr("&Text Only"));
+ m_actionToolBarTextOnly->setActionGroup(groupToolBarText);
+ m_actionToolBarTextOnly->setCheckable(true);
+ connect(m_actionToolBarTextOnly, SIGNAL(triggered(bool)),
+ SLOT(toolBarTextOnly(bool)));
+
+ m_actionToolBarTextSystem = createAction(tr("&System Default"));
+ m_actionToolBarTextSystem->setActionGroup(groupToolBarText);
+ m_actionToolBarTextSystem->setCheckable(true);
+ connect(m_actionToolBarTextSystem, SIGNAL(triggered(bool)),
+ SLOT(toolBarTextSystem(bool)));
+
+ m_actionTruncate = createAction(tr("&Truncate"));
+ connect(m_actionTruncate, SIGNAL(triggered()), SLOT(truncate()));
+
+ m_actionTruncateChildren = createAction(tr("Truncate C&hildren"));
+ connect(m_actionTruncateChildren, SIGNAL(triggered()),
+ SLOT(truncateChildren()));
+
+ m_actionShowToolbar = createAction(tr("&Toolbar"));
+ m_actionShowToolbar->setCheckable(true);
+ connect(m_actionShowToolbar, SIGNAL(triggered(bool)),
+ SLOT(showToolbar(bool)));
+
+ m_actionShowVariations = createAction(tr("Show &Variations"));
+ m_actionShowVariations->setCheckable(true);
+ connect(m_actionShowVariations, SIGNAL(triggered(bool)),
+ SLOT(showVariations(bool)));
+
+ m_actionUndo = createAction(tr("&Undo Move"));
+ setIcon(m_actionUndo, "pentobi-undo");
+ connect(m_actionUndo, SIGNAL(triggered()), SLOT(undo()));
+
+ m_actionVariantCallisto2 =
+ createActionVariant(Variant::callisto_2,
+ tr("Callisto (&2 Players)"));
+ m_actionVariantCallisto3 =
+ createActionVariant(Variant::callisto_3,
+ tr("Callisto (&3 Players)"));
+ m_actionVariantCallisto =
+ createActionVariant(Variant::callisto,
+ tr("Callisto (&4 Players)"));
+ m_actionVariantClassic2 =
+ createActionVariant(Variant::classic_2,
+ tr("Classic (&2 Players)"));
+ m_actionVariantClassic3 =
+ createActionVariant(Variant::classic_3,
+ tr("Classic (&3 Players)"));
+ m_actionVariantClassic =
+ createActionVariant(Variant::classic, tr("Classic (&4 Players)"));
+ m_actionVariantDuo = createActionVariant(Variant::duo, tr("&Duo"));
+ m_actionVariantJunior =
+ createActionVariant(Variant::junior, tr("J&unior"));
+ m_actionVariantTrigon2 =
+ createActionVariant(Variant::trigon_2, tr("Trigon (&2 Players)"));
+ m_actionVariantTrigon3 =
+ createActionVariant(Variant::trigon_3, tr("Trigon (&3 Players)"));
+ m_actionVariantTrigon =
+ createActionVariant(Variant::trigon, tr("Trigon (&4 Players)"));
+ m_actionVariantNexos2 =
+ createActionVariant(Variant::nexos_2, tr("Nexos (&2 Players)"));
+ m_actionVariantNexos =
+ createActionVariant(Variant::nexos, tr("Nexos (&4 Players)"));
+
+ m_actionVeryBadMove = createAction(tr("V&ery Bad"));
+ m_actionVeryBadMove->setActionGroup(groupMoveAnnotation);
+ m_actionVeryBadMove->setCheckable(true);
+ connect(m_actionVeryBadMove, SIGNAL(triggered(bool)),
+ SLOT(veryBadMove(bool)));
+
+ m_actionVeryGoodMove = createAction(tr("&Very Good"));
+ m_actionVeryGoodMove->setActionGroup(groupMoveAnnotation);
+ m_actionVeryGoodMove->setCheckable(true);
+ connect(m_actionVeryGoodMove, SIGNAL(triggered(bool)),
+ SLOT(veryGoodMove(bool)));
+}
+
+QAction* MainWindow::createActionVariant(Variant variant, const QString& text)
+{
+ auto action = createAction(text);
+ action->setCheckable(true);
+ action->setActionGroup(m_actionGroupVariant);
+ action->setData(static_cast<int>(variant));
+ connect(action, SIGNAL(triggered(bool)), SLOT(variantTriggered(bool)));
+ return action;
+}
+
+QWidget* MainWindow::createCentralWidget()
+{
+ auto widget = new QWidget;
+ // We add spacing around and between the two panels using streches (such
+ // that the spacing grows with the window size)
+ auto outerLayout = new QVBoxLayout;
+ widget->setLayout(outerLayout);
+ auto innerLayout = new QHBoxLayout;
+ outerLayout->addStretch(1);
+ outerLayout->addLayout(innerLayout, 100);
+ outerLayout->addStretch(1);
+ innerLayout->addStretch(1);
+ innerLayout->addWidget(createLeftPanel(), 60);
+ innerLayout->addStretch(1);
+ innerLayout->addLayout(createRightPanel(), 40);
+ innerLayout->addStretch(1);
+ // The central widget doesn't do anything with the focus right now, but we
+ // allow it to receive the focus such that the user can switch away the
+ // focus from the comment field and its blinking cursor.
+ widget->setFocusPolicy(Qt::StrongFocus);
+ return widget;
+}
+
+QWidget* MainWindow::createLeftPanel()
+{
+ m_splitter = new QSplitter(Qt::Vertical);
+ m_guiBoard = new GuiBoard(nullptr, m_bd);
+ m_splitter->addWidget(m_guiBoard);
+ m_comment = new QPlainTextEdit;
+ m_comment->setTabChangesFocus(true);
+ connect(m_comment, SIGNAL(textChanged()), SLOT(commentChanged()));
+ m_splitter->addWidget(m_comment);
+ m_splitter->setStretchFactor(0, 85);
+ m_splitter->setStretchFactor(1, 15);
+ m_splitter->setCollapsible(0, false);
+ m_splitter->setCollapsible(1, false);
+ return m_splitter;
+}
+
+void MainWindow::createMenu()
+{
+ auto menuGame = menuBar()->addMenu(tr("&Game"));
+ menuGame->addAction(m_actionNew);
+ menuGame->addAction(m_actionRatedGame);
+ menuGame->addSeparator();
+ m_menuVariant = menuGame->addMenu(tr("Game &Variant"));
+ auto menuClassic = m_menuVariant->addMenu(tr("&Classic"));
+ menuClassic->addAction(m_actionVariantClassic2);
+ menuClassic->addAction(m_actionVariantClassic3);
+ menuClassic->addAction(m_actionVariantClassic);
+ m_menuVariant->addAction(m_actionVariantDuo);
+ m_menuVariant->addAction(m_actionVariantJunior);
+ auto menuTrigon = m_menuVariant->addMenu(tr("&Trigon"));
+ menuTrigon->addAction(m_actionVariantTrigon2);
+ menuTrigon->addAction(m_actionVariantTrigon3);
+ menuTrigon->addAction(m_actionVariantTrigon);
+ auto menuNexos = m_menuVariant->addMenu(tr("&Nexos"));
+ menuNexos->addAction(m_actionVariantNexos2);
+ menuNexos->addAction(m_actionVariantNexos);
+ auto menuCallisto = m_menuVariant->addMenu(tr("C&allisto"));
+ menuCallisto->addAction(m_actionVariantCallisto2);
+ menuCallisto->addAction(m_actionVariantCallisto3);
+ menuCallisto->addAction(m_actionVariantCallisto);
+ menuGame->addAction(m_actionGameInfo);
+ menuGame->addSeparator();
+ menuGame->addAction(m_actionUndo);
+ menuGame->addAction(m_actionFindMove);
+ menuGame->addSeparator();
+ menuGame->addAction(m_actionOpen);
+ m_menuOpenRecent = menuGame->addMenu(tr("Open R&ecent"));
+ for (auto& action : m_actionRecentFile)
+ m_menuOpenRecent->addAction(action);
+ menuGame->addSeparator();
+ menuGame->addAction(m_actionSave);
+ menuGame->addAction(m_actionSaveAs);
+ m_menuExport = menuGame->addMenu(tr("E&xport"));
+ m_menuExport->addAction(m_actionExportImage);
+ m_menuExport->addAction(m_actionExportAsciiArt);
+ menuGame->addSeparator();
+ menuGame->addAction(m_actionQuit);
+
+ auto menuGo = menuBar()->addMenu(tr("G&o"));
+ menuGo->addAction(m_actionBeginning);
+ menuGo->addAction(m_actionBackward);
+ menuGo->addAction(m_actionForward);
+ menuGo->addAction(m_actionEnd);
+ menuGo->addSeparator();
+ menuGo->addAction(m_actionNextVariation);
+ menuGo->addAction(m_actionPreviousVariation);
+ menuGo->addSeparator();
+ menuGo->addAction(m_actionGotoMove);
+ menuGo->addAction(m_actionBackToMainVariation);
+ menuGo->addAction(m_actionBeginningOfBranch);
+ menuGo->addSeparator();
+ menuGo->addAction(m_actionFindNextComment);
+
+ auto menuEdit = menuBar()->addMenu(tr("&Edit"));
+ m_menuMoveAnnotation = menuEdit->addMenu(tr("&Move Annotation"));
+ m_menuMoveAnnotation->addAction(m_actionNoMoveAnnotation);
+ m_menuMoveAnnotation->addAction(m_actionVeryGoodMove);
+ m_menuMoveAnnotation->addAction(m_actionGoodMove);
+ m_menuMoveAnnotation->addAction(m_actionInterestingMove);
+ m_menuMoveAnnotation->addAction(m_actionDoubtfulMove);
+ m_menuMoveAnnotation->addAction(m_actionBadMove);
+ m_menuMoveAnnotation->addAction(m_actionVeryBadMove);
+ menuEdit->addSeparator();
+ menuEdit->addAction(m_actionMakeMainVariation);
+ menuEdit->addAction(m_actionMoveUpVariation);
+ menuEdit->addAction(m_actionMoveDownVariation);
+ menuEdit->addSeparator();
+ menuEdit->addAction(m_actionDeleteAllVariations);
+ menuEdit->addAction(m_actionTruncate);
+ menuEdit->addAction(m_actionTruncateChildren);
+ menuEdit->addAction(m_actionKeepOnlyPosition);
+ menuEdit->addAction(m_actionKeepOnlySubtree);
+ menuEdit->addSeparator();
+ menuEdit->addAction(m_actionSetupMode);
+ menuEdit->addAction(m_actionNextColor);
+
+ auto menuView = menuBar()->addMenu(tr("&View"));
+ menuView->addAction(m_actionShowToolbar);
+ m_menuToolBarText = menuView->addMenu(tr("Toolbar T&ext"));
+ m_menuToolBarText->addAction(m_actionToolBarNoText);
+ m_menuToolBarText->addAction(m_actionToolBarTextBesideIcons);
+ m_menuToolBarText->addAction(m_actionToolBarTextBelowIcons);
+ m_menuToolBarText->addAction(m_actionToolBarTextOnly);
+ m_menuToolBarText->addAction(m_actionToolBarTextSystem);
+ menuView->addAction(m_actionShowComment);
+ menuView->addSeparator();
+ auto menuMoveNumbers = menuView->addMenu(tr("&Move Marking"));
+ menuMoveNumbers->addAction(m_actionMoveMarkingLastDot);
+ menuMoveNumbers->addAction(m_actionMoveMarkingLastNumber);
+ menuMoveNumbers->addAction(m_actionMoveMarkingAllNumber);
+ menuMoveNumbers->addAction(m_actionMoveMarkingNone);
+ menuView->addAction(m_actionCoordinates);
+ menuView->addAction(m_actionShowVariations);
+ menuView->addSeparator();
+ menuView->addAction(m_actionFullscreen);
+
+ auto menuComputer = menuBar()->addMenu(tr("&Computer"));
+ menuComputer->addAction(m_actionComputerColors);
+ menuComputer->addAction(m_actionPlay);
+ menuComputer->addSeparator();
+ menuComputer->addAction(m_actionPlaySingleMove);
+ menuComputer->addAction(m_actionInterrupt);
+ menuComputer->addSeparator();
+ m_menuLevel = menuComputer->addMenu(QString());
+ m_menuLevel->addActions(m_actionGroupLevel->actions());
+
+ auto menuTools = menuBar()->addMenu(tr("&Tools"));
+ menuTools->addAction(m_actionRating);
+ menuTools->addAction(m_actionAnalyzeGame);
+
+ auto menuHelp = menuBar()->addMenu(tr("&Help"));
+ menuHelp->addAction(m_actionHelp);
+ menuHelp->addAction(m_actionAbout);
+}
+
+QLayout* MainWindow::createOrientationButtonBoxLeft()
+{
+ auto outerLayout = new QVBoxLayout;
+ auto layout = new QGridLayout;
+ layout->addWidget(createOBoxToolButton(m_actionRotateAnticlockwise), 0, 0);
+ layout->addWidget(createOBoxToolButton(m_actionRotateClockwise), 0, 1);
+ layout->addWidget(createOBoxToolButton(m_actionFlipHorizontally), 1, 0);
+ layout->addWidget(createOBoxToolButton(m_actionFlipVertically), 1, 1);
+ outerLayout->addStretch();
+ outerLayout->addLayout(layout);
+ outerLayout->addStretch();
+ return outerLayout;
+}
+
+QLayout* MainWindow::createOrientationButtonBoxRight()
+{
+ auto outerLayout = new QVBoxLayout;
+ auto layout = new QGridLayout;
+ layout->addWidget(createOBoxToolButton(m_actionPreviousPiece), 0, 0);
+ layout->addWidget(createOBoxToolButton(m_actionNextPiece), 0, 1);
+ layout->addWidget(createOBoxToolButton(m_actionClearPiece), 1, 0,
+ 1, 2, Qt::AlignHCenter);
+ outerLayout->addStretch();
+ outerLayout->addLayout(layout);
+ outerLayout->addStretch();
+ return outerLayout;
+}
+
+QLayout* MainWindow::createOrientationSelector()
+{
+ auto layout = new QHBoxLayout;
+ layout->addStretch();
+ layout->addLayout(createOrientationButtonBoxLeft());
+ layout->addSpacing(8);
+ m_orientationDisplay = new OrientationDisplay(nullptr, m_bd);
+ connect(m_orientationDisplay, SIGNAL(colorClicked(Color)),
+ SLOT(orientationDisplayColorClicked(Color)));
+ m_orientationDisplay->setSizePolicy(QSizePolicy::MinimumExpanding,
+ QSizePolicy::MinimumExpanding);
+ layout->addWidget(m_orientationDisplay);
+ layout->addSpacing(8);
+ layout->addLayout(createOrientationButtonBoxRight());
+ layout->addStretch();
+ return layout;
+}
+
+QLayout* MainWindow::createRightPanel()
+{
+ auto layout = new QVBoxLayout;
+ layout->addLayout(createOrientationSelector(), 20);
+ m_scoreDisplay = new ScoreDisplay;
+ layout->addWidget(m_scoreDisplay, 5);
+ auto pieceSelectorLayout = new SameHeightLayout;
+ layout->addLayout(pieceSelectorLayout, 80);
+ for (Color c : Color::Range(Color::range))
+ {
+ m_pieceSelector[c] = new PieceSelector(nullptr, m_bd, c);
+ connect(m_pieceSelector[c],
+ SIGNAL(pieceSelected(Color,Piece,const Transform*)),
+ SLOT(selectPiece(Color,Piece,const Transform*)));
+ pieceSelectorLayout->addWidget(m_pieceSelector[c]);
+ }
+ return layout;
+}
+
+void MainWindow::deleteAllVariations()
+{
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("Delete all variations?"),
+ tr("All variations but the main variation will be"
+ " removed from the game tree."));
+ auto deleteButton =
+ msgBox.addButton(tr("Delete Variations"), QMessageBox::DestructiveRole);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() != deleteButton)
+ return;
+ bool currentNodeChanges = ! is_main_variation(m_game.get_current());
+ if (currentNodeChanges)
+ cancelThread();
+ m_game.delete_all_variations();
+ updateWindow(currentNodeChanges);
+}
+
+void MainWindow::doubtfulMove(bool checked)
+{
+ if (! checked)
+ return;
+ m_game.set_doubtful_move();
+ updateWindow(false);
+}
+
+void MainWindow::createToolBar()
+{
+ auto toolBar = new QToolBar;
+ toolBar->setMovable(false);
+ toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
+ toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
+ toolBar->addAction(m_actionNew);
+ toolBar->addAction(m_actionRatedGame);
+ toolBar->addAction(m_actionUndo);
+ toolBar->addSeparator();
+ toolBar->addAction(m_actionComputerColors);
+ toolBar->addAction(m_actionPlay);
+ toolBar->addSeparator();
+ toolBar->addAction(m_actionBeginning);
+ toolBar->addAction(m_actionBackward);
+ toolBar->addAction(m_actionForward);
+ toolBar->addAction(m_actionEnd);
+ toolBar->addSeparator();
+ toolBar->addAction(m_actionNextVariation);
+ toolBar->addAction(m_actionPreviousVariation);
+ // Is this the right way to enable autorepeat buttons? Using
+ // QAction::autoRepeat applies only to keyboard and adding a QToolButton
+ // with QToolBar::addWidget() makes the tool button not respect the
+ // toolButtonStyle.
+ for (auto button : toolBar->findChildren<QToolButton*>())
+ {
+ auto action = button->defaultAction();
+ if (action == m_actionBackward || action == m_actionForward)
+ button->setAutoRepeat(true);
+ }
+ addToolBar(toolBar);
+ initToolBarText(toolBar);
+ QSettings settings;
+ auto toolBarText = settings.value("toolbar_text", "system").toString();
+ if (toolBarText == "no_text")
+ m_actionToolBarNoText->setChecked(true);
+ else if (toolBarText == "beside_icons")
+ m_actionToolBarTextBesideIcons->setChecked(true);
+ else if (toolBarText == "below_icons")
+ m_actionToolBarTextBelowIcons->setChecked(true);
+ else if (toolBarText == "text_only")
+ m_actionToolBarTextOnly->setChecked(true);
+ else
+ m_actionToolBarTextSystem->setChecked(true);
+}
+
+void MainWindow::deleteAutoSaveFile()
+{
+ QString autoSaveFile = getAutoSaveFile();
+ QFile file(autoSaveFile);
+ if (file.exists() && ! file.remove())
+ showError(tr("Could not delete %1").arg(autoSaveFile));
+}
+
+void MainWindow::enablePieceSelector(Color c)
+{
+ for (Color i : m_bd.get_colors())
+ {
+ m_pieceSelector[i]->checkUpdate();
+ m_pieceSelector[i]->setEnabled(i == c);
+ }
+}
+
+void MainWindow::end()
+{
+ gotoNode(get_last_node(m_game.get_current()));
+}
+
+bool MainWindow::eventFilter(QObject* object, QEvent* event)
+{
+ // Empty status tips can clear the status bar if the mouse goes over a
+ // menu. We don't want that because it deletes our "computer is thinking"
+ // message. This still happens with Qt 5.6 on some platforms.
+ if (event->type() == QEvent::StatusTip)
+ return true;
+ return QMainWindow::eventFilter(object, event);
+}
+
+void MainWindow::exportAsciiArt()
+{
+ QString file = QFileDialog::getSaveFileName(this, "", getLastDir(),
+ tr("Text files (*.txt);;All files (*)"));
+ if (file.isEmpty())
+ return;
+ rememberDir(file);
+ ofstream out(file.toLocal8Bit().constData());
+ m_bd.write(out, false);
+ if (! out)
+ showError(QString::fromLocal8Bit(strerror(errno)));
+}
+
+void MainWindow::exportImage()
+{
+ QSettings settings;
+ auto size = settings.value("export_image_size", 420).toInt();
+ QInputDialog dialog(this);
+ dialog.setWindowFlags(dialog.windowFlags()
+ & ~Qt::WindowContextHelpButtonHint);
+ dialog.setWindowTitle(tr("Export Image"));
+ dialog.setLabelText(tr("Image size:"));
+ dialog.setInputMode(QInputDialog::IntInput);
+ dialog.setIntRange(0, 2147483647);
+ dialog.setIntStep(40);
+ dialog.setIntValue(size);
+ if (! dialog.exec())
+ return;
+ size = dialog.intValue();
+ settings.setValue("export_image_size", size);
+ bool coordinates = m_actionCoordinates->isChecked();
+ BoardPainter boardPainter;
+ boardPainter.setCoordinates(coordinates);
+ boardPainter.setCoordinateColor(QColor(100, 100, 100));
+ QImage image(size, size, QImage::Format_ARGB32);
+ image.fill(Qt::transparent);
+ QPainter painter;
+ painter.begin(&image);
+ if (coordinates)
+ painter.fillRect(0, 0, size, size, QColor(216, 216, 216));
+ boardPainter.paintEmptyBoard(painter, size, size, m_bd.get_variant(),
+ m_bd.get_geometry());
+ Grid<unsigned> pieceId;
+ if (m_bd.get_board_type() == BoardType::nexos)
+ {
+ pieceId.fill(0, m_bd.get_geometry());
+ unsigned n = 0;
+ for (Color c : m_bd.get_colors())
+ for (Move mv : m_bd.get_setup().placements[c])
+ {
+ ++n;
+ for (Point p : m_bd.get_move_points(mv))
+ pieceId[p] = n;
+ }
+ for (auto mv : m_bd.get_moves())
+ {
+ ++n;
+ for (Point p : m_bd.get_move_points(mv.move))
+ pieceId[p] = n;
+ }
+ }
+ boardPainter.paintPieces(painter, m_bd.get_point_state(), pieceId,
+ &m_guiBoard->getLabels());
+ painter.end();
+ QString file;
+ while (true)
+ {
+ file = QFileDialog::getSaveFileName(this, file, getLastDir());
+ if (file.isEmpty())
+ break;
+ rememberDir(file);
+ QImageWriter writer(file);
+ if (writer.write(image))
+ break;
+ else
+ showError(writer.errorString());
+ }
+}
+
+void MainWindow::findMove()
+{
+ if (m_bd.is_game_over())
+ return;
+ if (! m_legalMoves)
+ m_legalMoves.reset(new MoveList);
+ Color to_play = m_bd.get_to_play();
+ if (m_legalMoves->empty())
+ {
+ if (! m_marker)
+ m_marker.reset(new MoveMarker);
+ m_bd.gen_moves(to_play, *m_marker, *m_legalMoves);
+ m_marker->clear(*m_legalMoves);
+ sort(m_legalMoves->begin(), m_legalMoves->end(),
+ [&](Move mv1, Move mv2) {
+ return getHeuristic(m_bd, mv1) > getHeuristic(m_bd, mv2);
+ });
+ }
+ if (m_legalMoves->empty())
+ return;
+ if (m_legalMoveIndex >= m_legalMoves->size())
+ m_legalMoveIndex = 0;
+ auto mv = (*m_legalMoves)[m_legalMoveIndex];
+ selectPiece(to_play, m_bd.get_move_piece(mv));
+ m_guiBoard->showMove(to_play, mv);
+ ++m_legalMoveIndex;
+}
+
+void MainWindow::findNextComment()
+{
+ auto& root = m_game.get_root();
+ auto& current = m_game.get_current();
+ auto node = find_next_comment(current);
+ if (! node && ¤t != &root)
+ {
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("The end of the tree was reached."),
+ tr("Continue the search from the start of the tree?"));
+ auto continueButton =
+ msgBox.addButton(tr("Continue From Start"),
+ QMessageBox::AcceptRole);
+ msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(continueButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() == continueButton)
+ {
+ node = &root;
+ if (! has_comment(*node))
+ node = find_next_comment(*node);
+ }
+ else
+ return;
+ }
+ if (! node)
+ {
+ showInfo(tr("No comment found"));
+ return;
+ }
+ showComment(true);
+ gotoNode(*node);
+}
+
+void MainWindow::flipHorizontally()
+{
+ Piece piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ return;
+ auto transform = m_guiBoard->getSelectedPieceTransform();
+ transform = m_bd.get_transforms().get_mirrored_horizontally(transform);
+ transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
+ m_guiBoard->setSelectedPieceTransform(transform);
+ m_orientationDisplay->setSelectedPieceTransform(transform);
+}
+
+void MainWindow::flipVertically()
+{
+ Piece piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ return;
+ auto transform = m_guiBoard->getSelectedPieceTransform();
+ transform = m_bd.get_transforms().get_mirrored_vertically(transform);
+ transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
+ m_guiBoard->setSelectedPieceTransform(transform);
+ m_orientationDisplay->setSelectedPieceTransform(transform);
+}
+
+void MainWindow::forward()
+{
+ gotoNode(m_game.get_current().get_first_child_or_null());
+}
+
+void MainWindow::fullscreen()
+{
+ if (isFullScreen())
+ {
+ // If F11 is pressed in fullscreen, we switch to normal
+ leaveFullscreen();
+ return;
+ }
+ QSettings settings;
+ menuBar()->hide();
+ findChild<QToolBar*>()->hide();
+ settings.setValue("geometry", saveGeometry());
+ m_wasMaximized = isMaximized();
+ showFullScreen();
+ if (! m_leaveFullscreenButton)
+ m_leaveFullscreenButton =
+ new LeaveFullscreenButton(this, m_actionLeaveFullscreen);
+ m_leaveFullscreenButton->showButton();
+}
+
+void MainWindow::gameInfo()
+{
+ GameInfoDialog dialog(this, m_game);
+ dialog.exec();
+ updateWindow(false);
+}
+
+void MainWindow::gameOver()
+{
+ auto variant = m_bd.get_variant();
+ auto nuColors = get_nu_colors(variant);
+ auto nuPlayers = get_nu_players(variant);
+ bool breakTies = (m_bd.get_piece_set() == PieceSet::callisto);
+ QString info;
+ if (nuColors == 2)
+ {
+ auto score = m_bd.get_score_twocolor(Color(0));
+ if (score == 1)
+ info = tr("Blue wins with 1 point.");
+ else if (score > 0)
+ info = tr("Blue wins with %1 points.").arg(score);
+ else if (score == -1)
+ info = tr("Green wins with 1 point.");
+ else if (score < 0)
+ info = tr("Green wins with %1 points.").arg(-score);
+ else if (breakTies)
+ info = tr("Green wins (tie resolved).");
+ else
+ info = tr("The game ends in a tie.");
+ }
+ else if (nuPlayers == 2)
+ {
+ LIBBOARDGAME_ASSERT(nuColors == 4);
+ auto score = m_bd.get_score_multicolor(Color(0));
+ if (score == 1)
+ info = tr("Blue/Red wins with 1 point.");
+ else if (score > 0)
+ info = tr("Blue/Red wins with %1 points.").arg(score);
+ else if (score == -1)
+ info = tr("Yellow/Green wins with 1 point.");
+ else if (score < 0)
+ info = tr("Yellow/Green wins with %1 points.").arg(-score);
+ else if (breakTies)
+ info = tr("Yellow/Green wins (tie resolved).");
+ else
+ info = tr("The game ends in a tie.");
+ }
+ else if (nuPlayers == 3)
+ {
+ auto blue = m_bd.get_points(Color(0));
+ auto yellow = m_bd.get_points(Color(1));
+ auto red = m_bd.get_points(Color(2));
+ auto maxPoints = max(blue, max(yellow, red));
+ if (breakTies && red == maxPoints
+ && (blue == maxPoints || yellow == maxPoints))
+ info = tr("Red wins (tie resolved).");
+ else if (breakTies && yellow == maxPoints && blue == maxPoints)
+ info = tr("Yellow wins (tie resolved).");
+ else if (blue == yellow && yellow == red)
+ info = tr("The game ends in a tie between all colors.");
+ else if (blue == maxPoints && blue == yellow)
+ info = tr("The game ends in a tie between Blue and Yellow.");
+ else if (blue == maxPoints && blue == red)
+ info = tr("The game ends in a tie between Blue and Red.");
+ else if (yellow == maxPoints && yellow == red)
+ info = tr("The game ends in a tie between Yellow and Red.");
+ else if (blue == maxPoints)
+ info = tr("Blue wins.");
+ else if (yellow == maxPoints)
+ info = tr("Yellow wins.");
+ else
+ info = tr("Red wins.");
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(nuPlayers == 4);
+ auto blue = m_bd.get_points(Color(0));
+ auto yellow = m_bd.get_points(Color(1));
+ auto red = m_bd.get_points(Color(2));
+ auto green = m_bd.get_points(Color(3));
+ auto maxPoints = max(blue, max(yellow, max(red, green)));
+ if (breakTies && green == maxPoints
+ && (red == maxPoints || blue == maxPoints
+ || yellow == maxPoints))
+ info = tr("Green wins (tie resolved).");
+ else if (breakTies && red == maxPoints
+ && (blue == maxPoints || yellow == maxPoints))
+ info = tr("Red wins (tie resolved).");
+ else if (breakTies && yellow == maxPoints && blue == maxPoints)
+ info = tr("Yellow wins (tie resolved).");
+ else if (blue == yellow && yellow == red && red == green)
+ info = tr("The game ends in a tie between all colors.");
+ else if (blue == maxPoints && blue == yellow && yellow == red)
+ info = tr("The game ends in a tie between Blue, Yellow and Red.");
+ else if (blue == maxPoints && blue == yellow && yellow == green)
+ info =
+ tr("The game ends in a tie between Blue, Yellow and Green.");
+ else if (blue == maxPoints && blue == red && red == green)
+ info = tr("The game ends in a tie between Blue, Red and Green.");
+ else if (yellow == maxPoints && yellow == red && red == green)
+ info = tr("The game ends in a tie between Yellow, Red and Green.");
+ else if (blue == maxPoints && blue == yellow)
+ info = tr("The game ends in a tie between Blue and Yellow.");
+ else if (blue == maxPoints && blue == red)
+ info = tr("The game ends in a tie between Blue and Red.");
+ else if (blue == maxPoints && blue == green)
+ info = tr("The game ends in a tie between Blue and Green.");
+ else if (yellow == maxPoints && yellow == red)
+ info = tr("The game ends in a tie between Yellow and Red.");
+ else if (yellow == maxPoints && yellow == green)
+ info = tr("The game ends in a tie between Yellow and Green.");
+ else if (red == maxPoints && red == green)
+ info = tr("The game ends in a tie between Red and Green.");
+ else if (blue == maxPoints)
+ info = tr("Blue wins.");
+ else if (yellow == maxPoints)
+ info = tr("Yellow wins.");
+ else if (red == maxPoints)
+ info = tr("Red wins.");
+ else
+ info = tr("Green wins.");
+ }
+ if (m_isRated)
+ {
+ QString detailText;
+ int oldRating = m_history->getRating().to_int();
+ unsigned place;
+ bool isPlaceShared;
+ m_bd.get_place(m_ratedGameColor, place, isPlaceShared);
+ float gameResult;
+ if (place == 0 && !isPlaceShared)
+ gameResult = 1;
+ else if (place == 0 && isPlaceShared)
+ gameResult = 0.5;
+ else
+ gameResult = 0;
+ unsigned nuOpp = get_nu_players(variant) - 1;
+ Rating oppRating = m_player->get_rating(variant);
+ QString date = QString(PentobiTree::get_date_today().c_str());
+ m_history->addGame(gameResult, oppRating, nuOpp, m_ratedGameColor,
+ gameResult, date, m_level, m_game.get_tree());
+ if (m_ratingDialog)
+ m_ratingDialog->updateContent();
+ int newRating = m_history->getRating().to_int();
+ if (newRating > oldRating)
+ detailText = tr("Your rating has increased from %1 to %2.")
+ .arg(QString::number(oldRating), QString::number(newRating));
+ else if (newRating == oldRating)
+ detailText = tr("Your rating stays at %1.").arg(oldRating);
+ else
+ detailText =
+ tr("Your rating has decreased from %1 to %2.")
+ .arg(QString::number(oldRating), QString::number(newRating));
+ setRated(false);
+ QSettings settings;
+ auto key = QString("next_rated_random_") + to_string_id(variant);
+ settings.remove(key);
+ QMessageBox msgBox(this);
+ Util::setNoTitle(msgBox);
+ msgBox.setIcon(QMessageBox::Information);
+ msgBox.setText(info);
+ msgBox.setInformativeText(detailText);
+ auto showRatingButton =
+ msgBox.addButton(tr("Show &Rating"), QMessageBox::AcceptRole);
+ msgBox.addButton(QMessageBox::Close);
+ msgBox.setDefaultButton(showRatingButton);
+ msgBox.exec();
+ auto result = msgBox.clickedButton();
+ if (result == showRatingButton)
+ showRating();
+ }
+ else
+ showInfo(info);
+}
+
+void MainWindow::genMove(bool playSingleMove)
+{
+ cancelThread();
+ ++m_genMoveId;
+ setCursor(QCursor(Qt::BusyCursor));
+ m_actionNextPiece->setEnabled(false);
+ m_actionPreviousPiece->setEnabled(false);
+ m_actionPlay->setEnabled(false);
+ m_actionPlaySingleMove->setEnabled(false);
+ m_actionInterrupt->setEnabled(true);
+ showStatus(tr("Computer is thinking..."));
+ clearPiece();
+ clear_abort();
+ m_lastRemainingSeconds = 0;
+ m_lastRemainingMinutes = 0;
+ m_player->set_level(m_level);
+ QFuture<GenMoveResult> future =
+ QtConcurrent::run(this, &MainWindow::asyncGenMove, m_bd.get_to_play(),
+ m_genMoveId, playSingleMove);
+ m_genMoveWatcher.setFuture(future);
+ m_isGenMoveRunning = true;
+}
+
+void MainWindow::genMoveFinished()
+{
+ m_actionInterrupt->setEnabled(false);
+ clearStatus();
+ GenMoveResult result = m_genMoveWatcher.future().result();
+ if (result.genMoveId != m_genMoveId)
+ {
+ // Callback from a move generation canceled with cancelThread()
+ return;
+ }
+ LIBBOARDGAME_ASSERT(m_isGenMoveRunning);
+ m_isGenMoveRunning = false;
+ setCursor(QCursor(Qt::ArrowCursor));
+ if (get_abort() && computerPlaysAll())
+ m_computerColors.fill(false);
+ Color c = result.color;
+ auto mv = result.move;
+ if (mv.is_null())
+ {
+ // No need to translate, should never happen if program is correct
+ showError("Computer failed to generate a move");
+ return;
+ }
+ if (! m_bd.is_legal(c, mv))
+ {
+ // No need to translate, should never happen if program is correct
+ showError("Computer generated illegal move");
+ return;
+ }
+ play(c, mv);
+ // Call updateWindow() before checkComputerMove() because checkComputerMove
+ // resets m_lastComputerMovesBegin if computer doesn't play current color
+ // and updateWindow needs m_lastComputerMovesBegin
+ updateWindow(true);
+ if (! result.playSingleMove)
+ checkComputerMove();
+}
+
+QString MainWindow::getFilter() const
+{
+ return tr("Blokus games (*.blksgf);;All files (*)");
+}
+
+QString MainWindow::getLastDir()
+{
+ QSettings settings;
+ auto dir = settings.value("last_dir", "").toString();
+ if (dir.isEmpty() || ! QFileInfo::exists(dir))
+ dir = QStandardPaths::writableLocation(
+ QStandardPaths::DocumentsLocation);
+ return dir;
+}
+
+QString MainWindow::getVersion() const
+{
+ QString version;
+#ifdef VERSION
+ version = VERSION;
+#endif
+ if (version.isEmpty())
+ version = "UNKNOWN";
+ return version;
+}
+
+void MainWindow::goodMove(bool checked)
+{
+ if (! checked)
+ return;
+ m_game.set_good_move();
+ updateWindow(false);
+}
+
+void MainWindow::gotoMove()
+{
+ vector<const SgfNode*> nodes;
+ auto& tree = m_game.get_tree();
+ auto node = &m_game.get_current();
+ do
+ {
+ if (! tree.get_move(*node).is_null())
+ nodes.insert(nodes.begin(), node);
+ node = node->get_parent_or_null();
+ }
+ while (node);
+ node = m_game.get_current().get_first_child_or_null();
+ while (node)
+ {
+ if (! tree.get_move(*node).is_null())
+ nodes.push_back(node);
+ node = node->get_first_child_or_null();
+ }
+ int maxMoves = int(nodes.size());
+ if (maxMoves == 0)
+ return;
+ int defaultValue = m_bd.get_nu_moves();
+ if (defaultValue == 0)
+ defaultValue = maxMoves;
+ QInputDialog dialog(this);
+ dialog.setWindowFlags(dialog.windowFlags()
+ & ~Qt::WindowContextHelpButtonHint);
+ dialog.setWindowTitle(tr("Go to Move"));
+ dialog.setLabelText(tr("Move number:"));
+ dialog.setInputMode(QInputDialog::IntInput);
+ dialog.setIntRange(1, static_cast<int>(nodes.size()));
+ dialog.setIntStep(1);
+ dialog.setIntValue(defaultValue);
+ if (dialog.exec())
+ gotoNode(*nodes[dialog.intValue() - 1]);
+}
+
+void MainWindow::gotoNode(const SgfNode& node)
+{
+ cancelThread();
+ leaveSetupMode();
+ try
+ {
+ m_game.goto_node(node);
+ }
+ catch (const InvalidTree& e)
+ {
+ showInvalidFile(m_file, e);
+ return;
+ }
+ if (m_analyzeGameWindow && m_analyzeGameWindow->isVisible())
+ m_analyzeGameWindow->analyzeGameWidget
+ ->setCurrentPosition(m_game, node);
+ m_autoPlay = false;
+ updateWindow(true);
+}
+
+void MainWindow::gotoNode(const SgfNode* node)
+{
+ if (node)
+ gotoNode(*node);
+}
+
+void MainWindow::gotoPosition(Variant variant,
+ const vector<ColorMove>& moves)
+{
+ if (m_bd.get_variant() != variant)
+ return;
+ auto& tree = m_game.get_tree();
+ auto node = &tree.get_root();
+ if (tree.has_move_ignore_invalid(*node))
+ // Move in root node not supported.
+ return;
+ for (ColorMove mv : moves)
+ {
+ bool found = false;
+ for (auto& i : node->get_children())
+ if (tree.get_move(i) == mv)
+ {
+ found = true;
+ node = &i;
+ break;
+ }
+ if (! found)
+ return;
+ }
+ gotoNode(*node);
+}
+
+void MainWindow::help()
+{
+ if (m_helpWindow)
+ {
+ m_helpWindow->show();
+ m_helpWindow->raise();
+ return;
+ }
+ QString path = HelpWindow::findMainPage(m_helpDir, "pentobi");
+ m_helpWindow = new HelpWindow(nullptr, tr("Pentobi Help"), path);
+ initToolBarText(m_helpWindow->findChild<QToolBar*>());
+ m_helpWindow->show();
+}
+
+void MainWindow::initGame()
+{
+ setRated(false);
+ if (m_analyzeGameWindow)
+ {
+ delete m_analyzeGameWindow;
+ m_analyzeGameWindow = nullptr;
+ }
+ m_game.init();
+ m_game.set_charset("UTF-8");
+#ifdef VERSION
+ m_game.set_application("Pentobi", VERSION);
+#else
+ m_game.set_application("Pentobi");
+#endif
+ m_game.set_date_today();
+ m_game.clear_modified();
+ QSettings settings;
+ if (! settings.value("computer_color_none").toBool())
+ {
+ for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
+ m_computerColors[c] = ! m_bd.is_same_player(c, Color(0));
+ m_autoPlay = true;
+ }
+ else
+ {
+ m_computerColors.fill(false);
+ m_autoPlay = false;
+ }
+ leaveSetupMode();
+ m_gameFinished = false;
+ m_isAutoSaveLoaded = false;
+ setFile("");
+}
+
+void MainWindow::initVariantActions()
+{
+ // Use a temporary const variable to avoid that QList detaches in for loop
+ const auto actions = m_actionGroupVariant->actions();
+ for (auto action : actions)
+ if (Variant(action->data().toInt()) == m_bd.get_variant())
+ {
+ action->setChecked(true);
+ return;
+ }
+}
+
+void MainWindow::initPieceSelectors()
+{
+ for (Color::IntType i = 0; i < Color::range; ++i)
+ {
+ bool isVisible = (i < m_bd.get_nu_colors());
+ m_pieceSelector[Color(i)]->setVisible(isVisible);
+ if (isVisible)
+ m_pieceSelector[Color(i)]->init();
+ }
+}
+
+void MainWindow::interestingMove(bool checked)
+{
+ if (! checked)
+ return;
+ m_game.set_interesting_move();
+ updateWindow(false);
+}
+
+void MainWindow::interrupt()
+{
+ cancelThread();
+ m_autoPlay = false;
+}
+
+void MainWindow::interruptPlay()
+{
+ if (! m_isGenMoveRunning)
+ return;
+ set_abort();
+ m_autoPlay = false;
+}
+
+bool MainWindow::isComputerToPlay() const
+{
+ Color to_play = m_bd.get_to_play();
+ if (m_game.get_variant() != Variant::classic_3 || to_play != Color(3))
+ return m_computerColors[to_play];
+ return m_computerColors[Color(m_bd.get_alt_player())];
+}
+
+void MainWindow::keepOnlyPosition()
+{
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("Keep only position?"),
+ tr("All previous and following moves and variations will"
+ " be removed from the game tree."));
+ auto keepOnlyPositionButton =
+ msgBox.addButton(tr("Keep Only Position"),
+ QMessageBox::DestructiveRole);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() != keepOnlyPositionButton)
+ return;
+ cancelThread();
+ m_game.keep_only_position();
+ updateWindow(true);
+}
+
+void MainWindow::keepOnlySubtree()
+{
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("Keep only subtree?"),
+ tr("All previous moves and variations will be removed"
+ " from the game tree."));
+ auto keepOnlySubtreeButton =
+ msgBox.addButton(tr("Keep Only Subtree"),
+ QMessageBox::DestructiveRole);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() != keepOnlySubtreeButton)
+ return;
+ cancelThread();
+ m_game.keep_only_subtree();
+ updateWindow(true);
+}
+
+void MainWindow::leaveFullscreen()
+{
+ if (! isFullScreen())
+ return;
+ QSettings settings;
+ auto showToolbar = settings.value("toolbar", true).toBool();
+ menuBar()->show();
+ findChild<QToolBar*>()->setVisible(showToolbar);
+ // m_leaveFullscreenButton can be null if the window was put in fullscreen
+ // mode by a "generic" method by the window manager (e.g. the title bar
+ // menu on KDE) and not by MainWindow::fullscreen()
+ if (m_leaveFullscreenButton)
+ m_leaveFullscreenButton->hideButton();
+ // Call showNormal() even if m_wasMaximized otherwise restoring the
+ // maximized window state does not work correctly on Xfce
+ showNormal();
+ if (m_wasMaximized)
+ showMaximized();
+}
+
+void MainWindow::leaveSetupMode()
+{
+ if (! m_actionSetupMode->isChecked())
+ return;
+ setupMode(false);
+}
+
+void MainWindow::levelTriggered(bool checked)
+{
+ if (checked)
+ setLevel(qobject_cast<QAction*>(sender())->data().toUInt());
+}
+
+void MainWindow::loadHistory()
+{
+ auto variant = m_game.get_variant();
+ if (m_history->getVariant() == variant)
+ return;
+ m_history->load(variant);
+ if (m_ratingDialog)
+ m_ratingDialog->updateContent();
+}
+
+void MainWindow::makeMainVariation()
+{
+ m_game.make_main_variation();
+ updateWindow(false);
+}
+
+void MainWindow::moveDownVariation()
+{
+ m_game.move_down_variation();
+ updateWindow(false);
+}
+
+void MainWindow::moveUpVariation()
+{
+ m_game.move_up_variation();
+ updateWindow(false);
+}
+
+void MainWindow::nextColor()
+{
+ m_game.set_to_play(m_bd.get_next(m_bd.get_to_play()));
+ auto to_play = m_bd.get_to_play();
+ m_orientationDisplay->selectColor(to_play);
+ clearPiece();
+ for (Color c : m_bd.get_colors())
+ m_pieceSelector[c]->setEnabled(to_play == c);
+ if (m_actionSetupMode->isChecked())
+ setSetupPlayer();
+ updateWindow(false);
+}
+
+void MainWindow::nextPiece()
+{
+ auto c = m_bd.get_to_play();
+ if (m_bd.get_pieces_left(c).empty())
+ return;
+ auto nuUniqPieces = m_bd.get_nu_uniq_pieces();
+ Piece::IntType i;
+ Piece selectedPiece = m_guiBoard->getSelectedPiece();
+ if (! selectedPiece.is_null())
+ i = static_cast<Piece::IntType>(selectedPiece.to_int() + 1);
+ else
+ i = 0;
+ while (true)
+ {
+ if (i >= nuUniqPieces)
+ i = 0;
+ if (m_bd.is_piece_left(c, Piece(i)))
+ break;
+ ++i;
+ }
+ selectPiece(c, Piece(i));
+}
+
+void MainWindow::nextTransform()
+{
+ Piece piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ return;
+ auto transform = m_guiBoard->getSelectedPieceTransform();
+ transform = m_bd.get_piece_info(piece).get_next_transform(transform);
+ m_guiBoard->setSelectedPieceTransform(transform);
+ m_orientationDisplay->setSelectedPieceTransform(transform);
+}
+
+void MainWindow::nextVariation()
+{
+ gotoNode(m_game.get_current().get_sibling());
+}
+
+void MainWindow::newGame()
+{
+ if (! checkSave())
+ return;
+ cancelThread();
+ initGame();
+ deleteAutoSaveFile();
+ updateWindow(true);
+}
+
+void MainWindow::noMoveAnnotation(bool checked)
+{
+ if (! checked)
+ return;
+ m_game.remove_move_annotation();
+ updateWindow(false);
+}
+
+void MainWindow::open()
+{
+ if (! checkSave())
+ return;
+ QString file = QFileDialog::getOpenFileName(this, tr("Open"), getLastDir(),
+ getFilter());
+ if (file.isEmpty())
+ return;
+ rememberDir(file);
+ if (open(file))
+ rememberFile(file);
+}
+
+bool MainWindow::open(const QString& file, bool isTemporary)
+{
+ if (file.isEmpty())
+ return false;
+ cancelThread();
+ TreeReader reader;
+ ifstream in(file.toLocal8Bit().constData());
+ try
+ {
+ reader.read(in);
+ }
+ catch (const TreeReader::ReadError& e)
+ {
+ if (! in)
+ {
+ QString text =
+ tr("Could not read file '%1'").arg(QFileInfo(file).fileName());
+ showError(text, QString::fromLocal8Bit(strerror(errno)));
+ }
+ else
+ {
+ showInvalidFile(file, e);
+ }
+ return false;
+ }
+ m_isAutoSaveLoaded = false;
+ if (! isTemporary)
+ {
+ setFile(file);
+ deleteAutoSaveFile();
+ }
+ if (m_analyzeGameWindow)
+ {
+ delete m_analyzeGameWindow;
+ m_analyzeGameWindow = nullptr;
+ }
+ setRated(false);
+ try
+ {
+ auto tree = reader.get_tree_transfer_ownership();
+ m_game.init(tree);
+ if (! libpentobi_base::node_util::has_setup(m_game.get_root()))
+ m_game.goto_node(get_last_node(m_game.get_root()));
+ initPieceSelectors();
+ }
+ catch (const InvalidTree& e)
+ {
+ showInvalidFile(file, e);
+ }
+ m_computerColors.fill(false);
+ m_autoPlay = false;
+ leaveSetupMode();
+ initVariantActions();
+ restoreLevel(m_bd.get_variant());
+ updateWindow(true);
+ loadHistory();
+ return true;
+}
+
+void MainWindow::openRecentFile()
+{
+ auto action = qobject_cast<QAction*>(sender());
+ if (action)
+ openCheckSave(action->data().toString());
+}
+
+void MainWindow::openCheckSave(const QString& file)
+{
+ if (checkSave())
+ open(file);
+}
+
+void MainWindow::orientationDisplayColorClicked(Color)
+{
+ if (m_actionSetupMode->isChecked())
+ nextColor();
+}
+
+void MainWindow::placePiece(Color c, Move mv)
+{
+ cancelThread();
+ bool isSetupMode = m_actionSetupMode->isChecked();
+ bool isAltColor =
+ (m_bd.get_variant() == Variant::classic_3 && c.to_int() == 3);
+ if ((! isAltColor && m_computerColors[c])
+ || (isAltColor && m_computerColors[Color(m_bd.get_alt_player())])
+ || isSetupMode)
+ // If the user enters a move previously played by the computer (e.g.
+ // after undoing moves) then it is unlikely that the user wants to keep
+ // the computer color settings.
+ m_computerColors.fill(false);
+ if (isSetupMode)
+ {
+ m_game.add_setup(c, mv);
+ setSetupPlayer();
+ updateWindow(true);
+ }
+ else
+ {
+ play(c, mv);
+ updateWindow(true);
+ checkComputerMove();
+ }
+}
+
+void MainWindow::play()
+{
+ cancelThread();
+ leaveSetupMode();
+ if (! isComputerToPlay())
+ {
+ m_computerColors.fill(false);
+ auto c = m_bd.get_to_play();
+ if (m_bd.get_variant() == Variant::classic_3 && c == Color(3))
+ m_computerColors[Color(m_bd.get_alt_player())] = true;
+ else
+ {
+ m_computerColors[c] = true;
+ m_computerColors[m_bd.get_second_color(c)] = true;
+ }
+ QSettings settings;
+ settings.setValue("computer_color_none", false);
+ }
+ m_autoPlay = true;
+ genMove();
+}
+
+void MainWindow::play(Color c, Move mv)
+{
+ m_game.play(c, mv, false);
+ m_gameFinished = false;
+ if (m_bd.is_game_over())
+ {
+ updateWindow(true);
+ repaint();
+ gameOver();
+ m_gameFinished = true;
+ deleteAutoSaveFile();
+ return;
+ }
+}
+
+void MainWindow::playSingleMove()
+{
+ cancelThread();
+ leaveSetupMode();
+ m_autoPlay = false;
+ genMove(true);
+}
+
+void MainWindow::pointClicked(Point p)
+{
+ // If a piece on the board is clicked on in setup mode, remove it and make
+ // it the selected piece without changing its orientation.
+ if (! m_actionSetupMode->isChecked())
+ return;
+ PointState s = m_bd.get_point_state(p);
+ if (s.is_empty())
+ return;
+ Color c = s.to_color();
+ Move mv = m_bd.get_move_at(p);
+ m_game.remove_setup(c, mv);
+ setSetupPlayer();
+ updateWindow(true);
+ selectPiece(c, m_bd.get_move_piece(mv), m_bd.find_transform(mv));
+ m_guiBoard->setSelectedPiecePoints(mv);
+}
+
+void MainWindow::previousPiece()
+{
+ auto c = m_bd.get_to_play();
+ if (m_bd.get_pieces_left(c).empty())
+ return;
+ auto nuUniqPieces = m_bd.get_nu_uniq_pieces();
+ Piece::IntType i;
+ Piece selectedPiece = m_guiBoard->getSelectedPiece();
+ if (! selectedPiece.is_null())
+ i = selectedPiece.to_int();
+ else
+ i = 0;
+ while (true)
+ {
+ if (i == 0)
+ i = static_cast<Piece::IntType>(nuUniqPieces - 1);
+ else
+ --i;
+ if (m_bd.is_piece_left(c, Piece(i)))
+ break;
+ }
+ selectPiece(c, Piece(i));
+}
+
+void MainWindow::previousTransform()
+{
+ Piece piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ return;
+ auto transform = m_guiBoard->getSelectedPieceTransform();
+ transform =
+ m_bd.get_piece_info(piece).get_previous_transform(transform);
+ m_guiBoard->setSelectedPieceTransform(transform);
+ m_orientationDisplay->setSelectedPieceTransform(transform);
+}
+
+void MainWindow::previousVariation()
+{
+ gotoNode(m_game.get_current().get_previous_sibling());
+}
+
+void MainWindow::ratedGame()
+{
+ if (! checkSave())
+ return;
+ cancelThread();
+ if (m_history->getNuGames() == 0)
+ {
+ InitialRatingDialog dialog(this);
+ if (dialog.exec() != QDialog::Accepted)
+ return;
+ m_history->init(Rating(static_cast<float>(dialog.getRating())));
+ }
+ int level;
+ QSettings settings;
+ unsigned random;
+ auto variant = m_bd.get_variant();
+ auto key = QString("next_rated_random_") + to_string_id(variant);
+ if (settings.contains(key))
+ random = settings.value(key).toUInt();
+ else
+ {
+ // RandomGenerator::ResultType may be larger than unsigned
+ random = static_cast<unsigned>(m_random.generate() % 1000);
+ settings.setValue(key, random);
+ }
+ m_history->getNextRatedGameSettings(m_maxLevel, random,
+ level, m_ratedGameColor);
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("Start rated game?"),
+ "<html>" +
+ tr("In this game, you play %1 against Pentobi level %2.")
+ .arg(getPlayerString(variant, m_ratedGameColor),
+ QString::number(level)));
+ auto startGameButton =
+ msgBox.addButton(tr("&Start Game"), QMessageBox::AcceptRole);
+ msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(startGameButton);
+ msgBox.exec();
+ auto result = msgBox.clickedButton();
+ if (result != startGameButton)
+ return;
+ setLevel(level);
+ initGame();
+ setFile("");
+ setRated(true);
+ m_computerColors.fill(true);
+ for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
+ if (m_bd.is_same_player(c, m_ratedGameColor))
+ m_computerColors[c] = false;
+ m_autoPlay = true;
+ QString computerPlayerName =
+ //: The first argument is the version of Pentobi
+ tr("Pentobi %1 (level %2)").arg(getVersion(), QString::number(level));
+ string charset = m_game.get_root().get_property("CA", "");
+ string computerPlayerNameStdStr =
+ Util::convertSgfValueFromQString(computerPlayerName, charset);
+ string humanPlayerNameStdStr =
+ Util::convertSgfValueFromQString(tr("Human"), charset);
+ for (Color c : Color::Range(m_bd.get_nu_nonalt_colors()))
+ if (m_computerColors[c])
+ m_game.set_player_name(c, computerPlayerNameStdStr);
+ else
+ m_game.set_player_name(c, humanPlayerNameStdStr);
+ // Setting the player names marks the game as modified but there is nothing
+ // important that would need to be saved yet
+ m_game.clear_modified();
+ deleteAutoSaveFile();
+ updateWindow(true);
+ checkComputerMove();
+}
+
+void MainWindow::rememberDir(const QString& file)
+{
+ if (file.isEmpty())
+ return;
+ QString canonicalFile = file;
+ QString canonicalFilePath = QFileInfo(file).canonicalFilePath();
+ if (! canonicalFilePath.isEmpty())
+ canonicalFile = canonicalFilePath;
+ QFileInfo info(canonicalFile);
+ QSettings settings;
+ settings.setValue("last_dir", info.dir().path());
+}
+
+void MainWindow::rememberFile(const QString& file)
+{
+ if (file.isEmpty())
+ return;
+ QString canonicalFile = file;
+ QString canonicalFilePath = QFileInfo(file).canonicalFilePath();
+ if (! canonicalFilePath.isEmpty())
+ canonicalFile = canonicalFilePath;
+ QSettings settings;
+ auto files = settings.value("recent_files").toStringList();
+ files.removeAll(canonicalFile);
+ files.prepend(canonicalFile);
+ while (files.size() > maxRecentFiles)
+ files.removeLast();
+ settings.setValue("recent_files", files);
+ settings.sync(); // updateRecentFiles() needs the new settings
+ updateRecentFiles();
+}
+
+void MainWindow::restoreLevel(Variant variant)
+{
+ QSettings settings;
+ QString key = QString("level_") + to_string_id(variant);
+ m_level = settings.value(key, 1).toInt();
+ if (m_level < 1)
+ m_level = 1;
+ if (m_level > m_maxLevel)
+ m_level = m_maxLevel;
+ m_actionGroupLevel->actions().at(m_level - 1)->setChecked(true);
+}
+
+void MainWindow::rotateAnticlockwise()
+{
+ Piece piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ return;
+ auto transform = m_guiBoard->getSelectedPieceTransform();
+ transform = m_bd.get_transforms().get_rotated_anticlockwise(transform);
+ transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
+ m_guiBoard->setSelectedPieceTransform(transform);
+ m_orientationDisplay->setSelectedPieceTransform(transform);
+ updateFlipActions();
+}
+
+void MainWindow::rotateClockwise()
+{
+ Piece piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ return;
+ auto transform = m_guiBoard->getSelectedPieceTransform();
+ transform = m_bd.get_transforms().get_rotated_clockwise(transform);
+ transform = m_bd.get_piece_info(piece).get_equivalent_transform(transform);
+ m_guiBoard->setSelectedPieceTransform(transform);
+ m_orientationDisplay->setSelectedPieceTransform(transform);
+ updateFlipActions();
+}
+
+void MainWindow::save()
+{
+ if (m_file.isEmpty())
+ saveAs();
+ else if (save(m_file))
+ {
+ m_game.clear_modified();
+ updateWindow(false);
+ }
+}
+
+bool MainWindow::save(const QString& file)
+{
+ if (! writeGame(file.toLocal8Bit().constData()))
+ {
+ showError(tr("The file could not be saved."),
+ /*: Error message if file cannot be saved. %1 is
+ replaced by the file name, %2 by the error message
+ of the operating system. */
+ tr("%1: %2").arg(file,
+ QString::fromLocal8Bit(strerror(errno))));
+ return false;
+ }
+ else
+ {
+ Util::removeThumbnail(file);
+ return true;
+ }
+}
+
+void MainWindow::saveAs()
+{
+ QString file = m_file;
+ if (file.isEmpty())
+ {
+ file = getLastDir();
+ file.append(QDir::separator());
+ file.append(tr("Untitled Game.blksgf"));
+ if (QFileInfo::exists(file))
+ for (unsigned i = 1; ; ++i)
+ {
+ file = getLastDir();
+ file.append(QDir::separator());
+ file.append(tr("Untitled Game %1.blksgf").arg(i));
+ if (! QFileInfo::exists(file))
+ break;
+ }
+ }
+ file = QFileDialog::getSaveFileName(this, tr("Save"), file, getFilter());
+ if (! file.isEmpty())
+ {
+ rememberDir(file);
+ if (save(file))
+ {
+ m_game.clear_modified();
+ updateWindow(false);
+ }
+ setFile(file);
+ rememberFile(file);
+ }
+}
+
+void MainWindow::searchCallback(double elapsedSeconds, double remainingSeconds)
+{
+ // If the search is longer than 10 sec, we show the (maximum) remaining
+ // time (only during a move generation, ignore search callbacks during
+ // game analysis)
+ if (! m_isGenMoveRunning || elapsedSeconds < 10)
+ return;
+ QString text;
+ int seconds = static_cast<int>(ceil(remainingSeconds));
+ if (seconds < 90)
+ {
+ if (seconds == m_lastRemainingSeconds)
+ return;
+ m_lastRemainingSeconds = seconds;
+ text =
+ tr("Computer is thinking... (up to %1 seconds remaining)")
+ .arg(seconds);
+ }
+ else
+ {
+ int minutes = static_cast<int>(ceil(remainingSeconds / 60));
+ if (minutes == m_lastRemainingMinutes)
+ return;
+ m_lastRemainingMinutes = minutes;
+ text =
+ tr("Computer is thinking... (up to %1 minutes remaining)")
+ .arg(minutes);
+ }
+ QMetaObject::invokeMethod(statusBar(), "showMessage", Q_ARG(QString, text),
+ Q_ARG(int, 0));
+}
+
+void MainWindow::selectNamedPiece()
+{
+ string name(qobject_cast<QAction*>(sender())->data().toString()
+ .toLocal8Bit().constData());
+ auto c = m_bd.get_to_play();
+ Board::PiecesLeftList pieces;
+ for (Piece::IntType i = 0; i < m_bd.get_nu_uniq_pieces(); ++i)
+ {
+ Piece piece(i);
+ if (m_bd.is_piece_left(c, piece)
+ && m_bd.get_piece_info(piece).get_name().find(name) == 0)
+ pieces.push_back(piece);
+ }
+ if (pieces.empty())
+ return;
+ auto piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ piece = pieces[0];
+ else
+ {
+ auto pos = std::find(pieces.begin(), pieces.end(), piece);
+ if (pos == pieces.end())
+ piece = pieces[0];
+ else
+ {
+ ++pos;
+ if (pos == pieces.end())
+ piece = pieces[0];
+ else
+ piece = *pos;
+ }
+ }
+ selectPiece(c, piece);
+}
+
+void MainWindow::selectPiece(Color c, Piece piece)
+{
+ selectPiece(c, piece, m_bd.get_transforms().get_default());
+}
+
+void MainWindow::selectPiece(Color c, Piece piece, const Transform* transform)
+{
+ if (m_isGenMoveRunning
+ || (m_bd.is_game_over() && ! m_actionSetupMode->isChecked()))
+ return;
+ m_game.set_to_play(c);
+ m_guiBoard->selectPiece(c, piece);
+ m_guiBoard->setSelectedPieceTransform(transform);
+ m_orientationDisplay->selectColor(c);
+ m_orientationDisplay->setSelectedPiece(piece);
+ m_orientationDisplay->setSelectedPieceTransform(transform);
+ bool can_rotate = m_bd.get_piece_info(piece).can_rotate();
+ m_actionRotateClockwise->setEnabled(can_rotate);
+ m_actionRotateAnticlockwise->setEnabled(can_rotate);
+ updateFlipActions();
+ m_actionClearPiece->setEnabled(true);
+}
+
+void MainWindow::setCommentText(const QString& text)
+{
+ m_ignoreCommentTextChanged = true;
+ m_comment->setPlainText(text);
+ m_ignoreCommentTextChanged = false;
+ if (! text.isEmpty())
+ m_comment->ensureCursorVisible();
+ m_comment->clearFocus();
+ updateWindow(false);
+}
+
+void MainWindow::setNoDelay()
+{
+ m_noDelay = true;
+}
+
+void MainWindow::setVariant(Variant variant)
+{
+ if (m_bd.get_variant() == variant)
+ return;
+ if (! checkSave())
+ {
+ initVariantActions();
+ return;
+ }
+ cancelThread();
+ QSettings settings;
+ settings.setValue("variant", to_string_id(variant));
+ clearPiece();
+ m_game.init(variant);
+ initPieceSelectors();
+ newGame();
+ loadHistory();
+ restoreLevel(variant);
+}
+
+void MainWindow::setFile(const QString& file)
+{
+ m_file = file;
+ // Don't use setWindowFilePath() because of QTBUG-16507
+ if (m_file.isEmpty())
+ setWindowTitle(tr("Pentobi"));
+ else
+ setWindowTitle(tr("[*]%1").arg(QFileInfo(m_file).fileName()));
+}
+
+void MainWindow::setLevel(unsigned level)
+{
+ if (level < 1 || level > m_maxLevel)
+ return;
+ m_level = level;
+ m_actionGroupLevel->actions().at(level - 1)->setChecked(true);
+ QSettings settings;
+ settings.setValue(QString("level_") + to_string_id(m_bd.get_variant()),
+ m_level);
+}
+
+void MainWindow::setMoveMarkingAllNumber(bool checked)
+{
+ if (! checked)
+ return;
+ QSettings settings;
+ settings.setValue("move_marking", "all_number");
+ updateWindow(false);
+}
+
+void MainWindow::setMoveMarkingLastDot(bool checked)
+{
+ if (! checked)
+ return;
+ QSettings settings;
+ settings.setValue("move_marking", "last_dot");
+ updateWindow(false);
+}
+
+void MainWindow::setMoveMarkingLastNumber(bool checked)
+{
+ if (! checked)
+ return;
+ QSettings settings;
+ settings.setValue("move_marking", "last_number");
+ updateWindow(false);
+}
+
+void MainWindow::setMoveMarkingNone(bool checked)
+{
+ if (! checked)
+ return;
+ QSettings settings;
+ settings.setValue("move_marking", "none");
+ updateWindow(false);
+}
+
+void MainWindow::setPlayToolTip()
+{
+ m_actionPlay->setToolTip(
+ m_computerColors[m_bd.get_to_play()] ?
+ tr("Make the computer continue to play the current color") :
+ tr("Make the computer play the current color"));
+}
+
+void MainWindow::setRated(bool isRated)
+{
+ m_isRated = isRated;
+ if (isRated)
+ {
+ statusBar()->addWidget(m_ratedGameLabelText);
+ m_ratedGameLabelText->show();
+ }
+ else if (m_ratedGameLabelText->isVisible())
+ statusBar()->removeWidget(m_ratedGameLabelText);
+}
+
+void MainWindow::setSetupPlayer()
+{
+ if (! m_game.has_setup())
+ m_game.remove_player();
+ else
+ m_game.set_player(m_bd.get_to_play());
+}
+
+void MainWindow::setTitleMenuLevel()
+{
+ QString title;
+ switch (m_game.get_variant())
+ {
+ case Variant::classic:
+ title = tr("&Level (Classic, 4 Players)");
+ break;
+ case Variant::classic_2:
+ title = tr("&Level (Classic, 2 Players)");
+ break;
+ case Variant::classic_3:
+ title = tr("&Level (Classic, 3 Players)");
+ break;
+ case Variant::duo:
+ title = tr("&Level (Duo)");
+ break;
+ case Variant::trigon:
+ title = tr("&Level (Trigon, 4 Players)");
+ break;
+ case Variant::trigon_2:
+ title = tr("&Level (Trigon, 2 Players)");
+ break;
+ case Variant::trigon_3:
+ title = tr("&Level (Trigon, 3 Players)");
+ break;
+ case Variant::junior:
+ title = tr("&Level (Junior)");
+ break;
+ case Variant::nexos:
+ title = tr("&Level (Nexos, 4 Players)");
+ break;
+ case Variant::nexos_2:
+ title = tr("&Level (Nexos, 2 Players)");
+ break;
+ case Variant::callisto:
+ title = tr("&Level (Callisto, 4 Players)");
+ break;
+ case Variant::callisto_2:
+ title = tr("&Level (Callisto, 2 Players)");
+ break;
+ case Variant::callisto_3:
+ title = tr("&Level (Callisto, 3 Players)");
+ break;
+ }
+ m_menuLevel->setTitle(title);
+}
+
+void MainWindow::setupMode(bool enable)
+{
+ // Currently, we allow setup mode only if no moves have been played. It
+ // should also work in inner nodes but this might be confusing for users
+ // and violate some assumptions in the user interface (e.g. node depth is
+ // equal to move number). Therefore, m_actionSetupMode is disabled if the
+ // root node has children, but we still need to check for it here because
+ // due to bugs in the Unitiy interface in Ubuntu 11.10, menu items are
+ // not always disabled if the corresponding action is disabled.
+ if (enable && m_game.get_root().has_children())
+ {
+ showInfo(tr("Setup mode cannot be used if moves have been played."));
+ enable = false;
+ }
+ m_actionSetupMode->setChecked(enable);
+ m_guiBoard->setFreePlacement(enable);
+ if (enable)
+ {
+ m_setupModeLabel->show();
+ for (Color c : m_bd.get_colors())
+ m_pieceSelector[c]->setEnabled(true);
+ m_computerColors.fill(false);
+ }
+ else
+ {
+ setSetupPlayer();
+ m_setupModeLabel->hide();
+ enablePieceSelector(m_bd.get_to_play());
+ updateWindow(false);
+ }
+}
+
+void MainWindow::showComment(bool checked)
+{
+ QSettings settings;
+ bool wasVisible = m_comment->isVisible();
+ if (wasVisible && ! checked)
+ settings.setValue("splitter_state", m_splitter->saveState());
+ settings.setValue("show_comment", checked);
+ m_comment->setVisible(checked);
+ if (! wasVisible && checked)
+ m_splitter->restoreState(
+ settings.value("splitter_state").toByteArray());
+
+}
+
+void MainWindow::showError(const QString& text, const QString& infoText,
+ const QString& detailText)
+{
+ ::showError(this, text, infoText, detailText);
+}
+
+void MainWindow::showInfo(const QString& text, const QString& infoText,
+ const QString& detailText, bool withIcon)
+{
+ ::showInfo(this, text, infoText, detailText, withIcon);
+}
+
+void MainWindow::showInvalidFile(QString file, const exception& e)
+{
+ showError(tr("Error in file '%1'").arg(QFileInfo(file).fileName()),
+ tr("The file is not a valid Blokus SGF file."), e.what());
+}
+
+void MainWindow::showRating()
+{
+ if (! m_ratingDialog)
+ {
+ m_ratingDialog = new RatingDialog(this, *m_history);
+ connect(m_ratingDialog, SIGNAL(openRecentFile(const QString&)),
+ SLOT(openCheckSave(const QString&)));
+ }
+ loadHistory();
+ m_ratingDialog->show();
+}
+
+void MainWindow::showStatus(const QString& text, bool temporary)
+{
+ int timeout = (temporary ? 4000 : 0);
+ statusBar()->showMessage(text, timeout);
+}
+
+void MainWindow::showToolbar(bool checked)
+{
+ QSettings settings;
+ settings.setValue("toolbar", checked);
+ findChild<QToolBar*>()->setVisible(checked);
+ m_menuToolBarText->setEnabled(checked);
+}
+
+QSize MainWindow::sizeHint() const
+{
+ auto geo = QApplication::desktop()->screenGeometry();
+ return QSize(geo.width() * 2 / 3, min(geo.width() * 4 / 10, geo.height()));
+}
+
+void MainWindow::toolBarNoText(bool checked)
+{
+ if (checked)
+ toolBarText("no_text", Qt::ToolButtonIconOnly);
+}
+
+void MainWindow::toolBarText(const QString& key, Qt::ToolButtonStyle style)
+{
+ QSettings settings;
+ settings.setValue("toolbar_text", key);
+ findChild<QToolBar*>()->setToolButtonStyle(style);
+ if (m_helpWindow)
+ m_helpWindow->findChild<QToolBar*>()->setToolButtonStyle(style);
+}
+
+void MainWindow::toolBarTextBesideIcons(bool checked)
+{
+ if (checked)
+ toolBarText("beside_icons", Qt::ToolButtonTextBesideIcon);
+}
+
+void MainWindow::toolBarTextBelowIcons(bool checked)
+{
+ if (checked)
+ toolBarText("below_icons", Qt::ToolButtonTextUnderIcon);
+}
+
+void MainWindow::toolBarTextOnly(bool checked)
+{
+ if (checked)
+ toolBarText("text_only", Qt::ToolButtonTextOnly);
+}
+
+void MainWindow::toolBarTextSystem(bool checked)
+{
+ if (checked)
+ toolBarText("system", Qt::ToolButtonFollowStyle);
+}
+
+void MainWindow::truncate()
+{
+ auto& current = m_game.get_current();
+ if (! current.has_parent())
+ return;
+ cancelThread();
+ if (current.has_children())
+ {
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("Truncate this subtree?"),
+ tr("This position and all following moves and"
+ " variations will be removed from the game tree."));
+ auto truncateButton =
+ msgBox.addButton(tr("Truncate"),
+ QMessageBox::DestructiveRole);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() != truncateButton)
+ return;
+ }
+ m_game.truncate();
+ m_autoPlay = false;
+ m_gameFinished = false;
+ updateWindow(true);
+}
+
+void MainWindow::truncateChildren()
+{
+ if (! m_game.get_current().has_children())
+ return;
+ cancelThread();
+ QMessageBox msgBox(this);
+ initQuestion(msgBox, tr("Truncate children?"),
+ tr("All following moves and variations will"
+ " be removed from the game tree."));
+ auto truncateButton =
+ msgBox.addButton(tr("Truncate Children"),
+ QMessageBox::DestructiveRole);
+ auto cancelButton = msgBox.addButton(QMessageBox::Cancel);
+ msgBox.setDefaultButton(cancelButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() != truncateButton)
+ return;
+ m_game.truncate_children();
+ m_gameFinished = false;
+ updateWindow(false);
+}
+
+void MainWindow::showVariations(bool checked)
+{
+ {
+ QSettings settings;
+ settings.setValue("show_variations", checked);
+ }
+ updateWindow(false);
+}
+
+void MainWindow::undo()
+{
+ auto& current = m_game.get_current();
+ if (current.has_children()
+ || ! m_game.get_tree().has_move_ignore_invalid(current)
+ || ! current.has_parent())
+ return;
+ cancelThread();
+ m_game.undo();
+ m_autoPlay = false;
+ m_gameFinished = false;
+ updateWindow(true);
+}
+
+void MainWindow::updateComment()
+{
+ string comment = m_game.get_comment();
+ if (comment.empty())
+ {
+ setCommentText("");
+ return;
+ }
+ string charset = m_game.get_root().get_property("CA", "");
+ setCommentText(Util::convertSgfValueToQString(comment, charset));
+}
+
+void MainWindow::updateFlipActions()
+{
+ Piece piece = m_guiBoard->getSelectedPiece();
+ if (piece.is_null())
+ return;
+ auto transform = m_guiBoard->getSelectedPieceTransform();
+ bool can_flip_horizontally =
+ m_bd.get_piece_info(piece).can_flip_horizontally(transform);
+ m_actionFlipHorizontally->setEnabled(can_flip_horizontally);
+ bool can_flip_vertically =
+ m_bd.get_piece_info(piece).can_flip_vertically(transform);
+ m_actionFlipVertically->setEnabled(can_flip_vertically);
+}
+
+void MainWindow::updateMoveAnnotationActions()
+{
+ if (m_game.get_move_ignore_invalid().is_null())
+ {
+ m_menuMoveAnnotation->setEnabled(false);
+ return;
+ }
+ m_menuMoveAnnotation->setEnabled(true);
+ double goodMove = m_game.get_good_move();
+ if (goodMove > 1)
+ {
+ m_actionVeryGoodMove->setChecked(true);
+ return;
+ }
+ if (goodMove > 0)
+ {
+ m_actionGoodMove->setChecked(true);
+ return;
+ }
+ double badMove = m_game.get_bad_move();
+ if (badMove > 1)
+ {
+ m_actionVeryBadMove->setChecked(true);
+ return;
+ }
+ if (badMove > 0)
+ {
+ m_actionBadMove->setChecked(true);
+ return;
+ }
+ if (m_game.is_interesting_move())
+ {
+ m_actionInterestingMove->setChecked(true);
+ return;
+ }
+ if (m_game.is_doubtful_move())
+ {
+ m_actionDoubtfulMove->setChecked(true);
+ return;
+ }
+ m_actionNoMoveAnnotation->setChecked(true);
+}
+
+void MainWindow::updateMoveNumber()
+{
+ auto& tree = m_game.get_tree();
+ auto& current = m_game.get_current();
+ unsigned move = get_move_number(tree, current);
+ unsigned movesLeft = get_moves_left(tree, current);
+ unsigned totalMoves = move + movesLeft;
+ string variation = get_variation_string(current);
+ QString text =
+ QString::fromLocal8Bit(get_position_info(tree, current).c_str());
+ QString toolTip;
+ if (variation.empty())
+ {
+ if (movesLeft == 0)
+ {
+ if (move > 0)
+ toolTip = tr("Move %1").arg(move);
+ }
+ else
+ {
+ if (move == 0)
+ toolTip = tr("%n move(s)", "", totalMoves);
+ else
+ toolTip = tr("Move %1 of %2").arg(QString::number(move),
+ QString::number(totalMoves));
+ }
+ }
+ else
+ toolTip = tr("Move %1 of %2 in variation %3")
+ .arg(QString::number(move), QString::number(totalMoves),
+ variation.c_str());
+ if (text.isEmpty())
+ {
+ if (m_moveNumber->isVisible())
+ statusBar()->removeWidget(m_moveNumber);
+ }
+ else
+ {
+ m_moveNumber->setText(text);
+ m_moveNumber->setToolTip(toolTip);
+ if (! m_moveNumber->isVisible())
+ {
+ statusBar()->addPermanentWidget(m_moveNumber);
+ m_moveNumber->show();
+ }
+ }
+}
+
+void MainWindow::updateRecentFiles()
+{
+ QSettings settings;
+ auto files = settings.value("recent_files").toStringList();
+ for (int i = 0; i < files.size(); ++i)
+ if (! QFileInfo::exists(files[i]))
+ {
+ files.removeAt(i);
+ --i;
+ }
+ int nuRecentFiles = files.size();
+ if (nuRecentFiles > maxRecentFiles)
+ nuRecentFiles = maxRecentFiles;
+ m_menuOpenRecent->setEnabled(nuRecentFiles > 0);
+ for (int i = 0; i < nuRecentFiles; ++i)
+ {
+ QFileInfo info = QFileInfo(files[i]);
+ QString name = info.absoluteFilePath();
+ // Don't prepend the filename by a number for a shortcut key
+ // because the file name may contain underscores and Ubuntu Unity does
+ // not handle this correctly (Unity bug #1390373)
+ m_actionRecentFile[i]->setText(name);
+ m_actionRecentFile[i]->setData(files[i]);
+ m_actionRecentFile[i]->setVisible(true);
+ }
+ for (int j = nuRecentFiles; j < maxRecentFiles; ++j)
+ m_actionRecentFile[j]->setVisible(false);
+}
+
+void MainWindow::updateWindow(bool currentNodeChanged)
+{
+ updateWindowModified();
+ m_guiBoard->copyFromBoard(m_bd);
+ QSettings settings;
+ auto markVariations = settings.value("show_variations", true).toBool();
+ unsigned nuMoves = m_bd.get_nu_moves();
+ unsigned markMovesBegin, markMovesEnd;
+ if (m_actionMoveMarkingAllNumber->isChecked())
+ {
+ markMovesBegin = 1;
+ markMovesEnd = nuMoves;
+ }
+ else if (m_actionMoveMarkingLastNumber->isChecked()
+ || m_actionMoveMarkingLastDot->isChecked())
+ {
+ markMovesBegin = nuMoves;
+ markMovesEnd = nuMoves;
+ }
+ else
+ {
+ markMovesBegin = 0;
+ markMovesEnd = 0;
+ }
+ gui_board_util::setMarkup(*m_guiBoard, m_game, markMovesBegin,
+ markMovesEnd, markVariations,
+ m_actionMoveMarkingLastDot->isChecked());
+ m_scoreDisplay->updateScore(m_bd);
+ if (m_legalMoves)
+ m_legalMoves->clear();
+ m_legalMoveIndex = 0;
+ bool isGameOver = m_bd.is_game_over();
+ auto to_play = m_bd.get_to_play();
+ if (isGameOver && ! m_actionSetupMode->isChecked())
+ m_orientationDisplay->clearSelectedColor();
+ else
+ m_orientationDisplay->selectColor(to_play);
+ if (currentNodeChanged)
+ {
+ clearPiece();
+ for (Color c : m_bd.get_colors())
+ m_pieceSelector[c]->checkUpdate();
+ if (! m_actionSetupMode->isChecked())
+ enablePieceSelector(to_play);
+ updateComment();
+ updateMoveAnnotationActions();
+ }
+ updateMoveNumber();
+ setPlayToolTip();
+ auto& tree = m_game.get_tree();
+ auto& current = m_game.get_current();
+ bool isMain = is_main_variation(current);
+ bool hasEarlierVariation = has_earlier_variation(current);
+ bool hasParent = current.has_parent();
+ bool hasChildren = current.has_children();
+ bool hasMove = tree.has_move_ignore_invalid(current);
+ bool hasMoves = m_bd.has_moves(to_play);
+ bool isEmpty = libboardgame_sgf::util::is_empty(tree);
+ bool hasNextVar = current.get_sibling();
+ bool hasPrevVar = current.get_previous_sibling();
+ m_actionAnalyzeGame->setEnabled(! m_isRated
+ && tree.has_main_variation_moves());
+ m_actionBackToMainVariation->setEnabled(! isMain);
+ m_actionBeginning->setEnabled(! m_isRated && hasParent);
+ m_actionBeginningOfBranch->setEnabled(hasEarlierVariation);
+ m_actionBackward->setEnabled(! m_isRated && hasParent);
+ m_actionComputerColors->setEnabled(! m_isRated);
+ m_actionDeleteAllVariations->setEnabled(tree.has_variations());
+ m_actionFindNextComment->setEnabled(! m_isRated);
+ m_actionForward->setEnabled(hasChildren);
+ m_actionEnd->setEnabled(hasChildren);
+ m_actionFindMove->setEnabled(! isGameOver);
+ m_actionGotoMove->setEnabled(! m_isRated &&
+ hasCurrentVariationOtherMoves(tree, current));
+ m_actionKeepOnlyPosition->setEnabled(! m_isRated
+ && (hasParent || hasChildren));
+ m_actionKeepOnlySubtree->setEnabled(hasParent && hasChildren);
+ m_actionGroupLevel->setEnabled(! m_isRated);
+ m_actionMakeMainVariation->setEnabled(! isMain);
+ m_actionMoveDownVariation->setEnabled(hasNextVar);
+ m_actionMoveUpVariation->setEnabled(hasPrevVar);
+ m_actionNew->setEnabled(! isEmpty);
+ m_actionNextVariation->setEnabled(hasNextVar);
+ if (! m_isGenMoveRunning)
+ {
+ m_actionNextPiece->setEnabled(! isGameOver);
+ m_actionPreviousPiece->setEnabled(! isGameOver);
+ m_actionPlay->setEnabled(! m_isRated && hasMoves);
+ m_actionPlaySingleMove->setEnabled(! m_isRated && hasMoves);
+ }
+ m_actionPreviousVariation->setEnabled(hasPrevVar);
+ m_actionRatedGame->setEnabled(! m_isRated);
+ m_actionSave->setEnabled(! m_file.isEmpty() && m_game.is_modified());
+ m_actionSaveAs->setEnabled(! isEmpty || m_game.is_modified());
+ // See also comment in setupMode()
+ m_actionSetupMode->setEnabled(! m_isRated && ! hasParent && ! hasChildren);
+ m_actionNextColor->setEnabled(! m_isRated);
+ m_actionTruncate->setEnabled(! m_isRated && hasParent);
+ m_actionTruncateChildren->setEnabled(hasChildren);
+ m_actionUndo->setEnabled(! m_isRated && hasParent && ! hasChildren
+ && hasMove);
+ m_actionGroupVariant->setEnabled(! m_isRated);
+ m_menuVariant->setEnabled(! m_isRated);
+ setTitleMenuLevel();
+}
+
+void MainWindow::updateWindowModified()
+{
+ if (! m_file.isEmpty())
+ setWindowModified(m_game.is_modified());
+}
+
+void MainWindow::variantTriggered(bool checked)
+{
+ if (checked)
+ setVariant(Variant(qobject_cast<QAction*>(sender())->data().toInt()));
+}
+
+void MainWindow::veryBadMove(bool checked)
+{
+ if (! checked)
+ return;
+ m_game.set_bad_move(2);
+ updateWindow(false);
+}
+
+void MainWindow::veryGoodMove(bool checked)
+{
+ if (! checked)
+ return;
+ m_game.set_good_move(2);
+ updateWindow(false);
+}
+
+void MainWindow::wheelEvent(QWheelEvent* event)
+{
+ int delta = event->delta() / 8 / 15;
+ if (delta > 0)
+ {
+ if (! m_guiBoard->getSelectedPiece().is_null())
+ for (int i = 0; i < delta; ++i)
+ nextTransform();
+ }
+ else if (delta < 0)
+ {
+ if (! m_guiBoard->getSelectedPiece().is_null())
+ for (int i = 0; i < -delta; ++i)
+ previousTransform();
+ }
+ event->accept();
+}
+
+bool MainWindow::writeGame(const string& file)
+{
+ ofstream out(file);
+ PentobiTreeWriter writer(out, m_game.get_tree());
+ writer.set_indent(1);
+ writer.write();
+ return static_cast<bool>(out);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/MainWindow.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_MAIN_WINDOW_H
+#define PENTOBI_MAIN_WINDOW_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QElapsedTimer>
+#include <QFutureWatcher>
+#include <QMainWindow>
+#include "RatingHistory.h"
+#include "libboardgame_util/RandomGenerator.h"
+#include "libpentobi_base/ColorMap.h"
+#include "libpentobi_base/Game.h"
+#include "libpentobi_mcts/Player.h"
+
+class QActionGroup;
+class QLabel;
+class QPlainTextEdit;
+class QSplitter;
+class AnalyzeGameWindow;
+class GuiBoard;
+class HelpWindow;
+class LeaveFullscreenButton;
+class OrientationDisplay;
+class PieceSelector;
+class RatingDialog;
+class ScoreDisplay;
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libboardgame_base::Transform;
+using libboardgame_util::ArrayList;
+using libboardgame_util::RandomGenerator;
+using libpentobi_base::Board;
+using libpentobi_base::ColorMap;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Game;
+using libpentobi_base::Move;
+using libpentobi_base::MoveList;
+using libpentobi_base::MoveMarker;
+using libpentobi_base::Piece;
+using libpentobi_base::Point;
+using libpentobi_base::Variant;
+using libpentobi_mcts::Player;
+
+//-----------------------------------------------------------------------------
+
+class MainWindow
+ : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(Variant variant, const QString& initialFile = "",
+ const QString& helpDir = "",
+ unsigned maxLevel = Player::max_supported_level,
+ const QString& booksDir = "", bool noBook = false,
+ unsigned nuThreads = 0);
+
+ ~MainWindow();
+
+ bool eventFilter(QObject* object, QEvent* event) override;
+
+ QSize sizeHint() const override;
+
+public slots:
+ void about();
+
+ void analyzeGame();
+
+ void backward();
+
+ void backToMainVariation();
+
+ void beginning();
+
+ void beginningOfBranch();
+
+ void clearPiece();
+
+ void computerColors();
+
+ void deleteAllVariations();
+
+ void end();
+
+ void exportAsciiArt();
+
+ void exportImage();
+
+ void findMove();
+
+ void findNextComment();
+
+ void flipHorizontally();
+
+ void flipVertically();
+
+ void forward();
+
+ void gotoMove();
+
+ /** Go to a node if a node with a position defined by a sequence of moves
+ still exists. */
+ void gotoPosition(Variant variant, const vector<ColorMove>& moves);
+
+ void help();
+
+ void gameInfo();
+
+ /** Abort current move generation and don't play a move. */
+ void interrupt();
+
+ /** Abort current move generation and play best move found so far. */
+ void interruptPlay();
+
+ void keepOnlyPosition();
+
+ void keepOnlySubtree();
+
+ void makeMainVariation();
+
+ void moveDownVariation();
+
+ void moveUpVariation();
+
+ void newGame();
+
+ void nextColor();
+
+ void nextVariation();
+
+ void nextPiece();
+
+ void nextTransform();
+
+ void open();
+
+ bool open(const QString& file, bool isTemporary = false);
+
+ void placePiece(Color c, Move mv);
+
+ void play();
+
+ void playSingleMove();
+
+ void pointClicked(Point p);
+
+ void previousPiece();
+
+ void previousTransform();
+
+ void previousVariation();
+
+ void ratedGame();
+
+ void rotateAnticlockwise();
+
+ void rotateClockwise();
+
+ void save();
+
+ void saveAs();
+
+ void selectPiece(Color c, Piece piece);
+
+ void selectPiece(Color c, Piece piece, const Transform* transform);
+
+ void setLevel(unsigned level);
+
+ void truncate();
+
+ void truncateChildren();
+
+ void undo();
+
+ void showToolbar(bool checked);
+
+ void showVariations(bool checked);
+
+ void showRating();
+
+ void setNoDelay();
+
+protected:
+ void closeEvent(QCloseEvent* event) override;
+
+ void wheelEvent(QWheelEvent* event) override;
+
+private:
+ struct GenMoveResult
+ {
+ bool playSingleMove;
+
+ Color color;
+
+ Move move;
+
+ unsigned genMoveId;
+ };
+
+ static const int maxRecentFiles = 9;
+
+ Game m_game;
+
+ const Board& m_bd;
+
+ unique_ptr<Player> m_player;
+
+ bool m_noDelay = false;
+
+ /** Was window maximized before entering fullscreen. */
+ bool m_wasMaximized = false;
+
+ bool m_isGenMoveRunning = false;
+
+ bool m_isAnalyzeRunning = false;
+
+ /** Should the computer generate a move if it is its turn?
+ Enabled on game start (if the computer plays at least one color)
+ or after selecting Play. Disabled when navigating in the game. */
+ bool m_autoPlay = false;
+
+ /** Flag indicating that the position after the last move played was
+ a terminal position. */
+ bool m_gameFinished;
+
+ bool m_isRated = false;
+
+ /** Flag set while setting the text in m_comment for fast return in the
+ textChanged() handler.
+ Used because QPlainTextEdit does not have a textEdited() signal and
+ we only need to handle edits. */
+ bool m_ignoreCommentTextChanged = false;
+
+ /** Color played by the user in a rated game.
+ Only defined if m_isRated is true. In game variants with multiple
+ colors per player, the user plays all colors of the player with
+ this color. */
+ Color m_ratedGameColor;
+
+ /** Integer ID assigned to the currently running move generation.
+ Used to ignore finished events from canceled move generations. */
+ unsigned m_genMoveId = 0;
+
+ unsigned m_maxLevel;
+
+ /** Current playing level of m_player.
+ Only use if m_useTimeLimit is false. Possible values for m_level are in
+ 1..maxLevel. Only used if m_timeLimit is zero. Stored independently of
+ the player and set at the player before each move generation, such that
+ setting a new level does not require to abort a running move
+ generation. */
+ unsigned m_level;
+
+ RandomGenerator m_random;
+
+ unique_ptr<RatingHistory> m_history;
+
+ /** Local variable in findMove().
+ Reused for efficiency. */
+ unique_ptr<MoveMarker> m_marker;
+
+ GuiBoard* m_guiBoard;
+
+ QString m_helpDir;
+
+ ColorMap<bool> m_computerColors;
+
+ ColorMap<PieceSelector*> m_pieceSelector;
+
+ OrientationDisplay* m_orientationDisplay;
+
+ ScoreDisplay* m_scoreDisplay;
+
+ QSplitter* m_splitter;
+
+ QPlainTextEdit* m_comment;
+
+ HelpWindow* m_helpWindow = nullptr;
+
+ RatingDialog* m_ratingDialog = nullptr;
+
+ AnalyzeGameWindow* m_analyzeGameWindow = nullptr;
+
+ QAction* m_actionAbout;
+
+ QAction* m_actionAnalyzeGame;
+
+ QAction* m_actionBackward;
+
+ QAction* m_actionBackToMainVariation;
+
+ QAction* m_actionBadMove;
+
+ QAction* m_actionBeginning;
+
+ QAction* m_actionBeginningOfBranch;
+
+ QAction* m_actionClearPiece;
+
+ QAction* m_actionComputerColors;
+
+ QAction* m_actionCoordinates;
+
+ QAction* m_actionDeleteAllVariations;
+
+ QAction* m_actionDoubtfulMove;
+
+ QAction* m_actionEnd;
+
+ QAction* m_actionExportAsciiArt;
+
+ QAction* m_actionExportImage;
+
+ QAction* m_actionFindMove;
+
+ QAction* m_actionFindNextComment;
+
+ QAction* m_actionFlipHorizontally;
+
+ QAction* m_actionFlipVertically;
+
+ QAction* m_actionForward;
+
+ QAction* m_actionFullscreen;
+
+ QAction* m_actionGameInfo;
+
+ QAction* m_actionGoodMove;
+
+ QAction* m_actionGotoMove;
+
+ QAction* m_actionHelp;
+
+ QAction* m_actionInterestingMove;
+
+ QAction* m_actionInterrupt;
+
+ QAction* m_actionInterruptPlay;
+
+ QAction* m_actionKeepOnlyPosition;
+
+ QAction* m_actionKeepOnlySubtree;
+
+ QAction* m_actionLeaveFullscreen;
+
+ QAction* m_actionMakeMainVariation;
+
+ QAction* m_actionMoveDownVariation;
+
+ QAction* m_actionMoveMarkingAllNumber;
+
+ QAction* m_actionMoveMarkingLastDot;
+
+ QAction* m_actionMoveMarkingLastNumber;
+
+ QAction* m_actionMoveMarkingNone;
+
+ QAction* m_actionMoveUpVariation;
+
+ QAction* m_actionMovePieceLeft;
+
+ QAction* m_actionMovePieceRight;
+
+ QAction* m_actionMovePieceUp;
+
+ QAction* m_actionMovePieceDown;
+
+ QAction* m_actionNextColor;
+
+ QAction* m_actionNextPiece;
+
+ QAction* m_actionNextTransform;
+
+ QAction* m_actionNextVariation;
+
+ QAction* m_actionNew;
+
+ QAction* m_actionRatedGame;
+
+ QAction* m_actionNoMoveAnnotation;
+
+ QAction* m_actionOpen;
+
+ QAction* m_actionPlacePiece;
+
+ QAction* m_actionPlay;
+
+ QAction* m_actionPlaySingleMove;
+
+ QAction* m_actionPreviousPiece;
+
+ QAction* m_actionPreviousTransform;
+
+ QAction* m_actionPreviousVariation;
+
+ QAction* m_actionQuit;
+
+ QAction* m_actionRecentFile[maxRecentFiles];
+
+ QAction* m_actionRotateAnticlockwise;
+
+ QAction* m_actionRotateClockwise;
+
+ QAction* m_actionSave;
+
+ QAction* m_actionSaveAs;
+
+ QAction* m_actionShowComment;
+
+ QAction* m_actionRating;
+
+ QAction* m_actionShowToolbar;
+
+ QAction* m_actionSetupMode;
+
+ QAction* m_actionToolBarNoText;
+
+ QAction* m_actionToolBarTextBesideIcons;
+
+ QAction* m_actionToolBarTextBelowIcons;
+
+ QAction* m_actionToolBarTextOnly;
+
+ QAction* m_actionToolBarTextSystem;
+
+ QAction* m_actionTruncate;
+
+ QAction* m_actionTruncateChildren;
+
+ QAction* m_actionShowVariations;
+
+ QAction* m_actionUndo;
+
+ QAction* m_actionVariantCallisto;
+
+ QAction* m_actionVariantCallisto2;
+
+ QAction* m_actionVariantCallisto3;
+
+ QAction* m_actionVariantClassic;
+
+ QAction* m_actionVariantClassic2;
+
+ QAction* m_actionVariantClassic3;
+
+ QAction* m_actionVariantDuo;
+
+ QAction* m_actionVariantJunior;
+
+ QAction* m_actionVariantNexos;
+
+ QAction* m_actionVariantNexos2;
+
+ QAction* m_actionVariantTrigon;
+
+ QAction* m_actionVariantTrigon2;
+
+ QAction* m_actionVariantTrigon3;
+
+ QAction* m_actionVeryGoodMove;
+
+ QAction* m_actionVeryBadMove;
+
+ QActionGroup* m_actionGroupLevel;
+
+ QActionGroup* m_actionGroupVariant;
+
+ QMenu* m_menuExport;
+
+ QMenu* m_menuLevel;
+
+ QMenu* m_menuMoveAnnotation;
+
+ QMenu* m_menuOpenRecent;
+
+ QMenu* m_menuToolBarText;
+
+ QMenu* m_menuVariant;
+
+ QLabel* m_setupModeLabel;
+
+ QLabel* m_ratedGameLabelText;
+
+ QFutureWatcher<GenMoveResult> m_genMoveWatcher;
+
+ QString m_file;
+
+ unique_ptr<MoveList> m_legalMoves;
+
+ unsigned m_legalMoveIndex;
+
+ QLabel* m_moveNumber;
+
+ LeaveFullscreenButton* m_leaveFullscreenButton = nullptr;
+
+ int m_lastRemainingSeconds;
+
+ int m_lastRemainingMinutes;
+
+ /** Is the current game a game loaded from the autosave file?
+ If yes, we need it to save again on quit even if it was not modified.
+ Note that the autosave game is deleted after loading to avoid that
+ it is used twice if two instances of Pentobi are started. */
+ bool m_isAutoSaveLoaded;
+
+
+ GenMoveResult asyncGenMove(Color c, int genMoveId, bool playSingleMove);
+
+ bool checkSave();
+
+ bool checkQuit();
+
+ void clearFile();
+
+ QAction* createAction(const QString& text = "");
+
+ QAction* createActionLevel(unsigned level, const QString& text);
+
+ void createActions();
+
+ QAction* createActionVariant(Variant variant, const QString& text);
+
+ QWidget* createCentralWidget();
+
+ QWidget* createLeftPanel();
+
+ void createMenu();
+
+ QLayout* createOrientationButtonBoxLeft();
+
+ QLayout* createOrientationButtonBoxRight();
+
+ QLayout* createOrientationSelector();
+
+ QLayout* createRightPanel();
+
+ void createToolBar();
+
+ void cancelThread();
+
+ void checkComputerMove();
+
+ void clearStatus();
+
+ bool computerPlaysAll() const;
+
+ void deleteAutoSaveFile();
+
+ void enablePieceSelector(Color c);
+
+ void gameOver();
+
+ void genMove(bool playSingleMove = false);
+
+ QString getFilter() const;
+
+ QString getLastDir();
+
+ QString getVersion() const;
+
+ void gotoNode(const SgfNode& node);
+
+ void gotoNode(const SgfNode* node);
+
+ void initGame();
+
+ void initVariantActions();
+
+ void initPieceSelectors();
+
+ bool isComputerToPlay() const;
+
+ void leaveSetupMode();
+
+ void play(Color c, Move mv);
+
+ void restoreLevel(Variant variant);
+
+ bool save(const QString& file);
+
+ void searchCallback(double elapsedSeconds, double remainingSeconds);
+
+ void setCommentText(const QString& text);
+
+ void setVariant(Variant variant);
+
+ void setPlayToolTip();
+
+ void setRated(bool isRated);
+
+ void setFile(const QString& file);
+
+ void showError(const QString& message, const QString& infoText = "",
+ const QString& detailText = "");
+
+ void showInfo(const QString& message, const QString& infoText = "",
+ const QString& detailText = "", bool withIcon = false);
+
+ void showInvalidFile(QString file, const exception& e);
+
+ void showStatus(const QString& text, bool temporary = false);
+
+ void updateMoveNumber();
+
+ void updateWindow(bool currentNodeChanged);
+
+ void updateWindowModified();
+
+ void updateComment();
+
+ void updateMoveAnnotationActions();
+
+ void loadHistory();
+
+ void updateRecentFiles();
+
+ void updateFlipActions();
+
+ bool writeGame(const string& file);
+
+private slots:
+ void analyzeGameFinished();
+
+ void badMove(bool checked);
+
+ void commentChanged();
+
+ void continueRatedGame();
+
+ void coordinates(bool checked);
+
+ void doubtfulMove(bool checked);
+
+ void fullscreen();
+
+ void genMoveFinished();
+
+ void goodMove(bool checked);
+
+ void interestingMove(bool checked);
+
+ void leaveFullscreen();
+
+ void levelTriggered(bool checked);
+
+ void noMoveAnnotation(bool checked);
+
+ void openCheckSave(const QString& file);
+
+ void openRecentFile();
+
+ void orientationDisplayColorClicked(Color c);
+
+ void rememberFile(const QString& file);
+
+ void rememberDir(const QString& file);
+
+ void selectNamedPiece();
+
+ void setMoveMarkingAllNumber(bool checked);
+
+ void setMoveMarkingLastNumber(bool checked);
+
+ void setMoveMarkingLastDot(bool checked);
+
+ void setMoveMarkingNone(bool checked);
+
+ void setSetupPlayer();
+
+ void setTitleMenuLevel();
+
+ void setupMode(bool checked);
+
+ void showComment(bool checked);
+
+ void toolBarNoText(bool checked);
+
+ void toolBarText(const QString& key, Qt::ToolButtonStyle style);
+
+ void toolBarTextBesideIcons(bool checked);
+
+ void toolBarTextBelowIcons(bool checked);
+
+ void toolBarTextOnly(bool checked);
+
+ void toolBarTextSystem(bool checked);
+
+ void veryBadMove(bool checked);
+
+ void veryGoodMove(bool checked);
+
+ void variantTriggered(bool checked);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_MAIN_WINDOW_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatedGamesList.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "RatedGamesList.h"
+
+#include <QHeaderView>
+#include <QKeyEvent>
+#include <QStandardItemModel>
+#include "libboardgame_util/Log.h"
+#include "libpentobi_gui/Util.h"
+
+//-----------------------------------------------------------------------------
+
+RatedGamesList::RatedGamesList(QWidget* parent)
+ : QTableView(parent)
+{
+ verticalHeader()->setVisible(false);
+ setShowGrid(false);
+ setEditTriggers(QAbstractItemView::NoEditTriggers);
+ setTabKeyNavigation(false);
+ setSelectionBehavior(QAbstractItemView::SelectRows);
+ setAlternatingRowColors(true);
+ m_model = new QStandardItemModel(this);
+ setModel(m_model);
+ connect(this, SIGNAL(doubleClicked(const QModelIndex&)),
+ SLOT(activateGame(const QModelIndex&)));
+}
+
+void RatedGamesList::activateGame(const QModelIndex& index)
+{
+ auto item = m_model->item(index.row(), 0);
+ if (! item)
+ return;
+ bool ok;
+ unsigned n = item->text().toUInt(&ok);
+ if (ok)
+ emit openRatedGame(n);
+}
+
+void RatedGamesList::focusInEvent(QFocusEvent* event)
+{
+ // Select current index if list has focus
+ selectRow(currentIndex().row());
+ scrollTo(currentIndex());
+ QTableView::focusInEvent(event);
+}
+
+void RatedGamesList::focusOutEvent(QFocusEvent* event)
+{
+ // Show selection only if list has focus
+ clearSelection();
+ QTableView::focusOutEvent(event);
+}
+
+void RatedGamesList::keyPressEvent(QKeyEvent* event)
+{
+ if (event->type() == QEvent::KeyPress
+ && static_cast<QKeyEvent*>(event)->key() == Qt::Key_Space)
+ {
+ QModelIndexList indexes =
+ selectionModel()->selection().indexes();
+ if (! indexes.isEmpty())
+ activateGame(indexes[0]);
+ return;
+ }
+ QTableView::keyPressEvent(event);
+}
+
+void RatedGamesList::updateContent(Variant variant,
+ const RatingHistory& history)
+{
+ m_model->clear();
+ QStringList headers;
+ headers << tr("Game") << tr("Your Color") << tr("Level") << tr("Result")
+ << tr("Date");
+ m_model->setHorizontalHeaderLabels(headers);
+ auto header = horizontalHeader();
+ header->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ header->setHighlightSections(false);
+ header->setSectionResizeMode(QHeaderView::ResizeToContents);
+ header->setStretchLastSection(true);
+ int nuRows = 0;
+ if (history.getGameInfos().size()
+ <= static_cast<size_t>(numeric_limits<int>::max()))
+ nuRows = static_cast<int>(history.getGameInfos().size());
+ m_model->setRowCount(nuRows);
+ setSortingEnabled(false);
+ for (int i = 0; i < nuRows; ++i)
+ {
+ auto& info = history.getGameInfos()[i];
+ auto number = new QStandardItem;
+ number->setData(info.number, Qt::DisplayRole);
+ auto color = new QStandardItem;
+ if (info.color.to_int() < get_nu_colors(variant))
+ color->setText(Util::getPlayerString(variant, info.color));
+ else
+ LIBBOARDGAME_LOG("Error: invalid color in rating history");
+ auto level = new QStandardItem;
+ level->setData(info.level, Qt::DisplayRole);
+ QString result;
+ if (info.result == 1)
+ result = tr("Win");
+ else if (info.result == 0.5)
+ result = tr("Tie");
+ else if (info.result == 0)
+ result = tr("Loss");
+ int row = nuRows - i - 1;
+ m_model->setItem(row, 0, number);
+ m_model->setItem(row, 1, color);
+ m_model->setItem(row, 2, level);
+ m_model->setItem(row, 3, new QStandardItem(result));
+ m_model->setItem(row, 4, new QStandardItem(info.date));
+ }
+ setSortingEnabled(true);
+ if (nuRows > 0)
+ selectionModel()->setCurrentIndex(model()->index(0, 0),
+ QItemSelectionModel::NoUpdate);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatedGamesList.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_RATED_GAMES_LIST
+#define PENTOBI_RATED_GAMES_LIST
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QTableView>
+#include "RatingHistory.h"
+
+class QStandardItemModel;
+
+//-----------------------------------------------------------------------------
+
+class RatedGamesList
+ : public QTableView
+{
+ Q_OBJECT
+
+public:
+ explicit RatedGamesList(QWidget* parent = nullptr);
+
+ void updateContent(Variant variant, const RatingHistory& history);
+
+signals:
+ void openRatedGame(unsigned n);
+
+protected:
+ void focusInEvent(QFocusEvent* event) override;
+
+ void focusOutEvent(QFocusEvent* event) override;
+
+ void keyPressEvent(QKeyEvent* event) override;
+
+private:
+ QStandardItemModel* m_model;
+
+private slots:
+ void activateGame(const QModelIndex& index);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_RATED_GAMES_LIST
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingDialog.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "RatingDialog.h"
+
+#include <QDialogButtonBox>
+#include <QFormLayout>
+#include <QFrame>
+#include <QLabel>
+#include <QMessageBox>
+#include <QPainter>
+#include <QPen>
+#include <QPushButton>
+#include <QSettings>
+#include <QVBoxLayout>
+#include "Util.h"
+
+//-----------------------------------------------------------------------------
+
+QLabel* createSelectableLabel()
+{
+ auto label = new QLabel;
+ label->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ return label;
+}
+
+//-----------------------------------------------------------------------------
+
+RatingDialog::RatingDialog(QWidget* parent, RatingHistory& history)
+ : QDialog(parent),
+ m_history(history)
+{
+ setWindowTitle(tr("Rating"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ auto layout = new QVBoxLayout;
+ setLayout(layout);
+ auto formLayout = new QFormLayout;
+ layout->addLayout(formLayout);
+ formLayout->setLabelAlignment(Qt::AlignLeft);
+ auto box = new QHBoxLayout;
+ m_labelRating = createSelectableLabel();
+ box->addWidget(m_labelRating);
+ box->addStretch();
+ formLayout->addRow(tr("Your rating:"), box);
+ m_labelVariant = createSelectableLabel();
+ formLayout->addRow(tr("Game variant:"), m_labelVariant);
+ m_labelNuGames = createSelectableLabel();
+ formLayout->addRow(tr("Number rated games:"), m_labelNuGames);
+ m_labelBestRating = createSelectableLabel();
+ formLayout->addRow(tr("Best previous rating:"), m_labelBestRating);
+ layout->addSpacing(layout->margin());
+ layout->addWidget(new QLabel(tr("Recent development:")));
+ m_graph = new RatingGraph;
+ layout->addWidget(m_graph, 1);
+ layout->addSpacing(layout->margin());
+ layout->addWidget(new QLabel(tr("Recent games:")));
+ m_list = new RatedGamesList;
+ layout->addWidget(m_list, 1);
+ auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
+ layout->addWidget(buttonBox);
+ m_clearButton =
+ buttonBox->addButton(tr("&Clear"), QDialogButtonBox::ActionRole);
+ buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
+ buttonBox->button(QDialogButtonBox::Close)->setAutoDefault(true);
+ buttonBox->button(QDialogButtonBox::Close)->setFocus();
+ updateContent();
+ connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
+ connect(buttonBox, SIGNAL(clicked(QAbstractButton*)),
+ SLOT(buttonClicked(QAbstractButton*)));
+ connect(m_list, SIGNAL(openRatedGame(unsigned)),
+ SLOT(activateGame(unsigned)));
+}
+
+void RatingDialog::activateGame(unsigned n)
+{
+ emit openRecentFile(m_history.getFile(n));
+}
+
+void RatingDialog::buttonClicked(QAbstractButton* button)
+{
+ if (button != static_cast<QAbstractButton*>(m_clearButton))
+ return;
+ QMessageBox msgBox(QMessageBox::Warning, "",
+ tr("Clear rating and delete rating history?"),
+ QMessageBox::Cancel, this);
+ Util::setNoTitle(msgBox);
+ auto clearButton =
+ msgBox.addButton(tr("Clear rating"), QMessageBox::DestructiveRole);
+ msgBox.setDefaultButton(clearButton);
+ msgBox.exec();
+ if (msgBox.clickedButton() != clearButton)
+ return;
+ m_history.clear();
+ updateContent();
+}
+
+void RatingDialog::updateContent()
+{
+ auto variant = m_history.getVariant();
+ unsigned nuGames = m_history.getNuGames();
+ Rating rating = m_history.getRating();
+ Rating bestRating = m_history.getBestRating();
+ if (nuGames == 0)
+ rating = Rating(0);
+ QString variantStr;
+ switch (variant)
+ {
+ case Variant::classic:
+ variantStr = tr("Classic (4 players)");
+ break;
+ case Variant::classic_2:
+ variantStr = tr("Classic (2 players)");
+ break;
+ case Variant::classic_3:
+ variantStr = tr("Classic (3 players)");
+ break;
+ case Variant::duo:
+ variantStr = tr("Duo");
+ break;
+ case Variant::trigon:
+ variantStr = tr("Trigon (4 players)");
+ break;
+ case Variant::trigon_2:
+ variantStr = tr("Trigon (2 players)");
+ break;
+ case Variant::trigon_3:
+ variantStr = tr("Trigon (3 players)");
+ break;
+ case Variant::junior:
+ variantStr = tr("Junior");
+ break;
+ case Variant::nexos:
+ variantStr = tr("Nexos (4 players)");
+ break;
+ case Variant::nexos_2:
+ variantStr = tr("Nexos (2 players)");
+ break;
+ case Variant::callisto:
+ variantStr = tr("Callisto (4 players)");
+ break;
+ case Variant::callisto_2:
+ variantStr = tr("Callisto (2 players)");
+ break;
+ case Variant::callisto_3:
+ variantStr = tr("Callisto (3 players)");
+ break;
+ }
+ m_labelVariant->setText(variantStr);
+ m_labelNuGames->setText(QString::number(nuGames));
+ if (nuGames == 0)
+ {
+ m_labelRating->setText("<b>--");
+ m_labelBestRating->setText("--");
+ }
+ else
+ {
+ m_labelRating->setText(QString("<b>%1").arg(rating.to_int()));
+ m_labelBestRating->setNum(bestRating.to_int());
+ }
+ m_graph->updateContent(m_history);
+ m_list->updateContent(variant, m_history);
+ m_clearButton->setEnabled(nuGames > 0);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingDialog.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_RATING_DIALOG_H
+#define PENTOBI_RATING_DIALOG_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QDialog>
+#include "RatedGamesList.h"
+#include "RatingGraph.h"
+#include "libpentobi_base/Variant.h"
+
+class QAbstractButton;
+class QLabel;
+
+using namespace std;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class RatingDialog
+ : public QDialog
+{
+ Q_OBJECT
+
+public:
+ /** Constructor.
+ @param parent
+ @param history (@ref libboardgame_doc_storesref) */
+ RatingDialog(QWidget* parent, RatingHistory& history);
+
+ void updateContent();
+
+signals:
+ void openRecentFile(const QString& file);
+
+private:
+ RatingHistory& m_history;
+
+ QPushButton* m_clearButton;
+
+ QLabel* m_labelVariant;
+
+ QLabel* m_labelNuGames;
+
+ QLabel* m_labelRating;
+
+ QLabel* m_labelBestRating;
+
+ RatingGraph* m_graph;
+
+ RatedGamesList* m_list;
+
+private slots:
+ void activateGame(unsigned n);
+
+ void buttonClicked(QAbstractButton*);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_RATING_DIALOG_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingGraph.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "RatingGraph.h"
+
+#include <QApplication>
+#include <QDesktopWidget>
+#include <QPainter>
+#include <QPen>
+
+//-----------------------------------------------------------------------------
+
+RatingGraph::RatingGraph(QWidget* parent)
+ : QFrame(parent)
+{
+ setMinimumSize(200, 60);
+ setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
+}
+
+void RatingGraph::paintEvent(QPaintEvent* event)
+{
+ QFrame::paintEvent(event);
+ QRect contentsRect = QFrame::contentsRect();
+ int width = contentsRect.width();
+ int height = contentsRect.height();
+ QPainter painter(this);
+ painter.translate(contentsRect.x(), contentsRect.y());
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(QColor(255, 255, 255));
+ painter.drawRect(0, 0, width, height);
+ if (! m_values.empty())
+ {
+ QFontMetrics metrics(painter.font());
+ float yRange = m_yMax - m_yMin;
+ float yTic = m_yMin;
+ float topMargin = ceil(1.2f * static_cast<float>(metrics.height()));
+ float bottomMargin = ceil(0.3f * static_cast<float>(metrics.height()));
+ float graphHeight =
+ static_cast<float>(height) - topMargin - bottomMargin;
+ QPen pen(QColor(96, 96, 96));
+ pen.setStyle(Qt::DotLine);
+ painter.setPen(pen);
+ int maxLabelWidth = 0;
+ while (yTic <= m_yMax)
+ {
+ int y =
+ static_cast<int>(round(
+ topMargin
+ + graphHeight - (yTic - m_yMin) / yRange * graphHeight));
+ painter.drawLine(0, y, width, y);
+ QString label;
+ label.setNum(yTic, 'f', 0);
+ int labelWidth = metrics.width(label + " ");
+ maxLabelWidth = max(maxLabelWidth, labelWidth);
+ painter.drawText(width - labelWidth, y - metrics.descent(),
+ label);
+ if (yRange < 600)
+ yTic += 100;
+ else
+ yTic += 200;
+ }
+ qreal dX = qreal(width - maxLabelWidth) / RatingHistory::maxGames;
+ qreal x = 0;
+ QPainterPath path;
+ for (unsigned i = 0; i < m_values.size(); ++i)
+ {
+ qreal y =
+ topMargin
+ + graphHeight - (m_values[i] - m_yMin) / yRange * graphHeight;
+ if (i == 0)
+ path.moveTo(x, y);
+ else
+ path.lineTo(x, y);
+ x += dX;
+ }
+ painter.setPen(Qt::red);
+ painter.setBrush(Qt::NoBrush);
+ painter.drawPath(path);
+ }
+}
+
+QSize RatingGraph::sizeHint() const
+{
+ auto geo = QApplication::desktop()->screenGeometry();
+ return QSize(geo.width() / 3, min(geo.width() / 12, geo.height() / 3));
+}
+
+void RatingGraph::updateContent(const RatingHistory& history)
+{
+ m_values.clear();
+ auto& games = history.getGameInfos();
+ if (games.empty())
+ {
+ update();
+ return;
+ }
+ m_yMin = games[0].rating.get();
+ m_yMax = m_yMin;
+ for (const RatingHistory::GameInfo& info : games)
+ {
+ float rating = info.rating.get();
+ m_yMin = min(m_yMin, rating);
+ m_yMax = max(m_yMax, rating);
+ m_values.push_back(rating);
+ }
+ m_yMin = floor((m_yMin / 100.f)) * 100;
+ m_yMax = ceil((m_yMax / 100.f)) * 100;
+ if (m_yMax == m_yMin)
+ m_yMax = m_yMin + 100;
+ update();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingGraph.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_RATING_GRAPH_H
+#define PENTOBI_RATING_GRAPH_H
+
+// Needed in the header because moc_*.cxx does not include config.h
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QFrame>
+#include "RatingHistory.h"
+
+//-----------------------------------------------------------------------------
+
+class RatingGraph
+ : public QFrame
+{
+ Q_OBJECT
+
+public:
+ explicit RatingGraph(QWidget* parent = nullptr);
+
+ void updateContent(const RatingHistory& history);
+
+ QSize sizeHint() const override;
+
+protected:
+ void paintEvent(QPaintEvent* event) override;
+
+private:
+ float m_yMin;
+
+ float m_yMax;
+
+ vector<float> m_values;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_RATING_GRAPH_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingHistory.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "RatingHistory.h"
+
+#include <fstream>
+#include <sstream>
+#include <QDir>
+#include <QFile>
+#include <QSettings>
+#include <QString>
+#include "Util.h"
+#include "libpentobi_base/PentobiTreeWriter.h"
+#include "libpentobi_mcts/Player.h"
+
+using libpentobi_base::to_string_id;
+using libpentobi_base::PentobiTreeWriter;
+using libpentobi_mcts::Player;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** 1000 Elo represents a beginner level. */
+const float startRating = 1000;
+
+QString getRatedGamesDir(Variant variant)
+{
+ return
+ Util::getDataDir() + "/rated_games/" + QString(to_string_id(variant));
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+RatingHistory::RatingHistory(Variant variant)
+{
+ load(variant);
+}
+
+void RatingHistory::addGame(float score, Rating opponentRating,
+ unsigned nuOpponents, Color color,
+ float result, const QString& date, int level,
+ const PentobiTree& tree)
+{
+ float kValue = (m_nuGames < 30 ? 40.f : 20.f);
+ m_rating.update(score, opponentRating, kValue, nuOpponents);
+ if (m_rating.get() > m_bestRating.get())
+ m_bestRating = m_rating;
+ ++m_nuGames;
+ GameInfo info;
+ info.number = m_nuGames;
+ info.color = color;
+ info.result = result;
+ info.date = date;
+ info.level = level;
+ info.rating = m_rating;
+ m_games.push_back(info);
+ size_t nuGames = m_games.size();
+ if (nuGames > maxGames)
+ m_games.erase(m_games.begin(), m_games.begin() + nuGames - maxGames);
+ save();
+ ofstream out(getFile(m_nuGames).toLocal8Bit().constData());
+ PentobiTreeWriter writer(out, tree);
+ writer.set_indent(1);
+ writer.write();
+ // Only save the last RatingHistory::maxGames games
+ if (m_nuGames > maxGames)
+ QFile::remove(getFile(m_nuGames - maxGames));
+}
+
+void RatingHistory::clear()
+{
+ QString variantStr = QString(to_string_id(m_variant));
+ QSettings settings;
+ settings.remove("rated_games_" + variantStr);
+ settings.remove("rating_" + variantStr);
+ settings.remove("best_rating_" + variantStr);
+ for (const RatingHistory::GameInfo& info : getGameInfos())
+ QFile::remove(getFile(info.number));
+ QFile::remove(m_file);
+ m_nuGames = 0;
+ m_rating = Rating(startRating);
+ m_bestRating = Rating(startRating);
+ m_games.clear();
+}
+
+QString RatingHistory::getFile(unsigned n) const
+{
+ return QString("%1/%2.blksgf").arg(m_dir, QString::number(n));
+}
+
+void RatingHistory::getNextRatedGameSettings(int maxLevel, unsigned random,
+ int& level, Color& color)
+{
+ color =
+ Color(static_cast<Color::IntType>(random % get_nu_players(m_variant)));
+ float minDiff = 0; // Initialize to avoid compiler warning
+ for (int i = 1; i <= maxLevel; ++i)
+ {
+ float diff =
+ abs(m_rating.get() - Player::get_rating(m_variant, i).get());
+ if (i == 1 || diff < minDiff)
+ {
+ minDiff = diff;
+ level = i;
+ }
+ }
+}
+
+void RatingHistory::init(Rating rating)
+{
+ m_rating = rating;
+ m_bestRating = rating;
+ m_nuGames = 0;
+ m_games.clear();
+ save();
+}
+
+void RatingHistory::load(Variant variant)
+{
+ m_variant = variant;
+ QString variantStr = QString(to_string_id(variant));
+ QSettings settings;
+ m_nuGames = settings.value("rated_games_" + variantStr, 0).toUInt();
+ // Default value is 1000 (Elo-rating for beginner-level play)
+ m_rating =
+ Rating(settings.value("rating_" + variantStr, startRating).toFloat());
+ m_bestRating =
+ Rating(settings.value("best_rating_" + variantStr, 0).toFloat());
+ m_games.clear();
+ m_dir = getRatedGamesDir(variant);
+ m_file = m_dir + "/history.dat";
+ ifstream file(m_file.toLocal8Bit().constData());
+ if (! file)
+ return;
+ string line;
+ while (getline(file, line) && m_games.size() < maxGames)
+ {
+ istringstream in(line);
+ GameInfo info;
+ unsigned c;
+ string date;
+ in >> info.number >> c >> info.result >> date >> info.level
+ >> info.rating;
+ info.date = QString(date.c_str());
+ if (! in || c >= get_nu_colors(variant))
+ return;
+ info.color = Color(static_cast<Color::IntType>(c));
+ if (info.number >= 1 && info.number <= m_nuGames)
+ m_games.push_back(info);
+ }
+ size_t nuGames = m_games.size();
+ if (nuGames > maxGames)
+ m_games.erase(m_games.begin(), m_games.begin() + nuGames - maxGames);
+ // Make the all-time best rating consistent with the rating history. Older
+ // versions of Pentobi (up to version 3) did not save the all-time best
+ // rating, so after an upgrade to a newer version of Pentobi, the history
+ // of recent rated games can contain a higher rating than the stored
+ // all-time best rating.
+ for (const RatingHistory::GameInfo& info : getGameInfos())
+ if (info.rating.get() > m_bestRating.get())
+ m_bestRating = info.rating;
+}
+
+void RatingHistory::save() const
+{
+ QString variantStr = QString(to_string_id(m_variant));
+ QSettings settings;
+ settings.setValue("rated_games_" + variantStr, m_nuGames);
+ settings.setValue("rating_" + variantStr,
+ static_cast<double>(m_rating.get()));
+ settings.setValue("best_rating_" + variantStr,
+ static_cast<double>(m_bestRating.get()));
+ LIBBOARDGAME_ASSERT(! m_file.isEmpty());
+ QDir dir("");
+ dir.mkpath(m_dir);
+ ofstream out(m_file.toLocal8Bit().constData());
+ for (auto& info : m_games)
+ out << info.number << ' ' << static_cast<unsigned>(info.color.to_int())
+ << ' ' << info.result << ' '
+ << info.date.toLocal8Bit().constData() << ' ' << info.level
+ << ' ' << info.rating << '\n';
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingHistory.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_RATING_HISTORY_H
+#define PENTOBI_RATING_HISTORY_H
+
+#include <vector>
+#include <QString>
+#include "libboardgame_base/Rating.h"
+#include "libpentobi_base/Color.h"
+#include "libpentobi_base/PentobiTree.h"
+#include "libpentobi_base/Variant.h"
+
+using namespace std;
+using libboardgame_base::Rating;
+using libpentobi_base::Color;
+using libpentobi_base::PentobiTree;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** History of rated games in a certain game variant. */
+class RatingHistory
+{
+public:
+ /** Maximum number of games to remember in the history. */
+ static const unsigned maxGames = 100;
+
+ struct GameInfo
+ {
+ /** Game number.
+ The first game played has number 0. */
+ unsigned number;
+
+ /** Color played by the human.
+ In game variants with multiple colors per player, the human played
+ all colors played by the player of this color. */
+ Color color;
+
+ /** Game result.
+ 0=Loss, 0.5=tie, 1=win from the viewpoint of the human. */
+ float result;
+
+ /** Date of the game in "YYYY-MM-DD" format. */
+ QString date;
+
+ /** The playing level of the computer opponent. */
+ int level;
+
+ /** The rating of the human after the game. */
+ Rating rating;
+ };
+
+
+ explicit RatingHistory(Variant variant);
+
+ /** Initialize rating to a given a-priori value. */
+ void init(Rating rating);
+
+ /** Get level and user color for next rated games.
+ @param maxLevel The maximum playing level.
+ @param random A random number to determine the color for the human.
+ @param[out] level The playing level for the next game.
+ @param[out] color The color for the human in the next game. */
+ void getNextRatedGameSettings(int maxLevel, unsigned random, int& level,
+ Color& color);
+
+ /** Append a new game. */
+ void addGame(float score, Rating opponentRating, unsigned nuOpponents,
+ Color color, float result, const QString& date, int level,
+ const PentobiTree& tree);
+
+ /** Get file name of the n'th rated game. */
+ QString getFile(unsigned n) const;
+
+ void load(Variant variant);
+
+ /** Saves the history. */
+ void save() const;
+
+ const vector<GameInfo>& getGameInfos() const;
+
+ Variant getVariant() const;
+
+ const Rating& getRating() const;
+
+ const Rating& getBestRating() const;
+
+ unsigned getNuGames() const;
+
+ void clear();
+
+private:
+ Variant m_variant;
+
+ Rating m_rating;
+
+ unsigned m_nuGames;
+
+ Rating m_bestRating;
+
+ QString m_dir;
+
+ QString m_file;
+
+ vector<GameInfo> m_games;
+};
+
+inline const vector<RatingHistory::GameInfo>& RatingHistory::getGameInfos()
+ const
+{
+ return m_games;
+}
+
+inline unsigned RatingHistory::getNuGames() const
+{
+ return m_nuGames;
+}
+
+inline const Rating& RatingHistory::getBestRating() const
+{
+ return m_bestRating;
+}
+
+inline const Rating& RatingHistory::getRating() const
+{
+ return m_rating;
+}
+
+inline Variant RatingHistory::getVariant() const
+{
+ return m_variant;
+}
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_RATING_HISTORY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/ShowMessage.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ShowMessage.h"
+
+#include <QMessageBox>
+#include "Util.h"
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void showMessage(QWidget* parent, QMessageBox::Icon icon, const QString& text,
+ const QString& infoText, const QString& detailText)
+{
+ QMessageBox msgBox(parent);
+ Util::setNoTitle(msgBox);
+ msgBox.setIcon(icon);
+ msgBox.setText(text);
+ msgBox.setInformativeText(infoText);
+ msgBox.setDetailedText(detailText);
+ msgBox.exec();
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+void initQuestion(QMessageBox& msgBox, const QString& text,
+ const QString& infoText)
+{
+ Util::setNoTitle(msgBox);
+ msgBox.setText(text);
+ msgBox.setInformativeText(infoText);
+}
+
+void showFatal(const QString& detailedText)
+{
+ // Don't translate these error messages. They shouldn't occur if the
+ // program is correct and if it is not, they can occur in situations
+ // when the translators are not yet installed.
+ QMessageBox msgBox;
+ msgBox.setWindowTitle("Pentobi");
+ msgBox.setIcon(QMessageBox::Critical);
+ msgBox.setText("An unexpected error occurred.");
+ QString infoText =
+ "Please report this error together with any details available with"
+ " the button below and other context information at the Pentobi"
+ " <a href=\"http://sf.net/p/pentobi/bugs\">bug tracker</a>.";
+ msgBox.setInformativeText("<html>" + infoText);
+ msgBox.setDetailedText(detailedText);
+ msgBox.exec();
+}
+
+void showError(QWidget* parent, const QString& text, const QString& infoText,
+ const QString& detailText)
+{
+ showMessage(parent,QMessageBox::Critical, text, infoText, detailText);
+}
+
+void showInfo(QWidget* parent, const QString& text, const QString& infoText,
+ const QString& detailText, bool withIcon)
+{
+ showMessage(parent,
+ withIcon ? QMessageBox::Information : QMessageBox::NoIcon,
+ text, infoText, detailText);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/ShowMessage.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_SHOW_MESSAGE_H
+#define PENTOBI_SHOW_MESSAGE_H
+
+#include <QString>
+
+class QMessageBox;
+class QWidget;
+
+//-----------------------------------------------------------------------------
+
+void initQuestion(QMessageBox& msgBox, const QString& text,
+ const QString& infoText = "");
+
+void showError(QWidget* parent, const QString& text,
+ const QString& infoText = "", const QString& detailText = "");
+
+void showInfo(QWidget* parent, const QString& text,
+ const QString& infoText = "", const QString& detailText = "",
+ bool withIcon = false);
+
+void showFatal(const QString& detailedText);
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_SHOW_MESSAGE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/Util.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Util.h"
+
+#include <QCryptographicHash>
+#include <QDialog>
+#include <QDir>
+#include <QFileInfo>
+#include <QStandardPaths>
+#include <QString>
+#include <QUrl>
+#include "libpentobi_mcts/Player.h"
+
+using libpentobi_mcts::Player;
+
+//-----------------------------------------------------------------------------
+
+namespace Util
+{
+
+QString getDataDir()
+{
+ return QStandardPaths::writableLocation(QStandardPaths::DataLocation);
+}
+
+void initDataDir()
+{
+ QString dataLocation = getDataDir();
+ QDir dir(dataLocation);
+ if (! dir.exists())
+ // Note: dataLocation is an absolute path but there is no static
+ // function QDir::mkpath()
+ dir.mkpath(dataLocation);
+}
+
+void removeThumbnail(const QString& file)
+{
+ // Note: in the future, it might be possible to trigger a thumbnail
+ // update via D-Bus instead of removing it, but this is not yet
+ // implemented in Gnome
+ QFileInfo info(file);
+ QString canonicalFile = info.canonicalFilePath();
+ if (canonicalFile.isEmpty())
+ canonicalFile = info.absoluteFilePath();
+ QByteArray url = QUrl::fromLocalFile(canonicalFile).toEncoded();
+ QByteArray md5 =
+ QCryptographicHash::hash(url, QCryptographicHash::Md5).toHex();
+ QString home = QDir::home().path();
+ QFile::remove(home + "/.thumbnails/normal/" + md5 + ".png");
+ QFile::remove(home + "/.thumbnails/large/" + md5 + ".png");
+}
+
+void setNoTitle(QDialog& dialog)
+{
+ // On many platforms, message boxes should have no title but using
+ // an emtpy string causes Qt to use the lower-case application name (tested
+ // on Linux with Qt 4.8). As a workaround, we set the title to a space
+ // character.
+ dialog.setWindowTitle(" ");
+}
+
+} // namespace Util
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/Util.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_UTIL_H
+#define PENTOBI_UTIL_H
+
+#include "RatingHistory.h"
+#include "libboardgame_base/Rating.h"
+#include "libpentobi_base/Color.h"
+#include "libpentobi_base/Variant.h"
+
+class QDialog;
+class QString;
+
+using libboardgame_base::Rating;
+using libpentobi_base::Color;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+namespace Util
+{
+
+/** Remove a thumbnail for a given file.
+ Currently, the QT open file dialog shows thumbnails even if they belong
+ to old versions of a file (see QTBUG-24724). This function can be used
+ to remove an out-of-date freedesktop.org thumbnail if we know a file has
+ changed (e.g. after saving). */
+void removeThumbnail(const QString& file);
+
+/** Return the platform-dependent directory for storing data for the current
+ application. */
+QString getDataDir();
+
+/** Create the platform-dependent directory for storing data for the current
+ application if it does not exist yet. */
+void initDataDir();
+
+/** Set an empty window title for message boxes and similar small dialogs. */
+void setNoTitle(QDialog& dialog);
+
+}
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_UTIL_H
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="user_interface.html">Previous</a> | <a href=
+"window_menu.html">Next</a></p>
+<h2>Become a Stronger Player</h2>
+<p>Pentobi has functionality that can help you to become a stronger Blokus
+player.</p>
+<h3 id="analysis">Game Analysis</h3>
+<p>A game can be analyzed by selecting <i>Analyze Game</i> from the
+<i>Tools</i> menu. This will make the computer player evaluate each position in
+the main variation. The result is displayed in a window with a diagram of
+colored dots.</p>
+<p align="center"><img src="analysis.jpg" alt="Game analysis window"></p>
+<div align="center" class="caption">Analysis of a game of variant Classic (2
+players).</div>
+<p>Each dot represents a game position in which the color of the dot was to
+play. The dots are ordered horizontally by move number. The vertical axis
+represents the estimated probability of winning the game for the color to play.
+Mouse clicks in the diagram will go to the corresponding position.</p>
+<p>The position values are only estimates and the computer will sometimes
+evaluate positions incorrectly. But sudden drops in the value can help you find
+moves that were potentially bad. You can go back to the position before the
+move and try to find a better move or ask the computer what it would have
+played by selecting <i>Play Single Move</i> from the <i>Computer</i> menu.</p>
+<h3 id="rating">Determine Your Rating</h3>
+<p>You can track your progress by playing rated games against the computer. The
+game results are used to determine your current rating. The rating is a number
+that represents your playing strength.</p>
+<p>A rated game is started with <i>Rated Game</i> from the <i>Game</i> menu or
+the toolbar. If you have not played any rated games in the current game
+variant, you will be asked to choose a start value, which can reduce the number
+of games needed for determining your real rating. If you are a beginner, leave
+the start value at 1000.</p>
+<p>For each rated game, the computer will choose a playing level for the
+computer opponent according to your current rating. The color you play will be
+randomly chosen in each game.</p>
+<p>During a rated game, most of the functions not needed for playing are
+disabled: you cannot undo moves, navigate in the game, change the computer
+colors or change the playing level. To get an accurate rating, you should
+always play rated games until the end.</p>
+<p>After the game has ended, your rating will be updated depending on the game
+result and the computer level. For the game result, it only matters if the game
+was won, lost or a tie. The exact number of score points does not matter.</p>
+<p align="center"><img src="rating.jpg" alt="Rating window"></p>
+<div align="center" class="caption">Window with rating graph.</div>
+<p>You can always see your current rating by selecting <i>Rating</i> from the
+<i>Tools</i> menu. This will open a window that shows the development of your
+rating during the last 100 games as a graph. The last 100 games are
+automatically saved and can be loaded by double-clicking on the rows in the
+game table below the graph.</p>
+<p align="right"><a href="user_interface.html">Previous</a> | <a href=
+"window_menu.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="nexos_rules.html">Previous</a> | <a href=
+"user_interface.html">Next</a></p>
+<h2>Callisto Rules</h2>
+<p>Callisto is a another board game similar to Blokus. The board is derived
+from the classic 20×20 Blokus board by removing the corners such that an
+octagon with a top edge of size six remains. The pieces are a subset of the
+polyominoes up to size five. They include three 1×1 pieces per player that play
+a special role.</p>
+<p align="center"><img src="pieces_callisto.png" alt=
+"Pieces for game variant Callisto"></p>
+<div align="center" class="caption">The 21 pieces.</div>
+<p>The 1×1 pieces may be placed anywhere on the board apart from the center of
+the board. The center consists of an octagon with width six and top edge size
+two. The first two moves of a player must use a 1×1 piece, the third 1×1 piece
+may be played anytime later.</p>
+<p align="center"><img src="board_callisto.png" alt=
+"Board for game variant Callisto"></p>
+<div align="center" class="caption">The board with the center having a darker
+color.</div>
+<p>All larger pieces may be placed anwhere on the board but must touch an
+existing piece of the same color edge-to-edge.</p>
+<p align="center"><img src="position_callisto.png" alt=
+"Example position for game variant Callisto"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<p>The score of a color is the number of squares on the board occupied by the
+color not counting 1×1 pieces. Bonus points are not used. Unlike in Blokus,
+ties are broken in favor of the player who started later.</p>
+<h3>Rules for two or three players</h3>
+<p>The game can be played with less than four players by using a smaller board.
+For three players, the board is an octagon with width 20 and top edge size two.
+For two players, the board is an octagon with width 17 and top edge size two.
+The size of the center stays the same.</p>
+<p align="right"><a href="nexos_rules.html">Previous</a> | <a href=
+"user_interface.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="index.html">Previous</a> | <a href=
+"duo_rules.html">Next</a></p>
+<h2>Classic Rules</h2>
+<p>There are four players, Blue, Yellow, Red and Green, and a board consisting
+of 20×20 squares.</p>
+<p>Each player has a set of 21 pieces of his color shaped like the polyominoes
+up to size five. (A polyomino is a shape built by a number of squares connected
+along the edges.)</p>
+<p align="center"><img src="pieces.png" alt=
+"Pieces for game variant Classic"></p>
+<div align="center" class="caption">The 21 pieces.</div>
+<p>The players alternate in placing one of their pieces on the board. Blue
+starts, followed by Yellow, then Red, then Green.</p>
+<p>Each player has a starting square. Blue's starting square is in the top left
+corner, Yellow's in the top right corner, Red's in the bottom right corner and
+Green's in the bottom left corner. The first piece of a player must cover its
+starting square.</p>
+<p align="center"><img src="board_classic.png" alt=
+"Board for game variant Classic"></p>
+<div align="center" class="caption">The 20×20 board with the starting<br>
+squares marked with colored dots.</div>
+<p>The following pieces must be placed on empty squares such that the new piece
+touches at least one piece of its own color corner-to-corner but does not touch
+any piece of its own color along the edges. The new piece may touch edges of
+pieces of the opponent colors.</p>
+<p align="center"><img src="position_classic.png" alt=
+"Example position for game variant Classic"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<p>When the player of a color cannot place any more pieces, the player passes
+and the next color continues.</p>
+<p>When none of the players can place any more pieces, the player with the
+highest score wins. The score of a color is the number of squares on the board
+occupied by the color, plus a bonus of 15 points if the color could place all
+of its pieces, plus an additional bonus of 5 points if the color could place
+all pieces and the last piece played was the one-square piece.</p>
+<h3>Rules for Two Players</h3>
+<p>The game can be played with two players. The first player plays both Blue
+and Red, the second player Yellow and Green. The points of both colors played
+by a player are added up.</p>
+<h3>Rules for Three Players</h3>
+<p>The game can also be played with three players. The players take turns
+playing the fourth color (Green). At the end of the game, the score of Green is
+ignored.</p>
+<h3>Colorless starting points</h3>
+<p>Note that the original Blokus Classic rules used colorless starting points.
+This means that each color may freely choose, which of the remaining unoccupied
+starting points to use for its first move. Pentobi currently only supports the
+rule variant with colored starting points because this rule variant was used on
+the Blokus online server at blokus.com and in most of the past Blokus
+tournaments.</p>
+<p align="right"><a href="index.html">Previous</a> | <a href=
+"duo_rules.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Previous</a> | <a href=
+"trigon_rules.html">Next</a></p>
+<h2>Duo Rules</h2>
+<p>The game variant Duo is another game variant for two players. The game is
+played on a smaller board with 14×14 squares. There is only one color per
+player (Blue and Green) and the starting squares are not in the corners, but on
+the square with the coordinates (5,10) for Blue, and on (10,5) for Green.</p>
+<p align="center"><img src="board_duo.png" alt=
+"Board for game variant Duo"></p>
+<div align="center" class="caption">The 14×14 board used in game variant Duo
+with<br>
+the starting squares marked with colored dots.</div>
+<p align="center"><img src="position_duo.png" alt=
+"Example position for game variant Duo"></p>
+<div align="center" class="caption">An example position in game variant
+Duo.</div>
+<p align="right"><a href="classic_rules.html">Previous</a> | <a href=
+"trigon_rules.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Next</a></p>
+<h1>Pentobi</h1>
+<p>Pentobi is a computer opponent for the board game Blokus. In this game, four
+players place pieces similar to the pieces of the computer game Tetris on a
+20×20 board. Pentobi also supports the game variants for two or three players
+and the game variants Duo, Trigon, Junior, Nexos and Callisto.</p>
+<p><a href="classic_rules.html">Classic Rules</a><br>
+<a href="duo_rules.html">Duo Rules</a><br>
+<a href="trigon_rules.html">Trigon Rules</a><br>
+<a href="junior_rules.html">Junior Rules</a><br>
+<a href="nexos_rules.html">Nexos Rules</a><br>
+<a href="callisto_rules.html">Callisto Rules</a><br>
+<a href="user_interface.html">How to Use Pentobi</a><br>
+<a href="become_stronger.html">Become a Stronger Player</a><br>
+<a href="window_menu.html">The Window Menu</a><br>
+<a href="shortcuts.html">Keyboard Shortcuts</a><br>
+<a href="system.html">System Requirements</a><br>
+<a href="license.html">License</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="trigon_rules.html">Previous</a> | <a href=
+"nexos_rules.html">Next</a></p>
+<h2>Junior Rules</h2>
+<p>Junior is a simplified game variant for two players. It is played on the
+same 14×14 board as game variant Duo but uses only a subset of the pentominoes
+and the players get two of each of those pentominoes.</p>
+<p align="center"><img src="pieces_junior.png" alt=
+"Pieces for game variant Junior"></p>
+<div align="center" class="caption">The 24 pieces used in Junior.</div>
+<p>Bonus points are not used in Junior.</p>
+<p align="right"><a href="trigon_rules.html">Previous</a> | <a href=
+"nexos_rules.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="system.html">Previous</a></p>
+<h2>License</h2>
+<p>Copyright © 2011–2017 Markus Enzenberger</p>
+<p>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 3 of the License, or (at your option) any
+later version.</p>
+<p>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.</p>
+<h3>Trademark Disclaimer</h3>
+<p>The trademark Blokus and other trademarks referred to are property of their
+respective trademark holders. The trademark holders are not affiliated with the
+author of the program Pentobi.</p>
+<p align="right"><a href="system.html">Previous</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="junior_rules.html">Previous</a> | <a href=
+"callisto_rules.html">Next</a></p>
+<h2>Nexos Rules</h2>
+<p>Nexos is a board game similar to Blokus. The board is a rectangular 13×13
+line grid. Each color uses 24 pieces that consist of up to four connected line
+segments.</p>
+<p align="center"><img src="pieces_nexos.png" alt="Pieces for Nexos"></p>
+<div align="center" class="caption">The 24 pieces.</div>
+<p>Each color has a starting intersection on the intersection of the third
+lines close to a corner. The first piece must touch the starting
+intersection.</p>
+<p align="center"><img src="board_nexos.png" alt="Board for Nexos"></p>
+<div align="center" class="caption">The board for Nexos with segments touching
+the<br>
+starting intersections marked with colored dots.</div>
+<p>The following pieces must be placed on empty line segments such that a
+segment of the new piece touches an intersection that is already touched by a
+segment of the same color. It does not matter if pieces of other colors touch
+or cover the same intersection. However, pieces may not overlap. The junctions
+between the segments within a piece are such that two rectangular junctions of
+different pieces can cover the same intersection without overlapping, but
+straight junctions cannot.</p>
+<p align="center"><img src="position_nexos.png" alt=
+"Example position for Nexos"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<p>The score of a color is the number of line segments on the board covered by
+the color, plus a bonus of 10 points if the color could place all of its
+pieces.</p>
+<h3>Rules for Two Players</h3>
+<p>Like Blokus, Nexos can be played with two players by having one player play
+Blue and Red and the other player Yellow and Green.</p>
+<p align="right"><a href="junior_rules.html">Previous</a> | <a href=
+"callisto_rules.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="window_menu.html">Previous</a> | <a href=
+"system.html">Next</a></p>
+<h2>Keyboard Shortcuts</h2>
+<p>In addition to the menu item shortcut keys, which are shown in the window
+menu, the following shortcut keys are supported by Pentobi. Note that these
+shortcuts are not active when the comment text field is shown and has the
+focus. In this case, the focus can be switched away from the comment text with
+the Tab key.</p>
+<dl>
+<dt>Plus</dt>
+<dd>
+<p>Select next piece</p>
+</dd>
+<dt>Minus</dt>
+<dd>
+<p>Select previous piece</p>
+</dd>
+<dt>0</dt>
+<dd>
+<p>Clear selected piece</p>
+</dd>
+<dt>Space</dt>
+<dd>
+<p>Next orientation of the selected piece</p>
+</dd>
+<dt>Shift+Space</dt>
+<dd>
+<p>Previous orientation of the selected piece</p>
+</dd>
+<dt>Left, Right, Up, Down</dt>
+<dd>
+<p>Move the selected piece.</p>
+</dd>
+<dt>Enter</dt>
+<dd>
+<p>Play the selected piece.</p>
+</dd>
+<dt>1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z</dt>
+<dd>
+<p>Select piece according to commonly used piece names. If there are multiple
+pieces with the letter (e.g. I3, I4, I5), pressing the key several times cycles
+between them. Some letters are used only in certain game variants. For example,
+A is used only in Trigon for the pieces A6 and A4 (also known as "lobster" and
+"triangle").</p>
+</dd>
+</dl>
+<p align="right"><a href="window_menu.html">Previous</a> | <a href=
+"system.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+body
+{
+ color: black;
+ background-color: white;
+ font-family: sans-serif;
+ font-size: 15px;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ max-width: 60em;
+}
+
+:link
+{
+ text-decoration: none;
+}
+
+div.caption
+{
+ font-size: 14px;
+}
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="shortcuts.html">Previous</a> | <a href=
+"license.html">Next</a></p>
+<h2>System Requirements</h2>
+<p>Minimum: 1 GB RAM, 1 GHz CPU<br>
+Recommended for playing level 9: 4 GB RAM, 2 GHz dual-core or faster
+CPU</p>
+<p>Pentobi will also work on systems that do not meet the minimum requirements
+but the highest playing level will be very slow on those systems (if the CPU is
+too slow) or have a reduced playing strength (if there is not enough
+memory).</p>
+<p align="right"><a href="shortcuts.html">Previous</a> | <a href=
+"license.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="duo_rules.html">Previous</a> | <a href=
+"junior_rules.html">Next</a></p>
+<h2>Trigon Rules</h2>
+<p>Trigon is another game variant. The rules a similar to game variant Classic
+but it uses a differently shaped board and a different set of pieces. Each
+color uses 22 pieces that are shaped like the polyiamonds up to size six. (A
+polyiamond is a shape built by a number of equilateral triangles connected
+along the edges.)</p>
+<p align="center"><img src="pieces_trigon.jpg" alt=
+"Pieces for game variant Trigon"></p>
+<div align="center" class="caption">The 22 Trigon pieces.</div>
+<p>The board also consists of triangles and is shaped like a hexagon with an
+edge size of nine triangles.</p>
+<p align="center"><img src="board_trigon.jpg" alt=
+"Board for game variant Trigon"></p>
+<div align="center" class="caption">The board with the starting<br>
+fields marked with gray dots.</div>
+<p>There are six starting points on the board, each located in the middle of
+the fourth row away from each edge. The starting points are not colored and the
+players may freely choose a starting point for the first piece of a color.</p>
+<p align="center"><img src="position_trigon.jpg" alt=
+"Example position for game variant Trigon"></p>
+<div align="center" class="caption">An example position after a few
+moves.</div>
+<h3>Rules for Two Players</h3>
+<p>Like game variant Classic, Trigon can be played with two players by having
+one player play Blue and Red and the other player Yellow and Green.</p>
+<h3>Rules for Three Players</h3>
+<p>Trigon can be played with three players using the same rules as for the
+four-player variant. The three-player variant is played on a smaller board with
+an edge size of eight triangles. The starting points are located in the middle
+of the third row away from each edge.</p>
+<p align="right"><a href="duo_rules.html">Previous</a> | <a href=
+"junior_rules.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="callisto_rules.html">Previous</a> | <a href=
+"become_stronger.html">Next</a></p>
+<h2>How to Use Pentobi</h2>
+<h3>Board</h3>
+<p>Pentobi's main window shows the board on the left side. The played pieces on
+the board can have numbers on them that indicate the move number in which the
+piece was played. An letter after the move number indicates that there exists a
+variation to this move (see below).</p>
+<p>Pieces can be played by moving them to a place that corresponds to a legal
+move with the mouse or arrow keys and pressing the left mouse button or the
+Enter key.</p>
+<h3>Pieces and Score</h3>
+<p>On the right side, the remaining pieces are shown. Above the remaining
+pieces is an orientation selector that shows the currently selected piece and
+allows the player to change its orientation. If no piece is selected and the
+game has not yet ended, a colored dot in the orientation selector shows the
+color to play.</p>
+<p>Pieces can be selected by clicking on one of the remaining pieces shown, by
+using the left/right arrow buttons in the orientation selector or by using
+<a href="shortcuts.html">shortcut keys</a>.</p>
+<p>Below the orientation selector is a score display, which displays the
+current points for each color or player. The points are the sum of on-board
+points and bonus points. Points are underlined if they are final because the
+color cannot play more pieces. A small star indicates that the points include a
+bonus.</p>
+<h3>Playing Against the Computer</h3>
+<p>The board can be used for creating game records of games played by humans or
+for playing games against the computer. In games against the computer, the
+computer can play any (or several) of the colors.</p>
+<p>When you start a new game, the human will play the color(s) of the first
+player by default and the computer all other colors. To change this, use
+<i>Computer Colors</i> from the <i>Computer</i> menu or toolbar and select the
+colors the computer should play.</p>
+<p>The exception is that the computer will play no color by default if it
+played no color in the previous game. This prevents the computer from
+automatically starting to play if the user mainly wants to use the board for
+entering move sequences or similar editing tasks. So if you want to use the
+board without playing against the computer, you need to disable the computer
+colors in the <i>Computer Colors</i> dialog only once and it will stay that
+way. After loading a saved game, the computer also plays no color by
+default.</p>
+<p>Selecting <i>Play</i> from the <i>Computer</i> menu or the toolbar always
+makes the computer play a move for the current color. If the computer did not
+already play this color before, it will also make the computer play this color
+(and only this color) from now on.</p>
+<h3>Move Variations and the Game Tree</h3>
+<p>When you play a game, Pentobi will store the sequence of moves and it is
+always possible to go back to a previous position and play differently. If you
+do this, the new sequence is stored as an alternative sequence (called
+variation). Variations can also be used by annotators for commenting on
+existing games. Variations can exist at any board position and can have
+subvariations themselves. The game can therefore become a game tree, in which
+each node represents a board position. You can navigate in the game tree with
+the items in the <i>Go</i> menu or in the toolbar.</p>
+<p>The main variation is the sequence of moves that starts at the start
+position and always selects the first child node in each position (e.g. by
+selecting <i>Forward</i> in the <i>Go</i> menu or toolbar). The main variation
+is supposed to represent the real game played. If you want a side variation to
+become the main variation, select <i>Make Main Variation</i> from the
+<i>Edit</i> menu.</p>
+<p align="right"><a href="callisto_rules.html">Previous</a> | <a href=
+"become_stronger.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="en">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="become_stronger.html">Previous</a> | <a href=
+"shortcuts.html">Next</a></p>
+<h2>The Window Menu</h2>
+<h3>Game</h3>
+<dl>
+<dt>New</dt>
+<dd>Start a new game.</dd>
+<dt>Rated Game</dt>
+<dd>Start a new <a href="become_stronger.html#rating">rated game</a> against
+the computer.</dd>
+<dt>Game Variant</dt>
+<dd>Select a game variant and start a new game of this game variant.</dd>
+<dt>Game Info</dt>
+<dd>Display or edit additional information about the game like the name of the
+players or the date when the game was played.</dd>
+<dt>Undo Move</dt>
+<dd>Undo the last move played and remove it from the game tree. Undoing a move
+is only possible if it is the last move in the current variation (i.e. a leaf
+node in the game tree; use <i>Edit/Truncate</i> to remove inner nodes of the
+game tree).</dd>
+<dt>Find Move</dt>
+<dd>Find a legal move for the current color and display it for a few seconds on
+the board. Selecting this item repeatedly will show all legal moves.</dd>
+<dt>Open</dt>
+<dd>Load a saved game. The board position after loading will be the last
+position in the main variation unless the game starts with a setup position. If
+the game starts with a setup, the board position will be the first position
+instead. This avoids that solutions are immediately shown if the file contains
+a Blokus puzzle as a setup with the solution as the main variation.</dd>
+<dt>Open Recent</dt>
+<dd>Load a recently used game.</dd>
+<dt>Save</dt>
+<dd>Save the current game.</dd>
+<dt>Save As</dt>
+<dd>Save the current game under a new file name.</dd>
+<dt>Export/Image</dt>
+<dd>Save the current position as an image file. Several image file formats are
+supported, the file format is derived from the file name ending (e.g. ".png"
+for the PNG format).</dd>
+<dt>Export/ASCII Art</dt>
+<dd>Save the current position as a text diagram. The text diagram should be
+viewed using a monospace font.</dd>
+<dt>Quit</dt>
+<dd>Quit Pentobi.</dd>
+</dl>
+<h3>Go</h3>
+<dl>
+<dt>Beginning</dt>
+<dd>Go to the beginning of the game.</dd>
+<dt>Backward</dt>
+<dd>Go one move backward in the current variation. The corresponding button in
+the toolbar supports autorepeat if pressed and held.</dd>
+<dt>Forward</dt>
+<dd>Go one move forward in the current variation. If the current position has
+several follow-up variations (i.e. the current node in the game tree has
+several child nodes), the first variation will be used. The corresponding
+button in the toolbar supports autorepeat if pressed and held.</dd>
+<dt>End</dt>
+<dd>Go to the end of the current variation. Like <i>Forward</i>, this also uses
+the first variation in positions with several follow-up variations.</dd>
+<dt>Next Variation</dt>
+<dd>Go to the next variation to the last move played (i.e. the next sibling
+node of the current node in the game tree).</dd>
+<dt>Previous Variation</dt>
+<dd>Go to the previous variation to the last move played (i.e. the previous
+sibling node of the current node in the game tree).</dd>
+<dt>Go to Move</dt>
+<dd>Go to the move with a given move number in the current variation.</dd>
+<dt>Back to Main Variation</dt>
+<dd>Go back to the last position in the current variation that belonged to the
+main variation.</dd>
+<dt>Beginning of Branch</dt>
+<dd>Go back to the last position in the current variation that had an
+alternative move.</dd>
+<dt>Find Next Comment</dt>
+<dd>Go to the next position that has a comment. If the comment text field was
+not visible, it will become visible. Selecting this item repeatedly will show
+all positions with comments in the game tree.</dd>
+</dl>
+<h3>Edit</h3>
+<dl>
+<dt>Move Annotation</dt>
+<dd>Add a chess-style annotation symbol (e.g. !!) to the current move. The
+symbols are appended to the move numbers in the status bar and, depending on
+the configuration of <i>Move Marking</i>, on the board.</dd>
+<dt>Make Main Variation</dt>
+<dd>Make the current variation the main variation of the game. This reorders
+the nodes in the game tree such that the current variation becomes the main
+variation.</dd>
+<dt>Move Variation Up</dt>
+<dd>Changes the order of variations such that the current position will appear
+earlier when iterating over the variations with <i>Next/Previous
+Variation</i>.</dd>
+<dt>Move Variation Down</dt>
+<dd>Changes the order of variations such that the current position will appear
+later when iterating over the variations with <i>Next/Previous
+Variation</i>.</dd>
+<dt>Delete All Variations</dt>
+<dd>Delete all variations but the main variation. If the current position is
+not in the main variation, it will first be changed to a position as in <i>Back
+to Main Variation</i>.</dd>
+<dt>Truncate</dt>
+<dd>Remove the node with the current position, including any subtree, from the
+game tree.</dd>
+<dt>Truncate Children</dt>
+<dd>Remove all child nodes of the node with the current position from the game
+tree.</dd>
+<dt>Keep Only Position</dt>
+<dd>Delete all moves and keep only the current position as a setup. This can be
+used to create files that start with a given fixed position.</dd>
+<dt>Keep Only Subtree</dt>
+<dd>Like <i>Keep Only Position</i> but does not delete the moves after the
+current position.</dd>
+<dt>Setup Mode</dt>
+<dd>Enter or leave setup mode. In setup mode, pieces can be placed anywhere on
+the board, even in violation of the game rules. Existing pieces can be removed
+from the board by clicking on them. The currently selected color also
+determines the color to play after the setup is finished. It can be changed
+with <i>Next Color</i> or by clicking on the orientation selector while no
+piece is selected. Setup mode can only be used if no moves have been played
+yet.</dd>
+<dt>Next Color</dt>
+<dd>Choose the next color for selecting pieces. This can be used for example to
+enter game records, in which moves of a color were skipped because the color
+ran out of time.</dd>
+</dl>
+<h3>View</h3>
+<dl>
+<dt>Toolbar</dt>
+<dd>Show or hide the toolbar.</dd>
+<dt>Toolbar Text</dt>
+<dd>Configure the appearance of the toolbar.</dd>
+<dt>Comment</dt>
+<dd>Show or hide a text field to display or edit comments on the current
+position.</dd>
+<dt>Move Marking</dt>
+<dd>Change the way moves are marked on the board. The options are to mark the
+last move played with a dot or with a number, or to show the numbers of all
+moves, or not to show any marks.</dd>
+<dt>Coordinates</dt>
+<dd>Display coordinates around the board for the fields on the board. The
+convention for the coordinates is the same as in the Blokus SGF file format
+used by Pentobi.</dd>
+<dt>Show Variations</dt>
+<dd>Appends a letter to the move number on the board if the move has
+variations. If moves are marked with dots instead of numbers, a circle will be
+used instead of a dot for moves not in the main variation.</dd>
+<dt>Fullscreen</dt>
+<dd>Make the main window full screen or leave full screen mode. It is
+platform-dependent if the window menu is shown in full screen mode. To leave
+full screen mode without using the window menu, press the F11 key.</dd>
+</dl>
+<h3>Computer</h3>
+<dl>
+<dt>Computer Colors</dt>
+<dd>Select which colors are played by the computer.</dd>
+<dt>Play</dt>
+<dd>Make the computer play a move for the current color. This can be used to
+change the color the computer plays or to resume playing after navigating in
+the game tree. If the computer did not already play the current color, it will
+play this color (and only this color) from now on.</dd>
+<dt>Play Single Move</dt>
+<dd>Make the computer play a single move for the current color without changing
+the colors played by the computer.</dd>
+<dt>Stop</dt>
+<dd>Abort the current move generation. You can make the computer continue to
+play by selecting <i>Play</i>.</dd>
+<dt>Level</dt>
+<dd>Change the playing strength of the computer. Higher levels are stronger but
+can make the computer take a long time for playing moves on slow computers. The
+computer will remember the level last used separately for each game variant and
+restore it when the game variant is changed.</dd>
+</dl>
+<h3>Tools</h3>
+<dl>
+<dt>Rating</dt>
+<dd>Show a dialog window with the <a href=
+"become_stronger.html#rating">rating</a> of the user in the current game
+variant.</dd>
+<dt>Analyze Game</dt>
+<dd>Perform a <a href="become_stronger.html#analysis">game analysis</a>.</dd>
+</dl>
+<h3>Help</h3>
+<dl>
+<dt>Pentobi Help</dt>
+<dd>Show a window to browse the Pentobi user manual.</dd>
+<dt>About Pentobi</dt>
+<dd>Show an info dialog with information about this version of Pentobi.</dd>
+</dl>
+<p align="right"><a href="become_stronger.html">Previous</a> | <a href=
+"shortcuts.html">Next</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="user_interface.html">Zurück</a> | <a href=
+"window_menu.html">Weiter</a></p>
+<h2>Ein stärkerer Spieler werden</h2>
+<p>Pentobi besitzt Funktionen, die Ihnen helfen können, ein stärkerer
+Blokus-Spieler zu werden.</p>
+<h3 id="analysis">Spielanalyse</h3>
+<p>Sie können ein Spiel analysieren, indem Sie <i>Spiel analysieren</i> aus dem
+<i>Extras</i>-Menü wählen. Dies lässt den Computer eine Bewertung jeder
+Brettstellung der Hauptvariante ausführen. Das Ergebnis wird in einem Fenster
+mit einem Diagramm farbiger Punkte dargestellt.</p>
+<p align="center"><img src="../../C/pentobi/analysis.jpg" alt=
+"Spielanalyse-Fenster"></p>
+<div align="center" class="caption">Analyse eines Spiels der Spielvariante
+Klassisch (2 Spieler).</div>
+<p>Jeder Punkt repräsentiert eine Spielstellung, in der die Farbe des Punkts am
+Zug war. Die Punkte sind horizontal nach Zugnummer angeordnet. Die vertikale
+Achse repräsentiert die Wahrscheinlichkeit, dass die Farbe das Spiel gewinnt.
+Mausklicks im Diagramm gehen zur jeweiligen Stellung.</p>
+<p>Die Werte stellen nur Schätzwerte dar und der Computer wird manchmal
+Stellungen nicht korrekt bewerten. Aber ein plötzliches Abfallen des Wertes
+kann Ihnen dabei helfen, Züge zu finden, die möglicherweise schlecht waren. Sie
+können zur Stellung vor dem Zug zurückgehen und versuchen, einen besseren Zug
+zu finden oder den Computer fragen, was er gespielt hätte, indem Sie
+<i>Einzelnen Zug spielen</i> aus dem <i>Computer</i>-Menü auswählen.</p>
+<h3 id="rating">Ihre Wertung ermitteln</h3>
+<p>Sie können Ihre Fortschritte verfolgen, indem Sie gewertete Spiele gegen den
+Computer spielen. Die Spielergebnisse werden benutzt, um Ihre gegenwärtige
+Wertung zu ermitteln. Die Wertung ist eine Zahl, die Ihre Spielstärke
+darstellt.</p>
+<p>Ein gewertetes Spiel wird mit <i>Gewertetes Spiel</i> aus dem
+<i>Spiel</i>-Menü oder der Werkzeugleiste gestartet. Wenn Sie in der
+gegenwärtigen Spielvariante noch keine gewerteten Spiele gespielt haben, werden
+Sie gefragt, eine Anfangswertung zu wählen, wodurch die Anzahl der Spiele
+reduziert wird, die nötig ist, um Ihre wirkliche Wertung zu bestimmen. Falls
+Sie Anfänger sind, belassen Sie die Anfangswertung auf 1000.</p>
+<p>Für jedes gewertete Spiel wird der Computer eine Spielstufe für den
+Computerspieler gemäß Ihrer gegenwärtigen Wertung wählen. Die Farbe, die Sie
+spielen, wird in jedem Spiel zufällig ausgewählt.</p>
+<p>Während eines gewerteten Spiels sind die meisten Funktionen, die nicht zum
+Spielen benötigt werden, deaktiviert: Sie können keine Züge zurücknehmen, im
+Spiel navigieren, die Computer-Farben ändern oder die Spielstufe ändern. Um
+eine akkurate Wertung zu erhalten, sollten Sie gewertete Spiele immer bis zum
+Ende spielen.</p>
+<p>Nachdem das Spiel beendet ist, wird Ihre Wertung in Abhängigkeit vom
+Spielergebnis und der Spielstufe aktualisiert. Für das Spielergebnis zählt nur,
+ob Sie gewonnen oder verloren haben, oder ob das Spiel in einem Unentschieden
+endete. Die genaue Anzahl der Spielpunkte spielt keine Rolle.</p>
+<p align="center"><img src="../../C/pentobi/rating.jpg" alt=
+"Wertungsfenster"></p>
+<div align="center" class="caption">Fenster mit Wertungsgraph.</div>
+<p>Sie können Ihre aktuelle Wertung jederzeit mit <i>Wertung</i> aus dem
+<i>Extras</i>-Menü sehen. Dies öffnet ein Fenster, in dem die Entwicklung Ihrer
+Wertung während der letzten 100 Spiele als Graph gezeigt wird. Die letzten 100
+Spiele werden automatisch gespeichert und können durch Doppelklick auf die
+Zeilen der Spieltabelle unter dem Graph geladen werden.</p>
+<p align="right"><a href="user_interface.html">Zurück</a> | <a href=
+"window_menu.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="nexos_rules.html">Zurück</a> | <a href=
+"user_interface.html">Weiter</a></p>
+<h2>Callisto-Regeln</h2>
+<p>Callisto ist ein weiteres Brettspiel ähnlich wie Blokus. Das Spielbrett ist
+vom klassischen 20×20-Blokus-Spielbrett abgeleitet, indem die Ecken entfernt
+werden, sodass ein Achteck verbleibt mit einer oberen Kantenlänge von sechs.
+Die Spielsteine sind eine Untermenge der Polyominos bis zur Größe fünf. Sie
+beinhalten drei 1×1-Spielsteine pro Spieler, die eine besondere Rolle
+spielen.</p>
+<p align="center"><img src="../../C/pentobi/pieces_callisto.png" alt=
+"Spielsteine für Spielvariante Callisto"></p>
+<div align="center" class="caption">Die 21 Spielsteine.</div>
+<p>Die 1×1-Spielsteine dürfen überall auf dem Spielbrett gesetzt werden außer
+im Zentrum des Spielbretts. Das Zentrums besteht aus einem Achteck mit Breite
+sechs und oberer Kantenlänge zwei. Die ersten zwei Züge eines Spielers müssen
+einen 1×1-Spielstein benutzen, der dritte 1×1-Spielstein kann jederzeit später
+gespielt werden.</p>
+<p align="center"><img src="../../C/pentobi/board_callisto.png" alt=
+"Spielbrett für Spielvariante Callisto"></p>
+<div align="center" class="caption">Das Brett mit einer dunkleren Farbe im
+Zentrum.</div>
+<p>Alle größeren Spielsteine dürfen überall auf dem Brett gesetzt werden,
+müssen aber einen existierenden Spielstein der selben Farbe Kante an Kante
+berühren.</p>
+<p align="center"><img src="../../C/pentobi/position_callisto.png" alt=
+"Beispielstellung für Spielvariante Callisto"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<p>Die Punktzahl einer Farbe ist die Anzahl der Quadrate auf dem Brett, die von
+der Farbe bedeckt sind, wobei die 1×1-Spielsteine nicht gezählt werden.
+Bonuspunkte werden nicht verwendet. Anders als in Blokus werden Unentschieden
+zugunsten des Spielers aufgelöst, der später begonnen hat.</p>
+<h3>Regeln für zwei oder drei Spieler</h3>
+<p>Das Spiel kann mit weniger als vier Spielern gespielt werden, indem ein
+kleineres Spielbrett verwendet wird. Für drei Spieler ist das Brett ein Achteck
+mit Breite 20 und obererer Kantenlänge zwei. Für zwei Spieler ist das Brett ein
+Achteck mit Breite 16 und obererer Kantenlänge zwei. Die Größe des Zentrums
+bleibt gleich.</p>
+<p align="right"><a href="nexos_rules.html">Zurück</a> | <a href=
+"user_interface.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="index.html">Zurück</a> | <a href=
+"duo_rules.html">Weiter</a></p>
+<h2>Klassische Regeln</h2>
+<p>Es gibt vier Spieler, Blau, Gelb, Rot und Grün, und ein Brett, das aus 20×20
+Quadraten besteht.</p>
+<p>Jeder Spieler besitzt 21 Spielsteine seiner Farbe, die die Form von
+Polyominos bis zur Größe fünf haben (ein Polyomino ist eine Figur, die aus
+einer Anzahl von Quadraten besteht, die entlang der Kanten verbunden sind).</p>
+<p align="center"><img src="../../C/pentobi/pieces.png" alt=
+"Spielsteine für Spielvariante Klassisch"></p>
+<div align="center" class="caption">Die 21 Spielsteine.</div>
+<p>Die Spieler setzen abwechselnd einen ihrer Spielsteine aufs Brett. Blau
+fängt an, gefolgt von Gelb, dann Rot, dann Grün.</p>
+<p>Jeder Spieler hat ein Startfeld. Das Startfeld von Blau ist in der oberen
+linken Ecke, das von Gelb in der oberen rechten Ecke, das von Rot in der
+unteren rechten Ecke und das von Grün in der unteren linken Ecke. Der erste
+Spielstein eines Spielers muss sein Startfeld abdecken.</p>
+<p align="center"><img src="../../C/pentobi/board_classic.png" alt=
+"Spielbrett für Spielvariante Klassisch"></p>
+<div align="center" class="caption">Das 20×20-Brett mit den Startfeldern<br>
+durch farbige Punkte markiert.</div>
+<p>Die folgenden Spielsteine müssen so auf leere Quadrate gesetzt werden, dass
+der neue Spielstein mindestens einen Spielstein der eigenen Farbe Ecke an Ecke
+berührt, aber keinen Spielstein der eigenen Farbe entlang der Kanten. Der neue
+Spielstein darf die Kanten von gegnerischen Spielsteinen berühren.</p>
+<p align="center"><img src="../../C/pentobi/position_classic.png" alt=
+"Beispielstellung für Spielvariante Klassisch"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<p>Wenn der Spieler einer Farbe keine Spielsteine mehr setzen kann, muss der
+Spieler aussetzen und die nächste Farbe ist am Zug.</p>
+<p>Wenn keiner der Spieler mehr einen Spielstein setzen kann, gewinnt der
+Spieler mit der höchsten Punktzahl. Die Punktzahl einer Farbe ist die Anzahl
+der Quadrate auf dem Brett, die von der Farbe besetzt sind, plus ein Bonus von
+15 Punkten, wenn die Farbe alle ihre Spielsteine setzen konnte, plus ein
+zusätzlicher Bonus von 5 Punkten, wenn die Farbe alle Spielsteine setzen konnte
+und der zuletzt gespielte Spielstein der Spielstein war, der aus einem Quadrat
+besteht.</p>
+<h3>Regeln für zwei Spieler</h3>
+<p>Das Spiel kann mit zwei Spielern gespielt werden. Der erste Spieler spielt
+Blau und Rot, der zweite Spieler Gelb und Grün. Die Punkte von beiden Farben
+eines Spielers werden addiert.</p>
+<h3>Regeln für drei Spieler</h3>
+<p>Das Spiel kann auch mit drei Spielern gespielt werden. Die Spieler wechseln
+sich beim Spielen der vierten Farbe (Grün) ab. Am Spielende wird die Punktzahl
+von Grün ignoriert.</p>
+<h3>Farblose Startfelder</h3>
+<p>Beachten Sie, dass die ursprünglichen klassischen Regeln für Blokus farblose
+Startfelder benutzen. Dies bedeutet, dass jede Farbe frei wählen darf, welches
+der verbleibenden noch freien Startfelder sie für ihren ersten Zug benutzt.
+Pentobi unterstützt zur Zeit nur die Regelvariante mit farbigen Startfeldern,
+weil diese Variante auf dem Blokus-Online-Server auf blokus.com und in den
+meisten bisherigen Blokus-Turnieren verwendet wurde.</p>
+<p align="right"><a href="index.html">Zurück</a> | <a href=
+"duo_rules.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Zurück</a> | <a href=
+"trigon_rules.html">Weiter</a></p>
+<h2>Duo-Regeln</h2>
+<p>Die Spielvariante Duo ist eine andere Spielvariante für zwei Spieler. Das
+Spiel wird auf einem kleineren Brett mit 14×14 Quadraten gespielt. Es gibt eine
+Farbe pro Spieler (Blau und Grün) und die Startfelder befinden sich nicht in
+den Ecken, sondern auf dem Feld mit den Koordinaten (5,10) für Blau und auf
+(10,5) für Grün.</p>
+<p align="center"><img src="../../C/pentobi/board_duo.png" alt=
+"Spielbrett für Spielvariante Duo"></p>
+<div align="center" class="caption">Das 14×14-Brett, das in der Spielvariante
+Duo benutzt<br>
+wird, mit den Startfeldern durch farbige Punkte markiert.</div>
+<p align="center"><img src="../../C/pentobi/position_duo.png" alt=
+"Beispielstellung für Spielvariante Duo"></p>
+<div align="center" class="caption">Eine Beispielstellung in der Spielvariante
+Duo.</div>
+<p align="right"><a href="classic_rules.html">Zurück</a> | <a href=
+"trigon_rules.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="classic_rules.html">Weiter</a></p>
+<h1>Pentobi</h1>
+<p>Pentobi ist ein Computer-Gegner für das Brettspiel Blokus. In diesem Spiel
+setzen vier Spieler Spielsteine, die ähnlich den Spielsteinen des
+Computerspiels Tetris sind, auf ein 20×20-Brett. Pentobi unterstützt auch die
+Spielvarianten für zwei oder drei Spieler und die Spielvarianten Duo, Trigon,
+Junior, Nexos und Callisto.</p>
+<p><a href="classic_rules.html">Klassische Regeln</a><br>
+<a href="duo_rules.html">Duo-Regeln</a><br>
+<a href="trigon_rules.html">Trigon-Regeln</a><br>
+<a href="junior_rules.html">Junior-Regeln</a><br>
+<a href="nexos_rules.html">Nexos-Regeln</a><br>
+<a href="callisto_rules.html">Callisto-Regeln</a><br>
+<a href="user_interface.html">Wie Sie Pentobi benutzen</a><br>
+<a href="become_stronger.html">Ein stärkerer Spieler werden</a><br>
+<a href="window_menu.html">Das Fenstermenü</a><br>
+<a href="shortcuts.html">Tastenkürzel</a><br>
+<a href="system.html">Systemvoraussetzungen</a><br>
+<a href="license.html">Lizenz</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="trigon_rules.html">Zurück</a> | <a href=
+"nexos_rules.html">Weiter</a></p>
+<h2>Junior-Regeln</h2>
+<p>Junior ist eine vereinfachte Spielvariante für zwei Spieler. Es wird auf dem
+gleichen 14×14-Brett gespielt wie die Spielvariante Duo, benutzt aber nur eine
+Teilmenge der Pentominos und die Spieler bekommen zwei von jedem dieser
+Pentominos.</p>
+<p align="center"><img src="../../C/pentobi/pieces_junior.png" alt=
+"Spielsteine für Spielvariante Junior"></p>
+<div align="center" class="caption">Die 24 Spielsteine, die in Junior benutzt
+werden.</div>
+<p>Bonuspunkte werden in Junior nicht benutzt.</p>
+<p align="right"><a href="trigon_rules.html">Zurück</a> | <a href=
+"nexos_rules.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="system.html">Zurück</a></p>
+<h2>Lizenz</h2>
+<p>Copyright © 2011–2017 Markus Enzenberger</p>
+<p>Dieses Programm ist freie Software. Sie können es unter den Bedingungen der
+GNU General Public License, wie von der Free Software Foundation
+veröffentlicht, weitergeben und/oder modifizieren, entweder gemäß Version 3 der
+Lizenz oder (nach Ihrer Wahl) jeder späteren Version.</p>
+<p>Die Veröffentlichung dieses Programms erfolgt in der Hoffnung, dass es Ihnen
+von Nutzen sein wird, aber OHNE IRGENDEINE GARANTIE, insbesondere ohne eine
+implizite Garantie der MARKTREIFE oder der VERWENDBARKEIT FÜR EINEN BESTIMMTEN
+ZWECK. Nähere Angaben finden Sie in der GNU General Public License.</p>
+<h3>Hinweis zu Markennamen</h3>
+<p>Der Markenname Blokus und andere erwähnte Marken sind Eigentum ihrer
+jeweiligen Markeninhaber. Die Markeninhaber stehen in keiner Verbindung mit dem
+Autor des Programms Pentobi.</p>
+<p align="right"><a href="system.html">Zurück</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi Help</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="junior_rules.html">Zurück</a> | <a href=
+"callisto_rules.html">Weiter</a></p>
+<h2>Nexos-Regeln</h2>
+<p>Nexos ist ein Brettspiel ähnlich wie Blokus. Das Spielbrett ist ein
+rechtwinkliges 13×13-Liniengitter. Jede Farbe benutzt 24 Spielsteine, die aus
+bis zu vier verbundenen Liniensegmenten bestehen.</p>
+<p align="center"><img src="../../C/pentobi/pieces_nexos.png" alt=
+"Spielsteine für Nexos"></p>
+<div align="center" class="caption">Die 24 Spielsteine.</div>
+<p>Jede Farbe hat einen Startkreuzungspunkt auf der Kreuzung der dritten Linien
+nahe einer Ecke. Der erste Spielstein muss den Startkreuzungspunkt
+berühren.</p>
+<p align="center"><img src="../../C/pentobi/board_nexos.png" alt=
+"Spielbrett für Nexos"></p>
+<div align="center" class="caption">Das Brett für Nexos mit den die
+Startkreuzungspunkte<br>
+berührenden Segmenten durch farbige Punkte markiert.</div>
+<p>Die folgenden Spielsteine müssen so auf leere Liniensegmente gesetzt werden,
+dass ein Segment des neuen Spielsteins einen Kreuzungspunkt berührt, den
+bereits ein Segment derselben Farbe berührt. Es spielt keine Rolle, ob
+Spielsteine anderer Farbe denselben Kreuzungspunkt berühren oder bedecken.
+Allerdings dürfen sich Spielsteine nicht überlappen. Die Verbindungen zwischen
+den Segmenten innerhalb eines Spielsteins sind so, dass zwei rechtwinklige
+Verbindungen verschiedener Spielsteine denselben Kreuzungspunkt bedecken können
+ohne sich zu überlappen, während gerade Verbindungen das nicht können.</p>
+<p align="center"><img src="../../C/pentobi/position_nexos.png" alt=
+"Beispielstellung für Nexos"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<p>Die Punktzahl einer Farbe ist die Anzahl der Liniensegmente auf dem Brett,
+die von der Farbe bedeckt sind, plus ein Bonus von 10 Punkten, wenn die Farbe
+alle ihre Spielsteine setzen konnte.</p>
+<h3>Regeln für zwei Spieler</h3>
+<p>Wie Blokus kann Nexos von zwei Spielern gespielt werden, indem ein Spieler
+Rot und Blau, und der andere Spieler Gelb und Grün spielt.</p>
+<p align="right"><a href="junior_rules.html">Zurück</a> | <a href=
+"callisto_rules.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="window_menu.html">Zurück</a> | <a href=
+"system.html">Weiter</a></p>
+<h2>Tastenkürzel</h2>
+<p>Zusätzlich zu den Tastenkürzeln der Menüpunkte, die im Fenstermenü gezeigt
+werden, werden die folgenden weiteren Tastenkürzel von Pentobi unterstützt.
+Beachten Sie, dass diese Tastenkürzel nicht aktiv sind, wenn das Kommentarfeld
+sichtbar ist und den Fokus besitzt. In diesem Fall kann der Fokus vom
+Kommentartext durch die Tabulator-Taste entfernt werden.</p>
+<dl>
+<dt>Plus</dt>
+<dd>
+<p>Nächsten Spielstein auswählen</p>
+</dd>
+<dt>Minus</dt>
+<dd>
+<p>Vorherigen Spielstein auswählen</p>
+</dd>
+<dt>0</dt>
+<dd>
+<p>Spielsteinauswahl löschen</p>
+</dd>
+<dt>Leertaste</dt>
+<dd>
+<p>Nächste Ausrichtung des ausgewählten Spielsteins</p>
+</dd>
+<dt>Umschalt+Leertaste</dt>
+<dd>
+<p>Vorherige Ausrichtung des ausgewählten Spielsteins</p>
+</dd>
+<dt>Links, Rechts, Oben, Unten</dt>
+<dd>
+<p>Bewegen des ausgewählten Spielsteins.</p>
+</dd>
+<dt>Enter</dt>
+<dd>
+<p>Spielen des ausgewählten Spielsteins.</p>
+</dd>
+<dt>1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z</dt>
+<dd>
+<p>Einen Spielstein entsprechend den üblicherweise benutzten Spielsteinnamen
+auswählen. Wenn es mehrere Spielsteine mit dem Buchstaben gibt (z. B. I3,
+I4, I5), dann kann durch mehrmaliges Drücken der Taste zwischen ihnen
+gewechselt werden. Einige Buchstaben werden nur in bestimmten Spielvarianten
+benutzt. Zum Beispiel wird A nur in Trigon für die Spielsteine A6 und A4
+benutzt (auch bekannt als „Hummer“ und „Dreieck“).</p>
+</dd>
+</dl>
+<p align="right"><a href="window_menu.html">Zurück</a> | <a href=
+"system.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="shortcuts.html">Zurück</a> | <a href=
+"license.html">Weiter</a></p>
+<h2>Systemvoraussetzungen</h2>
+<p>Minimum: 1 GB RAM, 1 GHz CPU<br>
+Empfohlen für Spielstufe 9: 4 GB RAM, 2 GHz Dual-Core- oder schnellere
+CPU</p>
+<p>Pentobi funktioniert auch auf Systemen, die das Systemminimum nicht
+erfüllen, aber die höchste Spielstufe kann auf diesen Systemen sehr langsam
+sein (wenn die CPU zu langsam ist) oder eine reduzierte Spielstärke haben (wenn
+nicht genügend Arbeitsspeicher vorhanden ist).</p>
+<p align="right"><a href="shortcuts.html">Zurück</a> | <a href=
+"license.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="duo_rules.html">Zurück</a> | <a href=
+"junior_rules.html">Weiter</a></p>
+<h2>Trigon-Regeln</h2>
+<p>Trigon ist eine weitere Spielvariante. Die Regeln sind ähnlich wie in der
+Spielvariante Klassisch, aber es werden ein anders geformtes Brett und andere
+Spielsteine verwendet. Jede Farbe benutzt 22 Spielsteine, die wie die
+Polyiamonds bis zur Größe sechs geformt sind (ein Polyiamond ist eine Figur,
+die aus einer Anzahl von gleichseitigen Dreiecken besteht, die entlang der
+Kanten verbunden sind).</p>
+<p align="center"><img src="../../C/pentobi/pieces_trigon.jpg" alt=
+"Spielsteine für Spielvariante Trigon"></p>
+<div align="center" class="caption">Die 22 Trigon-Spielsteine.</div>
+<p>Das Spielbrett besteht ebenfalls aus Dreiecken und hat die Form eines
+Sechsecks mit jeweils neun Dreiecken pro Kante.</p>
+<p align="center"><img src="../../C/pentobi/board_trigon.jpg" alt=
+"Spielbrett für Spielvariante Trigon"></p>
+<div align="center" class="caption">Das Brett mit den Startfeldern<br>
+durch graue Punkte markiert.</div>
+<p>Es gibt sechs Startfelder auf dem Brett, jedes in der Mitte der vierten
+Reihe von jeder Kante aus gesehen. Die Startfelder sind nicht farbig und die
+Spieler dürfen das Startfeld für den ersten Spielstein einer Farbe frei
+wählen.</p>
+<p align="center"><img src="../../C/pentobi/position_trigon.jpg" alt=
+"Beispielstellung für Spielvariante Trigon"></p>
+<div align="center" class="caption">Eine Beispielstellung nach ein paar
+Zügen.</div>
+<h3>Regeln für zwei Spieler</h3>
+<p>Wie die Spielvariante Klassisch kann Trigon mit zwei Spielern gespielt
+werden, indem ein Spieler Blau und Rot und der andere Gelb und Grün spielt.</p>
+<h3>Regeln für drei Spieler</h3>
+<p>Trigon kann mit drei Spielern gespielt werden, wobei dieselben Regeln wie
+für die Variante mit vier Spielern benutzt werden. Die Variante für drei
+Spieler wird auf einem kleineren Spielbrett mit einer Kantenlänge von acht
+Dreiecken gespielt. Die Startfelder sind in der Mitte der dritten Reihe von
+jeder Kante aus gesehen.</p>
+<p align="right"><a href="duo_rules.html">Zurück</a> | <a href=
+"junior_rules.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="callisto_rules.html">Zurück</a> | <a href=
+"become_stronger.html">Weiter</a></p>
+<h2>Wie Sie Pentobi benutzen</h2>
+<h3>Spielbrett</h3>
+<p>Pentobis Hauptfenster zeigt das Spielbrett auf der linken Seite. Auf den
+gespielten Spielsteinen auf dem Brett können sich Nummern befinden, die die
+Zugnummer angeben, zu der der Spielstein gespielt wurde. Ein Buchstabe nach der
+Zugnummer zeigt an, dass zu diesem Zug eine Variante existiert (siehe
+unten).</p>
+<p>Spielsteine können gespielt werden, indem sie mit der Maus oder den
+Pfeiltasten an eine Position gebracht werden, die einem legalen Zug entspricht,
+und dann die linke Maustaste oder die Eingabetaste gedrückt wird.</p>
+<h3>Spielsteine und Punkte</h3>
+<p>Auf der rechten Seite werden die verbleibenden Spielsteine gezeigt. Über den
+verbleibenden Spielsteinen befinden sich eine Orientierungsauswahl, die den
+ausgewählten Spielstein zeigt und es dem Spieler erlaubt, seine Orientierung zu
+ändern. Wenn kein Spielstein ausgewählt ist und das Spiel noch nicht beendet
+ist, zeigt ein farbiger Punkt in der Orientierungsauswahl, welche Farbe am Zug
+ist.</p>
+<p>Spielsteine können durch Klicken auf einen gezeigten verbleibenden
+Spielstein ausgewählt werden, durch Benutzen der Buttons mit dem
+Links/Rechts-Pfeil in der Orientierungsauswahl oder durch Benutzen von <a href=
+"shortcuts.html">Tastenkürzeln</a>.</p>
+<p>Unterhalb der Orientierungsauswahl befindet sich eine Punkteanzeige, die die
+gegenwärtigen Punkte für jede Farbe oder jeden Spieler zeigt. Die Punkte sind
+die Summe aus den Punkten auf dem Spielbrett und den Bonuspunkten. Punkte sind
+unterstrichen, wenn sie endgültig sind, weil die Farbe keine Spielsteine mehr
+spielen kann. Eine kleiner Stern zeigt an, dass die Punkte einen Bonus
+beinhalten.</p>
+<h3>Gegen den Computer spielen</h3>
+<p>Das Spielbrett kann benutzt werden, um Partien einzugeben, die von Menschen
+gespielt werden, oder um Spiele gegen den Computer zu spielen. In Spielen gegen
+den Computer kann der Computer jede der Farben (oder mehrere) spielen.</p>
+<p>Wenn Sie ein neues Spiel beginnen, ist voreingestellt, dass der Mensch die
+Farbe(n) des ersten Spielers spielt und der Computer alle anderen Farben. Um
+dies zu ändern, benutzen Sie <i>Computer-Farben</i> aus dem Menü
+<i>Computer</i> oder der Werkzeugleiste und wählen Sie die Farben, die der
+Computer spielen soll.</p>
+<p>Die Ausnahme ist, dass es voreingestellt ist, dass der Computer keine Farbe
+spielt, wenn er im letzten Spiel keine Farbe gespielt hat. Damit wird
+vermieden, dass der Computer unbeabsichtigt automatisch zu spielen beginnt,
+wenn der Benutzer das Spielbrett hauptsächlich zum Eingeben von Zugsequenzen
+oder ähnliche Aufgaben benutzen will. Wenn Sie also das Spielbrett benutzen
+wollen ohne gegen den Computer zu spielen, brauchen Sie nur einmal die Farben
+des Computers im Dialogfenster <i>Computer-Farben</i> abschalten und diese
+Einstellung wird sich nicht ändern. Nach dem Laden eines Spiels ist ebenfalls
+voreingestellt, dass der Computer keine Farbe spielt.</p>
+<p>Die Auswahl von <i>Spielen</i> aus dem Menü <i>Computer</i> oder der
+Werkzeugleiste lässt den Computer immer einen Zug für die gegenwärtige Farbe
+spielen. Wenn der Computer diese Farbe bisher nicht gespielt hat, wird er
+außerdem im weiteren Spielverlauf diese Farbe (und nur diese Farbe)
+spielen.</p>
+<h3>Zugvarianten und der Spielbaum</h3>
+<p>Wenn Sie ein Spiel spielen, wird Pentobi die Abfolge der Züge speichern und
+es ist jederzeit möglich, zu einer früheren Brettstellung zurückzugehen und
+anders zu spielen. Wenn Sie das tun, wird die neue Zugfolgen als eine
+alternative Zugfolge (genannt Variante) gespeichert. Varianten können auch von
+Kommentatoren benutzt werden, um Kommentierungen zu existierenden Spielen
+hinzuzufügen. Varianten können in jeder Brettstellung existieren und ihrerseits
+Untervarianten besitzen. Das Spiel kann daher zu einem Spielbaum werden, in dem
+jeder Knoten eine Brettstellung repräsentiert. Sie können im Spielbaum mit den
+Menüpunkten des Menüs <i>Gehe zu</i> oder der Werkzeugleiste navigieren.</p>
+<p>Die Hauptvariante ist die Zugfolge, die in der Startstellung beginnt und
+immer den ersten Kindknoten in jeder Brettstellung wählt (z. B. indem Sie
+<i>Vorwärts</i> im Menü <i>Gehe zu</i> oder der Werkzeugleiste benutzen). Die
+Hauptvariante sollte das wirklich gespielte Spiel darstellen. Wenn Sie eine
+Nebenvariante zur Hauptvariante machen wollen, wählen Sie <i>Zu Hauptvariante
+machen</i> aus dem <i>Bearbeiten</i>-Menü.</p>
+<p align="right"><a href="callistorules.html">Zurück</a> | <a href=
+"become_stronger.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html lang="de">
+<head>
+<title>Pentobi-Hilfe</title>
+<link rel="stylesheet" type="text/css" href="../../C/pentobi/stylesheet.css">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<p align="right"><a href="become_stronger.html">Zurück</a> | <a href=
+"shortcuts.html">Weiter</a></p>
+<h2>Das Fenstermenü</h2>
+<h3>Spiel</h3>
+<dl>
+<dt>Neu</dt>
+<dd>Beginnt ein neues Spiel.</dd>
+<dt>Gewertetes Spiel</dt>
+<dd>Beginnt ein neues <a href="become_stronger.html#rating">gewertetes
+Spiel</a> gegen den Computer.</dd>
+<dt>Spielvariante</dt>
+<dd>Wählt eine Spielvariante und beginnt ein neues Spiel dieser
+Spielvariante.</dd>
+<dt>Spielinformation</dt>
+<dd>Öffnet ein Dialogfenster zum Anzeigen oder Bearbeiten zusätzlicher
+Informationen über das Spiel, wie die Namen der Spieler oder das Datum, an dem
+das Spiel gespielt wurde.</dd>
+<dt>Zug rückgängig</dt>
+<dd>Nimmt den zuletzt gespielten Zug zurück und entfernt ihn aus dem Spielbaum.
+Das Zurücknehmen eines Zugs ist nur möglich, wenn er der letzte Zug der
+gegenwärtigen Variante ist (d. h. ein Endknoten im Spielbaum; benutzen Sie
+<i>Bearbeiten/Abschneiden</i> zum Entfernen innerer Knoten aus dem
+Spielbaum).</dd>
+<dt>Zug finden</dt>
+<dd>Findet einen legalen Zug für die gegenwärtige Farbe und zeigt ihn für ein
+paar Sekunden auf dem Spielbrett. Das wiederholte Auswählen dieses Menüpunkts
+zeigt alle legalen Züge.</dd>
+<dt>Öffnen</dt>
+<dd>Lädt ein gespeichertes Spiel. Die Brettstellung nach dem Laden ist die
+letzte Stellung in der Hauptvariante, sofern das Spiel nicht mit einer
+aufgebauten Brettstellung beginnt. Wenn das Spiel mit einer aufgebauten
+Brettstellung beginnt, ist die Stellung nach dem Laden stattdessen die
+Anfangsstellung. Dies vermeidet, dass Lösungen sofort angezeigt werden, wenn
+die Datei ein Blokus-Problem als aufgebaute Brettstellung enthält mit der
+Lösung in der Hauptvariante.</dd>
+<dt>Zuletzt benutzte Dateien</dt>
+<dd>Lädt ein kürzlich benutztes Spiel.</dd>
+<dt>Speichern</dt>
+<dd>Speichert das gegenwärtige Spiel.</dd>
+<dt>Speichern unter</dt>
+<dd>Speichert das gegenwärtige Spiel unter einem neuen Dateinamen.</dd>
+<dt>Exportieren/Grafik</dt>
+<dd>Speichert die gegenwärtige Brettstellung als eine Grafikdatei. Mehrere
+Grafikdateiformate werden unterstützt, das Dateiformat wird von der Dateiendung
+abgeleitet (z. B. „.png“ für das PNG-Format).</dd>
+<dt>Exportieren/ASCII-Art</dt>
+<dd>Speichert die gegenwärtige Brettstellung als Textdiagramm. Das Textdiagramm
+sollte mit einer Schriftart fester Breite betrachtet werden.</dd>
+<dt>Beenden</dt>
+<dd>Beendet Pentobi.</dd>
+</dl>
+<h3>Gehe zu</h3>
+<dl>
+<dt>Anfang</dt>
+<dd>Geht zum Anfang des Spiels.</dd>
+<dt>Zurück</dt>
+<dd>Geht einen Zug in der gegenwärtigen Variante zurück. Der entsprechende
+Button in der Werkzeugleiste unterstützt automatische Wiederholung, wenn er
+gedrückt gehalten wird.</dd>
+<dt>Vorwärts</dt>
+<dd>Geht einen Zug in der gegenwärtigen Variante vorwärts. Wenn die
+gegenwärtige Brettstellung mehrerer nachfolgende Varianten hat (d. h. der
+gegenwärtige Knoten im Spielbaum mehrere Kindknoten hat), wird die erste
+Variante benutzt. Der entsprechende Button in der Werkzeugleiste unterstützt
+automatische Wiederholung, wenn er gedrückt gehalten wird.</dd>
+<dt>Ende</dt>
+<dd>Geht zum Ende der gegenwärtigen Variante. Wie bei <i>Vorwärts</i> wird auch
+hier jeweils die erste Variante benutzt, wenn die Brettstellung mehrere
+nachfolgende Varianten hat.</dd>
+<dt>Nächste Variante</dt>
+<dd>Geht zur nächsten Variante zum zuletzt gespielten Zug (d. h. zum
+nächsten Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).</dd>
+<dt>Vorherige Variante</dt>
+<dd>Geht zur vorherigen Variante zum zuletzt gespielten Zug (d. h. zum
+vorherigen Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).</dd>
+<dt>Gehe zu Zug</dt>
+<dd>Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen
+Variante.</dd>
+<dt>Zurück zu Hauptvariante</dt>
+<dd>Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die
+zur Hauptvariante gehörte.</dd>
+<dt>Anfang der Verzweigung</dt>
+<dd>Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die
+einen alternativen Zug hatte.</dd>
+<dt>Nächsten Kommentar finden</dt>
+<dd>Geht zur nächsten Brettstellung, die einen Kommentar besitzt. Wenn das
+Kommentarfeld nicht sichtbar ist, wird es sichtbar gemacht. Das wiederholte
+Auswählen dieses Menüpunkts zeigt nacheinander alle Brettstellungen mit
+Kommentaren im Spielbaum.</dd>
+</dl>
+<h3>Bearbeiten</h3>
+<dl>
+<dt>Zugkommentierung</dt>
+<dd>Fügt ein wie in der Schachnotation benutztes Symbol (z. B. !!) zum
+gegenwärtigen Zug hinzu. Die Symbole werden an die Zugnummern in der
+Statusleiste angehängt und, abhängig von der Einstellung von
+<i>Zugmarkierung</i>, an die auf dem Spielbrett.</dd>
+<dt>Zu Hauptvariante machen</dt>
+<dd>Macht die gegenwärtige Variante zur Hauptvariante des Spiels. Dies ordnet
+die Knoten im Spielbaum so um, dass die gegenwärtige Variante zur Hauptvariante
+wird.</dd>
+<dt>Variante nach oben schieben</dt>
+<dd>Ändert die Reihenfolge der Varianten so, dass die gegenwärtige
+Brettstellung beim Durchlaufen der Varianten mit <i>Nächste/Vorherige
+Variante</i> früher erscheint.</dd>
+<dt>Variante nach unten schieben</dt>
+<dd>Ändert die Reihenfolge der Varianten so, dass die gegenwärtige
+Brettstellung beim Durchlaufen der Varianten mit <i>Nächste/Vorherige
+Variante</i> später erscheint.</dd>
+<dt>Alle Varianten löschen</dt>
+<dd>Löscht alle Varianten außer der Hauptvariante. Wenn sich die gegenwärtige
+Brettstellung nicht in der Hauptvariante befindet, wird zuvor zu einer
+Brettstellung in der Hauptvariante gewechselt wie in <i>Zurück zu
+Hauptvariante</i>.</dd>
+<dt>Abschneiden</dt>
+<dd>Entfernt den Knoten mit der gegenwärtigen Brettstellung zusammen mit dem
+auf ihn folgenden Teilbaum aus dem Spielbaum.</dd>
+<dt>Kindknoten abschneiden</dt>
+<dd>Entfernt alle Kindknoten des Knotens mit der gegenwärtigen Brettstellung
+aus dem Spielbaum.</dd>
+<dt>Nur Brettstellung behalten</dt>
+<dd>Löscht alle Züge und behält nur die gegenwärtige Brettstellung als feste
+Stellung. Dies kann zur Erzeugung von Dateien benutzt werden, die mit einer
+festgelegten Brettstellung beginnen.</dd>
+<dt>Nur Teilbaum behalten</dt>
+<dd>Wie <i>Nur Brettstellung behalten</i>, aber die Züge nach der gegenwärtigen
+Brettstellung werden nicht gelöscht.</dd>
+<dt>Stellungsaufbau</dt>
+<dd>Aktiviert oder deaktiviert den Stellungsaufbau-Modus. Im
+Stellungsaufbau-Modus können Spielsteine überall auf dem Brett abgelegt werden,
+auch unter Verletzung der Spielregeln. Existierende Spielsteine können durch
+Anklicken vom Brett entfernt werden. Die gegenwärtig gewählte Farbe legt auch
+die Farbe fest, die nach Beenden des Stellungsaufbaus am Zug ist. Sie kann mit
+<i>Nächste Farbe</i> oder durch Klicken auf die Orientierungsauswahl während
+kein Spielstein ausgewählt ist geändert werden. Der Stellungsaufbau-Modus kann
+nur benutzt werden, wenn noch keine Züge gespielt wurden.</dd>
+<dt>Nächste Farbe</dt>
+<dd>Wählt die nächste Farbe zum Auswählen eines Spielsteins. Dies kann zum
+Beispiel benutzt werden, um Partien einzugeben, bei denen Züge einer Farbe
+übersprungen wurden, da die Farbe aufgrund einer Bedenkzeitüberschreitung vom
+Weiterspielen ausgeschlossen wurde.</dd>
+</dl>
+<h3>Ansicht</h3>
+<dl>
+<dt>Werkzeugleiste</dt>
+<dd>Zeigt oder verbirgt die Werkzeugleiste.</dd>
+<dt>Werkzeugleistentext</dt>
+<dd>Konfiguriert das Aussehen der Werkzeugleiste.</dd>
+<dt>Kommentar</dt>
+<dd>Zeigt oder verbirgt ein Textfeld zum Anzeigen oder Bearbeiten von
+Kommentaren zur gegenwärtigen Brettstellung.</dd>
+<dt>Zugnummern</dt>
+<dd>Ändert die Anzeigeart von Zugnummern auf dem Spielbrett. Die Optionen sind
+nur die Nummer des zuletzt gespielten Zugs zu zeigen (oder der zuletzt
+gespielten Züge, wenn der Computer mehrere Züge nacheinander spielt) oder die
+Nummern aller Züge zu zeigen oder gar keine Nummern zu zeigen.</dd>
+<dt>Zugmarkierung</dt>
+<dd>Ändert die Markierung von Zügen auf dem Spielbrett. Die Optionen sind, den
+zuletzt gespielten Zug mit einem Punkt oder einer Nummer zu markieren, oder die
+Nummern aller Züge zu zeigen oder gar keine Markierung zu zeigen.</dd>
+<dt>Koordinaten</dt>
+<dd>Zeigt Koordinaten an den Rändern des Spielbretts für die Felder auf dem
+Spielbrett. Die Konvention für die Koordinaten ist dieselbe wie im von Pentobi
+benutzten Blokus-SGF-Dateiformat.</dd>
+<dt>Varianten zeigen</dt>
+<dd>Fügt einen Buchstaben an die Zugnummer auf dem Spielbrett an, wenn der Zug
+Varianten besitzt. Wenn Züge mit einem Punkt statt einer Nummer markiert
+werden, wird ein Kreis statt ein Punkt für Züge verwendet, die nicht in der
+Hauptvariante sind.</dd>
+<dt>Vollbild</dt>
+<dd>Schaltet das Hauptfenster in den Vollbildmodus oder verlässt den
+Vollbildmodus. Es ist systemabhängig, ob das Fenstermenü im Vollbildmodus
+angezeigt wird. Um den Vollbildmodus ohne das Benutzen des Fenstermenüs zu
+verlassen, drücken Sie die F11-Taste.</dd>
+</dl>
+<h3>Computer</h3>
+<dl>
+<dt>Computer-Farben</dt>
+<dd>Wählt aus, welche Farben vom Computer gespielt werden.</dd>
+<dt>Spielen</dt>
+<dd>Lässt den Computer einen Zug für die gegenwärtige Farbe spielen. Dies kann
+zum Ändern der Computer-Farbe benutzt werden oder um nach dem Navigieren im
+Spielbaum mit dem Spielen fortzufahren. Wenn der Computer die gegenwärtige
+Farbe nicht bereits spielte, wird er diese Farbe (und nur diese) im weiteren
+Spielverlauf spielen.</dd>
+<dt>Einzelnen Zug spielen</dt>
+<dd>Lässt den Computer einen einzelnen Zug für die gegenwärtige Farbe spielen
+ohne die vom Computer gespielten Farben zu ändern.</dd>
+<dt>Stopp</dt>
+<dd>Bricht die gegenwärtige Zuggenerierung ab. Sie können den Computer
+weiterspielen lassen, indem Sie <i>Spielen</i> auswählen.</dd>
+<dt>Spielstufe</dt>
+<dd>Ändert die Spielstärke des Computers. Höhere Spielstufen sind stärker,
+können aber die Bedenkzeiten des Computers auf langsamen Computern sehr
+verlängern.</dd>
+</dl>
+<h3>Extras</h3>
+<dl>
+<dt>Wertung</dt>
+<dd>Zeigt ein Dialogfenster mit der <a href=
+"become_stronger.html#rating">Wertung</a> des Benutzers in der gegenwärtigen
+Spielvariante.</dd>
+<dt>Spiel analysieren</dt>
+<dd>Führt eine <a href="become_stronger.html#analysis">Spielanalyse</a>
+durch.</dd>
+</dl>
+<h3>Hilfe</h3>
+<dl>
+<dt>Pentobi-Hilfe</dt>
+<dd>Zeigt ein Fenster mit dem Pentobi-Benutzerhandbuch.</dd>
+<dt>Über Pentobi</dt>
+<dd>Zeigt eine Dialogfenster mit Informationen über diese Version von
+Pentobi.</dd>
+</dl>
+<p align="right"><a href="become_stronger.html">Zurück</a> | <a href=
+"shortcuts.html">Weiter</a></p>
+</body>
+</html>
--- /dev/null
+<!-- Subset of the icons in pentobi/icons for use in pentobi_qml -->
+<RCC>
+ <qresource prefix="/qml">
+ <file>icons/pentobi-backward.svg</file>
+ <file>icons/pentobi-beginning.svg</file>
+ <file>icons/pentobi-computer-colors.svg</file>
+ <file>icons/pentobi-end.svg</file>
+ <file>icons/pentobi-forward.svg</file>
+ <file>icons/pentobi-newgame.svg</file>
+ <file>icons/pentobi-next-variation.svg</file>
+ <file>icons/pentobi-play.svg</file>
+ <file>icons/pentobi-previous-variation.svg</file>
+ <file>icons/pentobi-undo.svg</file>
+ </qresource>
+</RCC>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <g id="a">
+ <rect width="4" height="4" x="8" fill="#73d216"/>
+ <path d="m8 4v-4h4l-1 1h-2v2z" fill="#b8e888"/>
+ </g>
+ <use x="4" xlink:href="#a"/>
+ <use y="4" x="4" xlink:href="#a"/>
+ <use y="8" x="4" xlink:href="#a"/>
+ <g id="b">
+ <rect height="4" width="4" y="8" x="4" fill="#3465a4"/>
+ <path d="m4 12v-4h4l-1 1h-2v2z" fill="#82a0c7"/>
+ </g>
+ <use x="4" xlink:href="#b"/>
+ <use y="4" x="4" xlink:href="#b"/>
+ <use y="4" x="8" xlink:href="#b"/>
+ <g id="c">
+ <rect y="4" width="4" height="4" fill="#edd400"/>
+ <path d="m0 8v-4h4l-1 1h-2v2z" fill="#fff283"/>
+ </g>
+ <use y="4" xlink:href="#c"/>
+ <use y="8" xlink:href="#c"/>
+ <use y="8" x="4" xlink:href="#c"/>
+ <g id="d">
+ <rect width="4" height="4" fill="#C00"/>
+ <path d="m0 4v-4h4l-1 1h-2v2z" fill="#f57b7b"/>
+ </g>
+ <use x="4" xlink:href="#d"/>
+ <use y="4" x="4" xlink:href="#d"/>
+ <use y="4" x="8" xlink:href="#d"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a">
+<rect height="7" width="7" x="16" y="2" fill="#73d216"/>
+<path d="m16 9h7v-7l-1 1v 5h-5z" fill="#4e9a06"/>
+<path d="m16 9v-7h7l-1 1h-5v5z" fill="#8ae234"/>
+</g>
+<use xlink:href="#a" x="7"/>
+<use xlink:href="#a" x="7" y="7"/>
+<use xlink:href="#a" x="7" y="14"/>
+<g id="b">
+<rect height="7" width="7" x="9" y="16" fill="#3465a4"/>
+<path d="m9 23h7v-7l-1 1v 5h-5z" fill="#204a87"/>
+<path d="m9 23v-7h7l-1 1h-5v5z" fill="#558bc5"/>
+</g>
+<use xlink:href="#b" x="7"/>
+<use xlink:href="#b" x="7" y="7"/>
+<use xlink:href="#b" x="14" y="7"/>
+<g id="c">
+<rect height="7" width="7" x="2" y="9" fill="#edd400"/>
+<path d="m2 16h7v-7l-1 1v 5h-5z" fill="#c4a000"/>
+<path d="m2 16v-7h7l-1 1h-5v5z" fill="#fce94f"/>
+</g>
+<use xlink:href="#c" y="7"/>
+<use xlink:href="#c" y="14"/>
+<use xlink:href="#c" x="7" y="14"/>
+<g id="d">
+<rect height="7" width="7" x="2" y="2" fill="#C00"/>
+<path d="m2 9h7v-7l-1 1v 5h-5z" fill="#a40000"/>
+<path d="m2 9v-7h7l-1 1h-5v5z" fill="#ef2929"/>
+</g>
+<use xlink:href="#d" x="7"/>
+<use xlink:href="#d" x="7" y="7"/>
+<use xlink:href="#d" x="14" y="7"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a">
+<rect height="14" width="14" x="32" y="4" fill="#73d216"/>
+<path d="m32 18h14v-14l-1 1v12h-12z" fill="#4e9a06"/>
+<path d="m32 18v-14h14l-1 1h-12v12z" fill="#8ae234"/>
+</g>
+<use xlink:href="#a" x="14"/>
+<use xlink:href="#a" x="14" y="14"/>
+<use xlink:href="#a" x="14" y="28"/>
+<g id="b">
+<rect height="14" width="14" x="18" y="32" fill="#3465a4"/>
+<path d="m18 46h14v-14l-1 1v12h-12z" fill="#204a87"/>
+<path d="m18 46v-14h14l-1 1h-12v12z" fill="#558bc5"/>
+</g>
+<use xlink:href="#b" x="14"/>
+<use xlink:href="#b" x="14" y="14"/>
+<use xlink:href="#b" x="28" y="14"/>
+<g id="c">
+<rect height="14" width="14" x="4" y="18" fill="#edd400"/>
+<path d="m4 32h14v-14l-1 1v12h-12z" fill="#c4a000"/>
+<path d="m4 32v-14h14l-1 1h-12v12z" fill="#fce94f"/>
+</g>
+<use xlink:href="#c" y="14"/>
+<use xlink:href="#c" y="28"/>
+<use xlink:href="#c" x="14" y="28"/>
+<g id="d">
+<rect height="14" width="14" x="4" y="4" fill="#C00"/>
+<path d="m4 18h14v-14l-1 1v12h-12z" fill="#a40000"/>
+<path d="m4 18v-14h14l-1 1h-12v12z" fill="#ef2929"/>
+</g>
+<use xlink:href="#d" x="14"/>
+<use xlink:href="#d" x="14" y="14"/>
+<use xlink:href="#d" x="28" y="14"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m6.75 3.5v1.75h7v5.5h-7v1.75l-4.95-4.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m7.5 1.5v3h7v7h-7v3l-7-6.5z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m9.75 4.5v2.75h10v7.5h-10v2.75l-6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m10.5 2.5v4h10v9h-10v4l-8.5-8.5z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path d="m3 2v12h-1.5v-12zm5.75 1.4v1.85h6v5.5h-6v1.95l-5.3-4.8z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+ <path stroke-linejoin="round" stroke="#555753" d="m3.5 1.5v5l6-5v3h6v7h-6v3l-6-5v5h-3v-13z" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path d="m3.75 3.25v15.5h-2.5v-15.5zm8 1.25v2.75h9v7.5h-9v2.75l-6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+ <path stroke-linejoin="round" d="m4.5 2.5v8l8-8v4h9v9h-9v4l-8-8v8h-4v-17z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+ <linearGradient id="a" y2="20" gradientUnits="userSpaceOnUse" y1="0" x2="15" x1="10">
+ <stop stop-color="#dddfe2" offset="0"/>
+ <stop offset="1"/>
+ </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="1.7" height="14" width="15" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".34" height="8" width="11" stroke="#2e3436" y="2.5" x="2.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m2 12.5h12" fill="none"/>
+ <path d="m13 4v6h-10v-2.6789c2.8021-1.303 6.4697-2.752 10-3.321z" fill-opacity=".2"/>
+ <rect stroke-linejoin="round" height="9" width="9" stroke="#2b2d2b" y="6.5" x="6.5" fill="none"/>
+ <rect height="4" width="4" y="7" x="7" fill="#e63232"/>
+ <rect height="4" width="4" y="7" x="11" fill="#8ad83e"/>
+ <rect height="4" width="4" y="11" x="7" fill="#e3cc09"/>
+ <rect height="4" width="4" y="11" x="11" fill="#4b7bb9"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+ <linearGradient id="a" y2="20" gradientUnits="userSpaceOnUse" y1="0" x2="15" x1="10">
+ <stop stop-color="#dddfe2" offset="0"/>
+ <stop offset="1"/>
+ </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="2.3" height="19" width="21" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".5" height="12" width="15" stroke="#2e3436" y="3.5" x="3.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m3 17.5h16" fill="none"/>
+ <path d="m18 6.5v8.5h-14v-4c4-2 9-4 14-4.75z" fill-opacity=".2"/>
+ <rect stroke-linejoin="round" height="11" width="11" stroke="#2b2d2b" y="10.5" x="10.5" fill="none"/>
+ <rect height="5" width="5" y="11" x="11" fill="#e63232"/>
+ <rect height="5" width="5" y="11" x="16" fill="#8ad83e"/>
+ <rect height="5" width="5" y="16" x="11" fill="#e3cc09"/>
+ <rect height="5" width="5" y="16" x="16" fill="#4b7bb9"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path d="m13 2v12h1.5v-12zm-5.75 1.4v1.85h-6v5.5h6v1.95l5.3-4.8z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+ <path stroke-linejoin="round" stroke="#555753" d="m12.5 1.5v5l-6-5v3h-6v7h6v3l6-5v5h3v-13z" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m18.25 3.25v15.5h2.5v-15.5zm-8 1.25v2.75h-9v7.5h9v2.75l6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m17.5 2.5v8l-8-8v4h-9v9h9v4l8-8v8h4v-17z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m5.5 3-5 5 5 5v-3.5h5v3.5l5-5-5-5v3.5h-5z" stroke="#555" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m13 5.5-5-5-5 5h3.5v5h-3.5l5 5 5-5h-3.5v-5z" stroke="#555" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m9.25 3.5v1.75h-7v5.5h7v1.75l4.95-4.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m8.5 1.5v3h-7v7h7v3l7-6.5z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m12.25 4.5v2.75h-10v7.5h10v2.75l6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m11.5 2.5v4h-10v9h10v4l8.5-8.5z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="12" width="12" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+ <path d="m1 8v-7h7l-1 1h-5v5z" fill="#999B96"/>
+ <path d="m1 8h7v-7l-1 1v5h-5" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(7)"/>
+ <use xlink:href="#a" transform="translate(0 7)"/>
+ <use xlink:href="#a" transform="translate(7 7)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="15" width="15" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path d="m11 .51c0.54 0 1 2 1.4 2.3 .44 .3 2.8 .16 2.9 .63 .18 .46-1.8 1.6-2 2-.17 .48 .7 2.4 .25 2.8-.43 .3-2.1-1.1-2.7-1.1-.55 0-2.3 1.3-2.8 1.1s0.4-2.2 .2-2.6c-.1-.4-2.1-1.6-2-2.1 .2-.4 2.4-.3 2.9-.5 .5-.4 .8-2.5 1.8-2.5z" stroke="#edd400" fill="#fff"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="18" width="18" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+ <path d="m1 11v-10h10l-1 1h-8v8z" fill="#999B96"/>
+ <path d="m1 11h10v-10l-1 1v8h-8" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(10)"/>
+ <use xlink:href="#a" transform="translate(0 10)"/>
+ <use xlink:href="#a" transform="translate(10 10)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="21" width="21" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path stroke="#edd400" d="m16 .64c.69 .02 1.4 2.6 2 3 .53 .36 3.3 .11 3.5 .7 .19 .62-2.2 2.1-2.5 2.7-.2 .58 .91 3 .37 3.3-.57 .36-2.8-1.4-3.5-1.4-.66 0 -2.8 1.7-3.3 1.4-.55-.4 .52-2.9 .3-3.5-.2-.58-2.6-1.9-2.4-2.5 .24-.61 3.1-.43 3.7-.81 .53-.36 1.1-2.9 1.8-2.9z" fill="#fff"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m9.5 13 5-5l-5-5v3.5h-9v3h9z" stroke="#555" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m3.5 9.25h1.75v-7h5.5v7h1.75l-4.5 4.95z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m1.5 8.5h3v-7h7v7h3l-6.5 7z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m4.5 13.25h2.75v-10h7.5v10h2.75l-6.5 6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m2.5 12.5h4v-10h9v10h4l-8.5 8.5z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m3.5 1.5-2 2 4 4-4 4 2 2 4-4 4 4 2-2-4-4 4-4-2-2-4 4-4-4z" stroke="#555" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+ <linearGradient id="a" gradientUnits="userSpaceOnUse" x2="15" x1="10" y2="20">
+ <stop stop-color="#dddfe2" offset="0"/>
+ <stop offset="1"/>
+ </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="2.3" height="19" width="21" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".5" height="12" width="15" stroke="#2e3436" y="3.5" x="3.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m3 17.5h16" fill="none"/>
+ <path d="m18 6.5v8.5h-14v-4c4-2 9-4 14-4.75z" fill-opacity=".2"/>
+ <path stroke-linejoin="round" d="m21.5 14.5-11 7v-13.5z" stroke="#2e3436" fill="#eeeeec"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs>
+ <linearGradient id="a" y2="20" gradientUnits="userSpaceOnUse" x2="15" x1="10">
+ <stop stop-color="#dddfe2" offset="0"/>
+ <stop offset="1"/>
+ </linearGradient>
+ </defs>
+ <rect stroke-linejoin="round" ry="2.31" height="19" width="21" stroke="#555753" y=".5" x=".5" fill="#dfe2dc"/>
+ <rect stroke-linejoin="round" ry=".5" height="12" width="15" stroke="#2e3436" y="3.5" x="3.5" fill="url(#a)"/>
+ <path stroke="#555753" d="m3 17.5h16" fill="none"/>
+ <path d="m18 6.5v8.5h-14v-4c4-2 9-4 14-4.75z" fill-opacity=".2"/>
+ <path stroke-linejoin="round" d="m21.5 15-9 6.5v-13z" stroke="#2e3436" fill="#eeeeec"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+</defs>
+<path stroke-linejoin="round" d="m5.5 3-5 5 5 5v-3.5h9v-3h-9z" stroke="#555" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m3.5 7.75h1.75v7h5.5v-7h1.75l-4.5-4.95z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m1.5 8.5h3v7h7v-7h3l-6.5 -7z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m4.5 9.75h2.75v10h7.5v-10h2.75l-6.5-6.5z" stroke="#babdb6" stroke-width="1.5" fill="#888a85"/>
+<path stroke-linejoin="round" d="m2.5 10.5h4v10h9v-10h4l-8.5-8.5z" stroke="#555753" fill="none"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="4" y=".5" width="8" height="2" fill="#9b9f95" stroke="#878A84"/>
+ <path d="m6 13-5-11 .5-1.5h2.5l6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <path d="m10 13 5-11-.5-1.5h-2.5l-6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <circle cy="11" cx="8" r="5" fill="#a08300"/>
+ <circle stroke="#edd400" cy="11" cx="8" r="3.5" fill="#fce94f"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect x="5" y=".5" width="12" height="3" fill="#9b9f95" stroke="#878A84"/>
+ <path d="m6 13-5-11 .5-1.5h3.5l6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <path d="m16 13 5-11-.5-1.5h-3.5l-6 12z" fill="#babdb6" stroke="#878A84" stroke-linejoin="round"/>
+ <circle cy="15" stroke="#a08300" cx="11" r="6.5" fill="#edd400"/>
+ <circle cy="15" stroke="#bf9c00" cx="11" r="4.5" fill="#fce94f"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" stroke="#555" d="m7.5 .47c3.9 0 7 3.1 7 7h-3c0-2.2-1.8-4-4-4s-4 1.8-4 4c0 1.1 .48 2.2 1.2 2.9l1.9-1.9v6h-6l2-2c-1.3-1.3-2.1-3.1-2.1-5 0-3.9 3.1-7 7-7z" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path stroke-linejoin="round" stroke="#555" d="m7.5 .47c-3.9 0-7 3.1-7 7h3c0-2.2 1.8-4 4-4s4 1.8 4 4c0 1.1-.48 2.2-1.2 2.9l-1.9-1.9v6h6l-2-2c1.3-1.3 2.1-3.1 2.1-5 0-3.9-3.1-7-7-7z" fill="#babdb6"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="12" width="12" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+ <path d="m1 8v-7h7l-1 1h-5v5z" fill="#999B96"/>
+ <path d="m1 8h7v-7l-1 1v5h-5" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(7)"/>
+ <use xlink:href="#a" transform="translate(0 7)"/>
+ <use xlink:href="#a" transform="translate(7 7)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="15" width="15" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path stroke="#204a87" d="m1.5 9 5-5v3.6s2.1 0 3.5 1.4c1.1 1.1 1.5 4.3 1.5 4.3s-1.4-1.6-2.3-2.2c-1.2-.77-2.7-.64-2.7-.64v3.6z" fill="#729fcf"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <rect height="18" width="18" y="2" x="2" fill="#babdb6"/>
+ <g id="a">
+ <path d="m1 11v-10h10l-1 1h-8v8z" fill="#999B96"/>
+ <path d="m1 11h10v-10l-1 1v8h-8" fill="#c8cdc3"/>
+ </g>
+ <use xlink:href="#a" transform="translate(10)"/>
+ <use xlink:href="#a" transform="translate(0 10)"/>
+ <use xlink:href="#a" transform="translate(10 10)"/>
+ <rect stroke-linejoin="round" ry="1.5" height="21" width="21" stroke="#878A84" y=".5" x=".5" fill="none"/>
+ <path stroke="#204a87" d="m1.5 13 7-7v5s3 0 4.9 2c1.5 1.5 2.1 6 2.1 6s-2-2.3-3.3-3.1c-1.7-1.1-3.7-.89-3.7-.89v5z" fill="#729fcf"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a">
+<rect height="10" width="10" x="24" y="4" fill="#73d216"/>
+<path d="m24 14h10v-10l-1 1v 8h-8z" fill="#4e9a06"/>
+<path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+</g>
+<use xlink:href="#a" x="10"/>
+<use xlink:href="#a" x="10" y="10"/>
+<use xlink:href="#a" x="10" y="20"/>
+<g id="b">
+<rect height="10" width="10" x="14" y="24" fill="#3465a4"/>
+<path d="m14 34h10v-10l-1 1v 8h-8z" fill="#204a87"/>
+<path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+</g>
+<use xlink:href="#b" x="10"/>
+<use xlink:href="#b" x="10" y="10"/>
+<use xlink:href="#b" x="20" y="10"/>
+<g id="c">
+<rect height="10" width="10" x="4" y="14" fill="#edd400"/>
+<path d="m4 24h10v-10l-1 1v 8h-8z" fill="#c4a000"/>
+<path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+</g>
+<use xlink:href="#c" y="10"/>
+<use xlink:href="#c" y="20"/>
+<use xlink:href="#c" x="10" y="20"/>
+<g id="d">
+<rect height="10" width="10" x="4" y="4" fill="#C00"/>
+<path d="m4 14h10v-10l-1 1v 8h-8z" fill="#a40000"/>
+<path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+</g>
+<use xlink:href="#d" x="10"/>
+<use xlink:href="#d" x="10" y="10"/>
+<use xlink:href="#d" x="20" y="10"/>
+</svg>
--- /dev/null
+# Config file to override installation settings such that the executable
+# can be tested without installation
+BooksDir=@CMAKE_SOURCE_DIR@/src/books
+HelpDir=@CMAKE_SOURCE_DIR@/src/pentobi/help
+TranslationsPentobiDir=@CMAKE_BINARY_DIR@/src/pentobi
+TranslationsLibPentobiGuiDir=@CMAKE_BINARY_DIR@/src/libpentobi_gui
--- /dev/null
+ IDI_ICON1 ICON DISCARDABLE "pentobi.ico"
--- /dev/null
+<!DOCTYPE RCC>
+<RCC version="1.0">
+<qresource prefix="/pentobi">
+<file>icons/pentobi.png</file>
+<file>icons/pentobi-16.png</file>
+<file>icons/pentobi-32.png</file>
+<file>icons/pentobi-backward.png</file>
+<file>icons/pentobi-backward-16.png</file>
+<file>icons/pentobi-beginning.png</file>
+<file>icons/pentobi-beginning-16.png</file>
+<file>icons/pentobi-computer-colors.png</file>
+<file>icons/pentobi-computer-colors-16.png</file>
+<file>icons/pentobi-end.png</file>
+<file>icons/pentobi-end-16.png</file>
+<file>icons/pentobi-flip-horizontal.png</file>
+<file>icons/pentobi-flip-vertical.png</file>
+<file>icons/pentobi-forward.png</file>
+<file>icons/pentobi-forward-16.png</file>
+<file>icons/pentobi-newgame.png</file>
+<file>icons/pentobi-newgame-16.png</file>
+<file>icons/pentobi-next-piece.png</file>
+<file>icons/pentobi-next-variation.png</file>
+<file>icons/pentobi-next-variation-16.png</file>
+<file>icons/pentobi-piece-clear.png</file>
+<file>icons/pentobi-play.png</file>
+<file>icons/pentobi-play-16.png</file>
+<file>icons/pentobi-previous-piece.png</file>
+<file>icons/pentobi-previous-variation.png</file>
+<file>icons/pentobi-previous-variation-16.png</file>
+<file>icons/pentobi-rated-game.png</file>
+<file>icons/pentobi-rated-game-16.png</file>
+<file>icons/pentobi-rotate-left.png</file>
+<file>icons/pentobi-rotate-right.png</file>
+<file>icons/pentobi-undo.png</file>
+<file>icons/pentobi-undo-16.png</file>
+</qresource>
+</RCC>
--- /dev/null
+<!DOCTYPE RCC>
+<RCC version="1.0">
+<qresource prefix="/pentobi">
+<file>icons/pentobi@2x.png</file>
+<file>icons/pentobi-16@2x.png</file>
+<file>icons/pentobi-32@2x.png</file>
+<file>icons/pentobi-backward@2x.png</file>
+<file>icons/pentobi-backward-16@2x.png</file>
+<file>icons/pentobi-beginning@2x.png</file>
+<file>icons/pentobi-beginning-16@2x.png</file>
+<file>icons/pentobi-computer-colors@2x.png</file>
+<file>icons/pentobi-computer-colors-16@2x.png</file>
+<file>icons/pentobi-end@2x.png</file>
+<file>icons/pentobi-end-16@2x.png</file>
+<file>icons/pentobi-flip-horizontal@2x.png</file>
+<file>icons/pentobi-flip-vertical@2x.png</file>
+<file>icons/pentobi-forward@2x.png</file>
+<file>icons/pentobi-forward-16@2x.png</file>
+<file>icons/pentobi-newgame@2x.png</file>
+<file>icons/pentobi-newgame-16@2x.png</file>
+<file>icons/pentobi-next-piece@2x.png</file>
+<file>icons/pentobi-next-variation@2x.png</file>
+<file>icons/pentobi-next-variation-16@2x.png</file>
+<file>icons/pentobi-piece-clear@2x.png</file>
+<file>icons/pentobi-play@2x.png</file>
+<file>icons/pentobi-play-16@2x.png</file>
+<file>icons/pentobi-previous-piece@2x.png</file>
+<file>icons/pentobi-previous-variation@2x.png</file>
+<file>icons/pentobi-previous-variation-16@2x.png</file>
+<file>icons/pentobi-rated-game@2x.png</file>
+<file>icons/pentobi-rated-game-16@2x.png</file>
+<file>icons/pentobi-rotate-left@2x.png</file>
+<file>icons/pentobi-rotate-right@2x.png</file>
+<file>icons/pentobi-undo@2x.png</file>
+<file>icons/pentobi-undo-16@2x.png</file>
+</qresource>
+</RCC>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>MainWindow</name>
+ <message numerus="yes">
+ <source>%n move(s)</source>
+ <translation>
+ <numerusform>%n move</numerusform>
+ <numerusform>%n moves</numerusform>
+ </translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+ <name>AnalyzeGameWidget</name>
+ <message>
+ <source>Win</source>
+ <translation>Gewinn</translation>
+ </message>
+ <message>
+ <source>Loss</source>
+ <translation>Verlust</translation>
+ </message>
+ <message>
+ <source>Running game analysis...</source>
+ <translation>Spiel wird analysiert ...</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeGameWindow</name>
+ <message>
+ <source>Game Analysis</source>
+ <translation>Spielanalyse</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeSpeedDialog</name>
+ <message>
+ <source>Fast</source>
+ <translation>Schnell</translation>
+ </message>
+ <message>
+ <source>Normal</source>
+ <translation>Normal</translation>
+ </message>
+ <message>
+ <source>Slow</source>
+ <translation>Langsam</translation>
+ </message>
+ <message>
+ <source>Analysis speed:</source>
+ <translation>Analysegeschwindigkeit:</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <source>About Pentobi</source>
+ <translation>Über Pentobi</translation>
+ </message>
+ <message>
+ <source>Next &Color</source>
+ <translation>Nächste &Farbe</translation>
+ </message>
+ <message>
+ <source>S&etup Mode</source>
+ <translation>&Stellungsaufbau</translation>
+ </message>
+ <message>
+ <source>Could not read file '%1'</source>
+ <translation>Datei '%1' konnte nicht gelesen werden</translation>
+ </message>
+ <message>
+ <source>The file is not a valid Blokus SGF file.</source>
+ <translation>Die Datei ist keine gültige Blokus-SGF-Datei.</translation>
+ </message>
+ <message>
+ <source>Truncate this subtree?</source>
+ <translation>Diesen Teilbaum abschneiden?</translation>
+ </message>
+ <message>
+ <source>This position and all following moves and variations will be removed from the game tree.</source>
+ <translation>Diese Brettstellung und alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+ </message>
+ <message>
+ <source>Truncate</source>
+ <translation>Abschneiden</translation>
+ </message>
+ <message>
+ <source>Truncate children?</source>
+ <translation>Kindknoten abschneiden?</translation>
+ </message>
+ <message>
+ <source>All following moves and variations will be removed from the game tree.</source>
+ <translation>Alle folgenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+ </message>
+ <message>
+ <source>Truncate Children</source>
+ <translation>Kindknoten abschneiden</translation>
+ </message>
+ <message>
+ <source>Could not delete %1</source>
+ <translation>%1 konnte nicht gelöscht werden</translation>
+ </message>
+ <message>
+ <source>Rated game</source>
+ <translation>Gewertetes Spiel</translation>
+ </message>
+ <message>
+ <source>Continuing unfinished rated game.</source>
+ <translation>Unbeendetes gewertetes Spiel wird fortgesetzt.</translation>
+ </message>
+ <message>
+ <source>You play %1 in this game.</source>
+ <translation>Sie spielen %1 in diesem Spiel.</translation>
+ </message>
+ <message>
+ <source>&copy; 2011&ndash;%1 Markus Enzenberger</source>
+ <translation>&copy; 2011&ndash;%1 Markus Enzenberger</translation>
+ </message>
+ <message>
+ <source>Analyze Game</source>
+ <translation>Spiel analysieren</translation>
+ </message>
+ <message>
+ <source>&About Pentobi</source>
+ <translation>Über &Pentobi</translation>
+ </message>
+ <message>
+ <source>&Analyze Game...</source>
+ <translation>Spiel &analysieren ...</translation>
+ </message>
+ <message>
+ <source>B&ackward</source>
+ <translation>&Zurück</translation>
+ </message>
+ <message>
+ <source>Go one move backward</source>
+ <translation>Einen Zug zurück gehen</translation>
+ </message>
+ <message>
+ <source>Back to &Main Variation</source>
+ <translation>Zurück zu Hau&ptvariante</translation>
+ </message>
+ <message>
+ <source>&Bad</source>
+ <translation>Schl&echt</translation>
+ </message>
+ <message>
+ <source>&Beginning</source>
+ <translation>&Anfang</translation>
+ </message>
+ <message>
+ <source>Go to beginning of game</source>
+ <translation>Zum Anfang des Spiels gehen</translation>
+ </message>
+ <message>
+ <source>Beginning of Bran&ch</source>
+ <translation>Anfang der Verz&weigung</translation>
+ </message>
+ <message>
+ <source>Clear Piece</source>
+ <translation>Spielstein löschen</translation>
+ </message>
+ <message>
+ <source>&Computer Colors</source>
+ <translation>&Computer-Farben</translation>
+ </message>
+ <message>
+ <source>Set the colors played by the computer</source>
+ <translation>Die vom Computer gespielten Farben festlegen</translation>
+ </message>
+ <message>
+ <source>C&oordinates</source>
+ <translation>K&oordinaten</translation>
+ </message>
+ <message>
+ <source>&Delete All Variations</source>
+ <translation>Alle &Varianten löschen</translation>
+ </message>
+ <message>
+ <source>&Doubtful</source>
+ <translation>&Zweifelhaft</translation>
+ </message>
+ <message>
+ <source>&End</source>
+ <translation>&Ende</translation>
+ </message>
+ <message>
+ <source>Go to end of moves</source>
+ <translation>Zum Ende der Züge gehen</translation>
+ </message>
+ <message>
+ <source>&ASCII Art</source>
+ <translation>&ASCII-Art</translation>
+ </message>
+ <message>
+ <source>I&mage</source>
+ <translation>&Grafik</translation>
+ </message>
+ <message>
+ <source>&Find Move</source>
+ <translation>Zug fin&den</translation>
+ </message>
+ <message>
+ <source>Flip Horizontally</source>
+ <translation>Waagrecht umdrehen</translation>
+ </message>
+ <message>
+ <source>Flip Vertically</source>
+ <translation>Senkrecht umdrehen</translation>
+ </message>
+ <message>
+ <source>&Forward</source>
+ <translation>&Vorwärts</translation>
+ </message>
+ <message>
+ <source>Go one move forward</source>
+ <translation>Einen Zug vorwärts gehen</translation>
+ </message>
+ <message>
+ <source>&Fullscreen</source>
+ <translation>Voll&bild</translation>
+ </message>
+ <message>
+ <source>Ga&me Info</source>
+ <translation>Spielinf&ormation</translation>
+ </message>
+ <message>
+ <source>St&op</source>
+ <translation>St&opp</translation>
+ </message>
+ <message>
+ <source>&Duo</source>
+ <translation>&Duo</translation>
+ </message>
+ <message>
+ <source>&Good</source>
+ <translation>&Gut</translation>
+ </message>
+ <message>
+ <source>Game analysis is only possible in the main variation.</source>
+ <translation>Spielanalyse ist nur in der Hauptvariante möglich.</translation>
+ </message>
+ <message>
+ <source>Find Next &Comment</source>
+ <translation>Nächsten &Kommentar finden</translation>
+ </message>
+ <message>
+ <source>&Go to Move...</source>
+ <translation>&Gehe zu Zug ...</translation>
+ </message>
+ <message>
+ <source>Pentobi &Help</source>
+ <translation>Pentobi-&Hilfe</translation>
+ </message>
+ <message>
+ <source>I&nteresting</source>
+ <translation>I&nteressant</translation>
+ </message>
+ <message>
+ <source>&Keep Only Position</source>
+ <translation>Nur &Brettstellung behalten</translation>
+ </message>
+ <message>
+ <source>Keep Only &Subtree</source>
+ <translation>Nur &Teilbaum behalten</translation>
+ </message>
+ <message>
+ <source>Leave Fullscreen</source>
+ <translation>Vollbild verlassen</translation>
+ </message>
+ <message>
+ <source>M&ake Main Variation</source>
+ <translation>Zu &Hauptvariante machen</translation>
+ </message>
+ <message>
+ <source>Move Variation D&own</source>
+ <translation>Variante nach &unten schieben</translation>
+ </message>
+ <message>
+ <source>Move Variation &Up</source>
+ <translation>Variante nach &oben schieben</translation>
+ </message>
+ <message>
+ <source>&1</source>
+ <translation>&1</translation>
+ </message>
+ <message>
+ <source>&2</source>
+ <translation>&2</translation>
+ </message>
+ <message>
+ <source>&3</source>
+ <translation>&3</translation>
+ </message>
+ <message>
+ <source>&4</source>
+ <translation>&4</translation>
+ </message>
+ <message>
+ <source>&5</source>
+ <translation>&5</translation>
+ </message>
+ <message>
+ <source>&6</source>
+ <translation>&6</translation>
+ </message>
+ <message>
+ <source>&7</source>
+ <translation>&7</translation>
+ </message>
+ <message>
+ <source>&8</source>
+ <translation>&8</translation>
+ </message>
+ <message>
+ <source>&None</source>
+ <comment>move numbers</comment>
+ <translation>&Keine</translation>
+ </message>
+ <message>
+ <source>Next Piece</source>
+ <translation>Nächster Spielstein</translation>
+ </message>
+ <message>
+ <source>&Next Variation</source>
+ <translation>&Nächste Variante</translation>
+ </message>
+ <message>
+ <source>Go to next variation</source>
+ <translation>Zur nächsten Variante gehen</translation>
+ </message>
+ <message>
+ <source>&Rated Game</source>
+ <translation>Ge&wertetes Spiel</translation>
+ </message>
+ <message>
+ <source>&New</source>
+ <translation>&Neu</translation>
+ </message>
+ <message>
+ <source>Start a new game</source>
+ <translation>Ein neues Spiel beginnen</translation>
+ </message>
+ <message>
+ <source>N&one</source>
+ <comment>move annotation</comment>
+ <translation>&Keine</translation>
+ </message>
+ <message>
+ <source>&Open...</source>
+ <translation>Öffn&en ...</translation>
+ </message>
+ <message>
+ <source>&Play</source>
+ <translation>&Spielen</translation>
+ </message>
+ <message>
+ <source>Play &Single Move</source>
+ <translation>&Einzelnen Zug spielen</translation>
+ </message>
+ <message>
+ <source>Previous Piece</source>
+ <translation>Vorheriger Spielstein</translation>
+ </message>
+ <message>
+ <source>&Previous Variation</source>
+ <translation>Vor&herige Variante</translation>
+ </message>
+ <message>
+ <source>Go to previous variation</source>
+ <translation>Zur vorherigen Variante gehen</translation>
+ </message>
+ <message>
+ <source>Rotate Anticlockwise</source>
+ <translation>Gegen den Uhrzeigersinn drehen</translation>
+ </message>
+ <message>
+ <source>Rotate Clockwise</source>
+ <translation>Im Uhrzeigersinn drehen</translation>
+ </message>
+ <message>
+ <source>&Quit</source>
+ <translation>&Beenden</translation>
+ </message>
+ <message>
+ <source>&Save</source>
+ <translation>&Speichern</translation>
+ </message>
+ <message>
+ <source>Save &As...</source>
+ <translation>Speichern &unter ...</translation>
+ </message>
+ <message>
+ <source>&Comment</source>
+ <translation>&Kommentar</translation>
+ </message>
+ <message>
+ <source>&Rating</source>
+ <translation>&Wertung</translation>
+ </message>
+ <message>
+ <source>&No Text</source>
+ <translation>&Kein Text</translation>
+ </message>
+ <message>
+ <source>Text &Beside Icons</source>
+ <translation>Text n&eben Symbolen</translation>
+ </message>
+ <message>
+ <source>Text Bel&ow Icons</source>
+ <translation>Text &unter Symbolen</translation>
+ </message>
+ <message>
+ <source>&Text Only</source>
+ <translation>&Nur Text</translation>
+ </message>
+ <message>
+ <source>&System Default</source>
+ <translation>&Systemvorgabe</translation>
+ </message>
+ <message>
+ <source>&Truncate</source>
+ <translation>&Abschneiden</translation>
+ </message>
+ <message>
+ <source>Truncate C&hildren</source>
+ <translation>&Kindknoten abschneiden</translation>
+ </message>
+ <message>
+ <source>Show &Variations</source>
+ <translation>&Varianten zeigen</translation>
+ </message>
+ <message>
+ <source>&Undo Move</source>
+ <translation>Zug rück&gängig</translation>
+ </message>
+ <message>
+ <source>V&ery Bad</source>
+ <translation>Seh&r schlecht</translation>
+ </message>
+ <message>
+ <source>&Very Good</source>
+ <translation>&Sehr gut</translation>
+ </message>
+ <message>
+ <source>&Edit</source>
+ <translation>&Bearbeiten</translation>
+ </message>
+ <message>
+ <source>&Move Annotation</source>
+ <translation>&Zugkommentierung</translation>
+ </message>
+ <message>
+ <source>&View</source>
+ <translation>&Ansicht</translation>
+ </message>
+ <message>
+ <source>Toolbar T&ext</source>
+ <translation>Werkzeugleistent&ext</translation>
+ </message>
+ <message>
+ <source>&Computer</source>
+ <translation>&Computer</translation>
+ </message>
+ <message>
+ <source>&Tools</source>
+ <translation>E&xtras</translation>
+ </message>
+ <message>
+ <source>&Help</source>
+ <translation>&Hilfe</translation>
+ </message>
+ <message>
+ <source>Delete all variations?</source>
+ <translation>Alle Varianten löschen?</translation>
+ </message>
+ <message>
+ <source>All variations but the main variation will be removed from the game tree.</source>
+ <translation>Alle Varianten außer der Hauptvariante werden aus dem Spielbaum entfernt.</translation>
+ </message>
+ <message>
+ <source>Delete Variations</source>
+ <translation>Varianten löschen</translation>
+ </message>
+ <message>
+ <source>&Toolbar</source>
+ <translation>&Werkzeugleiste</translation>
+ </message>
+ <message>
+ <source>Text files (*.txt);;All files (*)</source>
+ <translation>Textdateien (*.txt);;Alle Dateien (*)</translation>
+ </message>
+ <message>
+ <source>No comment found</source>
+ <translation>Kein Kommentar gefunden</translation>
+ </message>
+ <message>
+ <source>Blokus games (*.blksgf);;All files (*)</source>
+ <translation>Blokus-Partien (*.blksgf);;Alle Dateien (*)</translation>
+ </message>
+ <message>
+ <source>Move number:</source>
+ <translation>Zugnummer:</translation>
+ </message>
+ <message>
+ <source>Go to Move</source>
+ <translation>Gehe zu Zug</translation>
+ </message>
+ <message>
+ <source>Keep only position?</source>
+ <translation>Nur Brettstellung behalten?</translation>
+ </message>
+ <message>
+ <source>All previous and following moves and variations will be removed from the game tree.</source>
+ <translation>Alle vorhergehenden und nachfolgenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+ </message>
+ <message>
+ <source>Keep only subtree?</source>
+ <translation>Nur Teilbaum behalten?</translation>
+ </message>
+ <message>
+ <source>All previous moves and variations will be removed from the game tree.</source>
+ <translation>Alle vorhergehenden Züge und Varianten werden aus dem Spielbaum entfernt.</translation>
+ </message>
+ <message>
+ <source>Start rated game?</source>
+ <translation>Gewertetes Spiel beginnen?</translation>
+ </message>
+ <message>
+ <source>In this game, you play %1 against Pentobi level&nbsp;%2.</source>
+ <translation>In diesem Spiel spielen Sie %1 gegen Pentobi Spielstufe&nbsp;%2.</translation>
+ </message>
+ <message>
+ <source>&Start Game</source>
+ <translation>&Spiel beginnen</translation>
+ </message>
+ <message>
+ <source>Pentobi %1 (level %2)</source>
+ <extracomment>The first argument is the version of Pentobi</extracomment>
+ <translation>Pentobi %1 (Stufe %2)</translation>
+ </message>
+ <message>
+ <source>Human</source>
+ <translation>Mensch</translation>
+ </message>
+ <message>
+ <source>Open</source>
+ <translation>Öffnen</translation>
+ </message>
+ <message>
+ <source>The file could not be saved.</source>
+ <translation>Die Datei konnte nicht gespeichert werden.</translation>
+ </message>
+ <message>
+ <source>%1: %2</source>
+ <extracomment>Error message if file cannot be saved. %1 is replaced by the file name, %2 by the error message of the operating system.</extracomment>
+ <translation>%1: %2</translation>
+ </message>
+ <message>
+ <source>Untitled Game.blksgf</source>
+ <translation>Unbenanntes Spiel.blksgf</translation>
+ </message>
+ <message>
+ <source>Untitled Game %1.blksgf</source>
+ <translation>Unbenanntes Spiel %1.blksgf</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation>Speichern</translation>
+ </message>
+ <message>
+ <source>Pentobi</source>
+ <translation>Pentobi</translation>
+ </message>
+ <message>
+ <source>Version %1</source>
+ <translation>Version %1</translation>
+ </message>
+ <message>
+ <source>Computer opponent for the board game Blokus.</source>
+ <translation>Computer-Gegner für das Brettspiel Blokus.</translation>
+ </message>
+ <message>
+ <source>The file has been modified.</source>
+ <translation>Die Datei wurde geändert.</translation>
+ </message>
+ <message>
+ <source>Do you want to save your changes?</source>
+ <translation>Änderungen speichern?</translation>
+ </message>
+ <message>
+ <source>&Don't Save</source>
+ <translation>&Nicht speichern</translation>
+ </message>
+ <message>
+ <source>The current game is not finished.</source>
+ <translation>Das gegenwärtige Spiel ist nicht beendet.</translation>
+ </message>
+ <message>
+ <source>Do you want to abort the game?</source>
+ <translation>Möchten Sie das Spiel verwerfen?</translation>
+ </message>
+ <message>
+ <source>&Abort Game</source>
+ <translation>Spiel &verwerfen</translation>
+ </message>
+ <message>
+ <source>&9</source>
+ <translation>&9</translation>
+ </message>
+ <message>
+ <source>&All with Number</source>
+ <translation>&Alle mit Nummer</translation>
+ </message>
+ <message>
+ <source>Last with &Dot</source>
+ <translation>Letzter mit &Punkt</translation>
+ </message>
+ <message>
+ <source>&Last with Number</source>
+ <translation>Letzter mit &Nummer</translation>
+ </message>
+ <message>
+ <source>Start a rated game</source>
+ <translation>Ein gewertetes Spiel beginnen</translation>
+ </message>
+ <message>
+ <source>Classic (&3 Players)</source>
+ <translation>Klassisch (&3 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Game</source>
+ <translation>&Spiel</translation>
+ </message>
+ <message>
+ <source>Game &Variant</source>
+ <translation>Spiel&variante</translation>
+ </message>
+ <message>
+ <source>Open R&ecent</source>
+ <translation>&Zuletzt benutzte Dateien</translation>
+ </message>
+ <message>
+ <source>E&xport</source>
+ <translation>E&xportieren</translation>
+ </message>
+ <message>
+ <source>G&o</source>
+ <translation>&Gehe zu</translation>
+ </message>
+ <message>
+ <source>&Move Marking</source>
+ <translation>&Zugmarkierung</translation>
+ </message>
+ <message>
+ <source>Export Image</source>
+ <translation>Grafik exportieren</translation>
+ </message>
+ <message>
+ <source>Image size:</source>
+ <translation>Bildgröße:</translation>
+ </message>
+ <message>
+ <source>The end of the tree was reached.</source>
+ <translation>Das Ende des Spielbaums wurde erreicht.</translation>
+ </message>
+ <message>
+ <source>Continue the search from the start of the tree?</source>
+ <translation>Die Suche vom Beginn des Spielbaums fortsetzen?</translation>
+ </message>
+ <message>
+ <source>Continue From Start</source>
+ <translation>Suche vom Beginn fortsetzen</translation>
+ </message>
+ <message>
+ <source>Show &Rating</source>
+ <translation>&Wertung zeigen</translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation>Pentobi-Hilfe</translation>
+ </message>
+ <message>
+ <source>Keep Only Position</source>
+ <translation>Nur Brettstellung behalten</translation>
+ </message>
+ <message>
+ <source>Keep Only Subtree</source>
+ <translation>Nur Teilbaum behalten</translation>
+ </message>
+ <message>
+ <source>[*]%1</source>
+ <translation>[*]%1</translation>
+ </message>
+ <message>
+ <source>Setup mode cannot be used if moves have been played.</source>
+ <translation>Stellungsaufbau-Modus kann nicht benutzt werden, wenn Züge gespielt wurden.</translation>
+ </message>
+ <message>
+ <source>Setup mode</source>
+ <translation>Stellungsaufbau</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie.</source>
+ <translation>Das Spiel endet in einem Unentschieden.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between all colors.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen allen Farben.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Blue, Yellow and Red.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Blue, Yellow and Green.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Blue, Red and Green.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Yellow, Red and Green.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Blue and Yellow.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Blau und Gelb.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Blue and Red.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Blau und Rot.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Blue and Green.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Blau und Grün.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Yellow and Red.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Gelb und Rot.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Yellow and Green.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Gelb und Grün.</translation>
+ </message>
+ <message>
+ <source>The game ends in a tie between Red and Green.</source>
+ <translation>Das Spiel endet in einem Unentschieden zwischen Rot und Grün.</translation>
+ </message>
+ <message>
+ <source>Blue wins.</source>
+ <translation>Blau gewinnt.</translation>
+ </message>
+ <message>
+ <source>Yellow wins.</source>
+ <translation>Gelb gewinnt.</translation>
+ </message>
+ <message>
+ <source>Red wins.</source>
+ <translation>Rot gewinnt.</translation>
+ </message>
+ <message>
+ <source>Green wins.</source>
+ <translation>Grün gewinnt.</translation>
+ </message>
+ <message>
+ <source>Your rating has increased from %1 to %2.</source>
+ <translation>Ihre Wertung hat sich von %1 auf %2 erhöht.</translation>
+ </message>
+ <message>
+ <source>Your rating stays at %1.</source>
+ <translation>Ihre Wertung bleibt auf %1.</translation>
+ </message>
+ <message>
+ <source>Your rating has decreased from %1 to %2.</source>
+ <translation>Ihre Wertung hat sich von %1 auf %2 erniedrigt.</translation>
+ </message>
+ <message>
+ <source>Error in file '%1'</source>
+ <translation>Fehler in Datei '%1'</translation>
+ </message>
+ <message>
+ <source>Move %1</source>
+ <translation>Zug %1</translation>
+ </message>
+ <message numerus="yes">
+ <source>%n move(s)</source>
+ <translation>
+ <numerusform>%n Zug</numerusform>
+ <numerusform>%n Züge</numerusform>
+ </translation>
+ </message>
+ <message>
+ <source>Move %1 of %2</source>
+ <translation>Zug %1 von %2</translation>
+ </message>
+ <message>
+ <source>Move %1 of %2 in variation %3</source>
+ <translation>Zug %1 von %2 in Variante %3</translation>
+ </message>
+ <message>
+ <source>Make the computer continue to play the current color</source>
+ <translation>Den Computer die gegenwärtige Farbe weiterspielen lassen</translation>
+ </message>
+ <message>
+ <source>Make the computer play the current color</source>
+ <translation>Den Computer die gegenwärtige Farbe spielen lassen</translation>
+ </message>
+ <message>
+ <source>Classic (&4 Players)</source>
+ <translation>Klassisch (&4 Spieler)</translation>
+ </message>
+ <message>
+ <source>J&unior</source>
+ <translation>J&unior</translation>
+ </message>
+ <message>
+ <source>Classic (&2 Players)</source>
+ <translation>Klassisch (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Nexos (&2 Players)</source>
+ <translation>Nexos (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Nexos (&4 Players)</source>
+ <translation>Nexos (&4 Spieler)</translation>
+ </message>
+ <message>
+ <source>Trigon (&2 Players)</source>
+ <translation>Trigon (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Trigon (&3 Players)</source>
+ <translation>Trigon (&3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Trigon (&4 Players)</source>
+ <translation>Trigon (&4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Classic</source>
+ <translation>&Klassisch</translation>
+ </message>
+ <message>
+ <source>&Trigon</source>
+ <translation>&Trigon</translation>
+ </message>
+ <message>
+ <source>&Nexos</source>
+ <translation>&Nexos</translation>
+ </message>
+ <message>
+ <source>Blue wins with 1 point.</source>
+ <translation>Blau gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Blue wins with %1 points.</source>
+ <translation>Blau gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Green wins with 1 point.</source>
+ <translation>Grün gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Green wins with %1 points.</source>
+ <translation>Grün gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with 1 point.</source>
+ <translation>Blau/Rot gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with %1 points.</source>
+ <translation>Blau/Rot gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with 1 point.</source>
+ <translation>Gelb/Grün gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with %1 points.</source>
+ <translation>Gelb/Grün gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Callisto (&2 Players)</source>
+ <translation>Callisto (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Callisto (&3 Players)</source>
+ <translation>Callisto (&3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Callisto (&4 Players)</source>
+ <translation>Callisto (&4 Spieler)</translation>
+ </message>
+ <message>
+ <source>C&allisto</source>
+ <translation>&Callisto</translation>
+ </message>
+ <message>
+ <source>Green wins (tie resolved).</source>
+ <translation>Grün gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins (tie resolved).</source>
+ <translation>Gelb/Grün gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>Red wins (tie resolved).</source>
+ <translation>Rot gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>Yellow wins (tie resolved).</source>
+ <translation>Gelb gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>&Level (Classic, 4 Players)</source>
+ <translation>Spielst&ufe (Klassisch, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Classic, 2 Players)</source>
+ <translation>Spielst&ufe (Klassisch, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Classic, 3 Players)</source>
+ <translation>Spielst&ufe (Klassisch, 3 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Duo)</source>
+ <translation>Spielst&ufe (Duo)</translation>
+ </message>
+ <message>
+ <source>&Level (Trigon, 4 Players)</source>
+ <translation>Spielst&ufe (Trigon, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Trigon, 2 Players)</source>
+ <translation>Spielst&ufe (Trigon, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Trigon, 3 Players)</source>
+ <translation>Spielst&ufe (Trigon, 3 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Junior)</source>
+ <translation>Spielst&ufe (Junior)</translation>
+ </message>
+ <message>
+ <source>&Level (Nexos, 4 Players)</source>
+ <translation>Spielst&ufe (Nexos, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Nexos, 2 Players)</source>
+ <translation>Spielst&ufe (Nexos, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Callisto, 4 Players)</source>
+ <translation>Spielst&ufe (Callisto, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Callisto, 2 Players)</source>
+ <translation>Spielst&ufe (Callisto, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Callisto, 3 Players)</source>
+ <translation>Spielst&ufe (Callisto, 3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Computer is thinking... (up to %1 seconds remaining)</source>
+ <translation>Computer denkt ... (maximal %1 Sekunden verbleibend)</translation>
+ </message>
+ <message>
+ <source>Computer is thinking... (up to %1 minutes remaining)</source>
+ <translation>Computer denkt ... (maximal %1 Minuten verbleibend)</translation>
+ </message>
+ <message>
+ <source>Computer is thinking...</source>
+ <translation>Computer denkt ...</translation>
+ </message>
+</context>
+<context>
+ <name>RatedGamesList</name>
+ <message>
+ <source>Game</source>
+ <translation>Spiel</translation>
+ </message>
+ <message>
+ <source>Your Color</source>
+ <translation>Ihre Farbe</translation>
+ </message>
+ <message>
+ <source>Level</source>
+ <translation>Stufe</translation>
+ </message>
+ <message>
+ <source>Result</source>
+ <translation>Ergebnis</translation>
+ </message>
+ <message>
+ <source>Date</source>
+ <translation>Datum</translation>
+ </message>
+ <message>
+ <source>Win</source>
+ <translation>Gewinn</translation>
+ </message>
+ <message>
+ <source>Tie</source>
+ <translation>Unentschieden</translation>
+ </message>
+ <message>
+ <source>Loss</source>
+ <translation>Verlust</translation>
+ </message>
+</context>
+<context>
+ <name>RatingDialog</name>
+ <message>
+ <source>Rating</source>
+ <translation>Wertung</translation>
+ </message>
+ <message>
+ <source>Your rating:</source>
+ <translation>Ihre Wertung:</translation>
+ </message>
+ <message>
+ <source>Game variant:</source>
+ <translation>Spielvariante:</translation>
+ </message>
+ <message>
+ <source>Number rated games:</source>
+ <translation>Anzahl gewertete Spiele:</translation>
+ </message>
+ <message>
+ <source>Best previous rating:</source>
+ <translation>Beste frühere Wertung:</translation>
+ </message>
+ <message>
+ <source>Recent games:</source>
+ <translation>Zuletzt gespielte Spiele:</translation>
+ </message>
+ <message>
+ <source>&Clear</source>
+ <translation>&Löschen</translation>
+ </message>
+ <message>
+ <source>Clear rating and delete rating history?</source>
+ <translation>Wertung und Wertungsentwicklung löschen?</translation>
+ </message>
+ <message>
+ <source>Clear rating</source>
+ <translation>Wertung löschen</translation>
+ </message>
+ <message>
+ <source>Classic (4 players)</source>
+ <translation>Klassisch (4 Spieler)</translation>
+ </message>
+ <message>
+ <source>Classic (2 players)</source>
+ <translation>Klassisch (2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Classic (3 players)</source>
+ <translation>Klassisch (3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation>Duo</translation>
+ </message>
+ <message>
+ <source>Trigon (4 players)</source>
+ <translation>Trigon (4 Spieler)</translation>
+ </message>
+ <message>
+ <source>Trigon (2 players)</source>
+ <translation>Trigon (2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Trigon (3 players)</source>
+ <translation>Trigon (3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation>Junior</translation>
+ </message>
+ <message>
+ <source>Recent development:</source>
+ <translation>Aktuelle Entwicklung:</translation>
+ </message>
+ <message>
+ <source>Nexos (4 players)</source>
+ <translation>Nexos (4 Spieler)</translation>
+ </message>
+ <message>
+ <source>Nexos (2 players)</source>
+ <translation>Nexos (2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Callisto (4 players)</source>
+ <translation>Callisto (4 Spieler)</translation>
+ </message>
+ <message>
+ <source>Callisto (2 players)</source>
+ <translation>Callisto (2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Callisto (3 players)</source>
+ <translation>Callisto (3 Spieler)</translation>
+ </message>
+</context>
+<context>
+ <name>main</name>
+ <message>
+ <source>Not enough memory.</source>
+ <translation>Nicht genügend Speicher.</translation>
+ </message>
+ <message>
+ <source>Pentobi</source>
+ <translation>Pentobi</translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+add_executable(pentobi-gtp
+ Engine.h
+ Engine.cpp
+ Main.cpp
+)
+
+target_link_libraries(pentobi-gtp
+ pentobi_mcts
+ pentobi_base
+ boardgame_base
+ boardgame_sgf
+ boardgame_util
+ boardgame_sys
+ boardgame_gtp
+ )
+
+if(CMAKE_THREAD_LIBS_INIT)
+ target_link_libraries(pentobi-gtp ${CMAKE_THREAD_LIBS_INIT})
+endif()
+
+if(MINGW AND (CMAKE_SIZEOF_VOID_P EQUAL "4"))
+ set_target_properties(pentobi-gtp PROPERTIES LINK_FLAGS -Wl,--large-address-aware)
+endif()
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/Engine.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Engine.h"
+
+#include <fstream>
+#include "libpentobi_mcts/Util.h"
+
+namespace pentobi_gtp {
+
+using libboardgame_gtp::Failure;
+using libpentobi_mcts::Float;
+
+//-----------------------------------------------------------------------------
+
+Engine::Engine(Variant variant, unsigned level, bool use_book,
+ const string& books_dir, unsigned nu_threads)
+ : libpentobi_base::Engine(variant)
+{
+ create_player(variant, level, books_dir, nu_threads);
+ get_mcts_player().set_use_book(use_book);
+ add("get_value", &Engine::cmd_get_value);
+ add("name", &Engine::cmd_name);
+ add("param", &Engine::cmd_param);
+ add("move_values", &Engine::cmd_move_values);
+ add("save_tree", &Engine::cmd_save_tree);
+ add("version", &Engine::cmd_version);
+}
+
+Engine::~Engine() = default;
+
+void Engine::cmd_get_value(Response& response)
+{
+ response << get_search().get_tree().get_root().get_value();
+}
+
+void Engine::cmd_move_values(Response& response)
+{
+ auto& search = get_search();
+ auto& tree = search.get_tree();
+ auto& bd = get_board();
+ vector<const Search::Node*> children;
+ for (auto& i : tree.get_root_children())
+ children.push_back(&i);
+ sort(children.begin(), children.end(), libpentobi_mcts::util::compare_node);
+ response << fixed;
+ for (auto node : children)
+ response << setprecision(0) << node->get_visit_count() << ' '
+ << setprecision(1) << node->get_value_count() << ' '
+ << setprecision(3) << node->get_value() << ' '
+ << bd.to_string(node->get_move(), true) << '\n';
+}
+
+void Engine::cmd_name(Response& response)
+{
+ response.set("Pentobi");
+}
+
+void Engine::cmd_save_tree(const Arguments& args)
+{
+ auto& search = get_search();
+ if (! search.get_last_history().is_valid())
+ throw Failure("no search tree");
+ ofstream out(args.get());
+ libpentobi_mcts::util::dump_tree(out, search);
+}
+
+void Engine::cmd_param(const Arguments& args, Response& response)
+{
+ auto& p = get_mcts_player();
+ auto& s = get_search();
+ if (args.get_size() == 0)
+ response
+ << "avoid_symmetric_draw " << s.get_avoid_symmetric_draw() << '\n'
+ << "auto_param " << s.get_auto_param() << '\n'
+ << "exploration_constant " << s.get_exploration_constant() << '\n'
+ << "expand_threshold " << s.get_expand_threshold() << '\n'
+ << "expand_threshold_inc " << s.get_expand_threshold_inc() << '\n'
+ << "fixed_simulations " << p.get_fixed_simulations() << '\n'
+ << "rave_child_max " << s.get_rave_child_max() << '\n'
+ << "rave_parent_max " << s.get_rave_parent_max() << '\n'
+ << "rave_weight " << s.get_rave_weight() << '\n'
+ << "reuse_subtree " << s.get_reuse_subtree() << '\n'
+ << "use_book " << p.get_use_book() << '\n';
+ else
+ {
+ args.check_size(2);
+ string name = args.get(0);
+ if (name == "avoid_symmetric_draw")
+ s.set_avoid_symmetric_draw(args.parse<bool>(1));
+ else if (name == "auto_param")
+ s.set_auto_param(args.parse<bool>(1));
+ else if (name == "exploration_constant")
+ s.set_exploration_constant(args.parse<Float>(1));
+ else if (name == "expand_threshold")
+ s.set_expand_threshold(args.parse<Float>(1));
+ else if (name == "expand_threshold_inc")
+ s.set_expand_threshold_inc(args.parse<Float>(1));
+ else if (name == "fixed_simulations")
+ p.set_fixed_simulations(args.parse<Float>(1));
+ else if (name == "rave_child_max")
+ s.set_rave_child_max(args.parse<Float>(1));
+ else if (name == "rave_parent_max")
+ s.set_rave_parent_max(args.parse<Float>(1));
+ else if (name == "rave_weight")
+ s.set_rave_weight(args.parse<Float>(1));
+ else if (name == "reuse_subtree")
+ s.set_reuse_subtree(args.parse<bool>(1));
+ else if (name == "use_book")
+ p.set_use_book(args.parse<bool>(1));
+ else
+ {
+ ostringstream msg;
+ msg << "unknown parameter '" << name << "'";
+ throw Failure(msg.str());
+ }
+ }
+}
+
+void Engine::cmd_version(Response& response)
+{
+ string version;
+#ifdef VERSION
+ version = VERSION;
+#endif
+ if (version.empty())
+ version = "UNKNOWN";
+ // By convention, the version string of unreleased versions contains the
+ // string UNKNOWN (appended to the last released version). In this case, or
+ // if VERSION was undefined, we append the build date.
+ if (version.find("UNKNOWN") != string::npos)
+ {
+ version.append(" (");
+ version.append(__DATE__);
+ version.append(")");
+ }
+#if LIBBOARDGAME_DEBUG
+ version.append(" (dbg)");
+#endif
+ response.set(version);
+}
+
+void Engine::create_player(Variant variant, unsigned level,
+ const string& books_dir, unsigned nu_threads)
+{
+ auto max_level = level;
+ m_player.reset(new Player(variant, max_level, books_dir, nu_threads));
+ get_mcts_player().set_level(level);
+ set_player(*m_player);
+}
+
+Player& Engine::get_mcts_player()
+{
+ try
+ {
+ return dynamic_cast<Player&>(*m_player);
+ }
+ catch (const bad_cast&)
+ {
+ throw Failure("current player is not mcts player");
+ }
+}
+
+Search& Engine::get_search()
+{
+ return get_mcts_player().get_search();
+}
+
+void Engine::use_cpu_time(bool enable)
+{
+ get_mcts_player().use_cpu_time(enable);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace pentobi_gtp
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/Engine.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_GTP_ENGINE_H
+#define PENTOBI_GTP_ENGINE_H
+
+#include "libpentobi_base/Engine.h"
+#include "libpentobi_mcts/Player.h"
+
+namespace pentobi_gtp {
+
+using namespace std;
+using libboardgame_gtp::Arguments;
+using libboardgame_gtp::Response;
+using libpentobi_base::PlayerBase;
+using libpentobi_base::Variant;
+using libpentobi_mcts::Player;
+using libpentobi_mcts::Search;
+
+//-----------------------------------------------------------------------------
+
+class Engine
+ : public libpentobi_base::Engine
+{
+public:
+ Engine(Variant variant, unsigned level = 5,
+ bool use_book = true, const string& books_dir = "",
+ unsigned nu_threads = 0);
+
+ ~Engine();
+
+ void cmd_param(const Arguments&, Response&);
+ void cmd_get_value(Response&);
+ void cmd_move_values(Response&);
+ void cmd_name(Response&);
+ void cmd_save_tree(const Arguments&);
+ void cmd_version(Response&);
+
+ Player& get_mcts_player();
+
+ /** @see Player::use_cpu_time() */
+ void use_cpu_time(bool enable);
+
+private:
+ unique_ptr<PlayerBase> m_player;
+
+ void create_player(Variant variant, unsigned level,
+ const string& books_dir, unsigned nu_threads);
+
+ Search& get_search();
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace pentobi_gtp
+
+#endif // PENTOBI_GTP_ENGINE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/Main.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <fstream>
+#include "Engine.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/Options.h"
+#include "libboardgame_util/RandomGenerator.h"
+
+using namespace std;
+using libboardgame_gtp::Failure;
+using libboardgame_util::Options;
+using libboardgame_util::RandomGenerator;
+using libpentobi_base::parse_variant_id;
+using libpentobi_base::Board;
+using libpentobi_base::Variant;
+using libpentobi_mcts::Player;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+string get_application_dir_path(int argc, char** argv)
+{
+ if (argc == 0 || ! argv || ! argv[0])
+ return "";
+ string application_path(argv[0]);
+#ifdef _WIN32
+ auto pos = application_path.find_last_of("/\\");
+#else
+ auto pos = application_path.find_last_of("/");
+#endif
+ if (pos == string::npos)
+ return "";
+ return application_path.substr(0, pos);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+ string application_dir_path = get_application_dir_path(argc, argv);
+ try
+ {
+ vector<string> specs = {
+ "book:",
+ "config|c:",
+ "color",
+ "cputime",
+ "game|g:",
+ "help|h",
+ "level|l:",
+ "nobook",
+ "noresign",
+ "quiet|q",
+ "seed|r:",
+ "showboard",
+ "threads:",
+ "version|v"
+ };
+ Options opt(argc, argv, specs);
+ if (opt.contains("help"))
+ {
+ cout <<
+ "Usage: pentobi_gtp [options] [input files]\n"
+ "--book load an external book file\n"
+ "--config,-c set GTP config file\n"
+ "--color colorize text output of boards\n"
+ "--cputime use CPU time\n"
+ "--game,-g game variant (classic, classic_2, classic_3,\n"
+ " duo, trigon, trigon_2, trigon_3, junior)\n"
+ "--help,-h print help message and exit\n"
+ "--level,-l set playing strength level\n"
+ "--seed,-r set random seed\n"
+ "--showboard automatically write board to stderr after\n"
+ " changes\n"
+ "--nobook disable opening book\n"
+ "--noresign disable resign\n"
+ "--quiet,-q do not print logging messages\n"
+ "--threads number of threads in the search\n"
+ "--version,-v print version and exit\n";
+ return 0;
+ }
+ if (opt.contains("version"))
+ {
+#ifdef VERSION
+ cout << "Pentobi " << VERSION << '\n';
+#else
+ cout << "Pentobi UNKNONW";
+#endif
+ return 0;
+ }
+ unsigned threads = 1;
+ if (opt.contains("threads"))
+ {
+ threads = opt.get<unsigned>("threads");
+ if (threads == 0)
+ throw runtime_error("Number of threads must be greater zero.");
+ }
+ Board::color_output = opt.contains("color");
+ if (opt.contains("quiet"))
+ libboardgame_util::disable_logging();
+ if (opt.contains("seed"))
+ RandomGenerator::set_global_seed(
+ opt.get<RandomGenerator::ResultType>("seed"));
+ string variant_string = opt.get("game", "classic");
+ Variant variant;
+ if (! parse_variant_id(variant_string, variant))
+ throw runtime_error("invalid game variant " + variant_string);
+ auto level = opt.get<unsigned>("level", 4);
+ if (level < 1 || level > Player::max_supported_level)
+ throw runtime_error("invalid level");
+ auto use_book = (! opt.contains("nobook"));
+ string books_dir = application_dir_path;
+ pentobi_gtp::Engine engine(variant, level, use_book, books_dir,
+ threads);
+ engine.set_resign(! opt.contains("noresign"));
+ if (opt.contains("showboard"))
+ engine.set_show_board(true);
+ if (opt.contains("cputime"))
+ engine.use_cpu_time(true);
+ string book_file = opt.get("book", "");
+ if (! book_file.empty())
+ {
+ ifstream in(book_file);
+ engine.get_mcts_player().load_book(in);
+ }
+ string config_file = opt.get("config", "");
+ if (! config_file.empty())
+ {
+ ifstream in(config_file);
+ if (! in)
+ throw runtime_error("Error opening " + config_file);
+ engine.exec(in, true, libboardgame_util::get_log_stream());
+ }
+ auto& args = opt.get_args();
+ if (! args.empty())
+ for (auto& file : args)
+ {
+ ifstream in(file);
+ if (! in)
+ throw runtime_error("Error opening " + file);
+ engine.exec_main_loop(in, cout);
+ }
+ else
+ engine.exec_main_loop(cin, cout);
+ return 0;
+ }
+ catch (const Failure& e)
+ {
+ LIBBOARDGAME_LOG("Error: command in config file failed: ", e.what());
+ return 1;
+ }
+ catch (const exception& e)
+ {
+ LIBBOARDGAME_LOG("Error: ", e.what());
+ return 1;
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+find_package(ECM REQUIRED NO_MODULE)
+set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
+
+include(KDEInstallDirs)
+include(KDECompilerSettings)
+include(KDECMakeSettings)
+
+find_package(KF5 REQUIRED COMPONENTS KIO)
+
+include_directories(${CMAKE_SOURCE_DIR}/src)
+
+add_library(pentobi-thumbnail MODULE
+ PentobiThumbCreator.h
+ PentobiThumbCreator.cpp
+)
+
+target_link_libraries(pentobi-thumbnail
+ pentobi_kde_thumbnailer
+ KF5::KIOWidgets
+)
+
+install(TARGETS pentobi-thumbnail DESTINATION ${PLUGIN_INSTALL_DIR})
+install(FILES pentobi-thumbnail.desktop DESTINATION ${SERVICES_INSTALL_DIR})
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_kde_thumbnailer/PentobiThumbCreator.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PentobiThumbCreator.h"
+
+#include <QImage>
+#include "libpentobi_thumbnail/CreateThumbnail.h"
+
+//-----------------------------------------------------------------------------
+
+extern "C" {
+
+Q_DECL_EXPORT ThumbCreator* new_creator() { return new PentobiThumbCreator; }
+
+}
+
+//-----------------------------------------------------------------------------
+
+PentobiThumbCreator::~PentobiThumbCreator() = default;
+
+bool PentobiThumbCreator::create(const QString& path, int width, int height,
+ QImage& image)
+{
+ image = QImage(width, height, QImage::Format_ARGB32);
+ if (image.isNull())
+ return false;
+ image.fill(Qt::transparent);
+ return createThumbnail(path, width, height, image);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_kde_thumbnailer/PentobiThumbCreator.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H
+#define PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H
+
+#include <QObject>
+#include <kio/thumbcreator.h>
+
+//-----------------------------------------------------------------------------
+
+class PentobiThumbCreator
+ : public QObject,
+ public ThumbCreator
+{
+ Q_OBJECT
+
+public:
+ virtual ~PentobiThumbCreator();
+
+ bool create(const QString& path, int width, int height,
+ QImage& image) override;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_KDE_THUMBNAILER_PENTOBI_THUMB_CREATOR_H
--- /dev/null
+[Desktop Entry]
+Type=Service
+Name=Blokus games
+Name[de]=Blokus-Partien
+ServiceTypes=ThumbCreator
+MimeType=application/x-blokus-sgf;
+X-KDE-Library=pentobi-thumbnail
--- /dev/null
+Pentobi.pro.user
--- /dev/null
+set(CMAKE_AUTOMOC TRUE)
+
+include_directories(${CMAKE_SOURCE_DIR}/src)
+
+set(pentobi_qml_SRCS
+ GameModel.h
+ GameModel.cpp
+ Main.cpp
+ PieceModel.h
+ PieceModel.cpp
+ PlayerModel.h
+ PlayerModel.cpp
+)
+
+qt5_add_resources(pentobi_qml_RC_SRCS
+ resources.qrc
+ qml/themes/theme_light.qrc
+ qml/themes/theme_shared.qrc
+ ../books/pentobi_books.qrc
+ ../pentobi/icons.qrc
+)
+
+add_executable(pentobi_qml WIN32
+ ${pentobi_qml_SRCS}
+ ${pentobi_qml_RC_SRCS}
+)
+
+target_link_libraries(pentobi_qml
+ pentobi_mcts
+ pentobi_base
+ boardgame_base
+ boardgame_sgf
+ boardgame_util
+ boardgame_sys
+)
+
+qt5_use_modules(pentobi_qml Concurrent Gui Qml Svg)
+
+if(CMAKE_THREAD_LIBS_INIT)
+ target_link_libraries(pentobi_qml ${CMAKE_THREAD_LIBS_INIT})
+endif()
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_qml/GameModel.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GameModel.h"
+
+#include <cerrno>
+#include <cstring>
+#include <fstream>
+#include <QDebug>
+#include <QSettings>
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libpentobi_base/PentobiTreeWriter.h"
+#include "libpentobi_base/TreeUtil.h"
+
+using namespace std;
+using libboardgame_sgf::InvalidTree;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::util::back_to_main_variation;
+using libboardgame_sgf::util::get_last_node;
+using libboardgame_sgf::util::is_main_variation;
+using libpentobi_base::get_piece_set;
+using libpentobi_base::to_string_id;
+using libpentobi_base::BoardType;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMap;
+using libpentobi_base::ColorMove;
+using libpentobi_base::CoordPoint;
+using libpentobi_base::MovePoints;
+using libpentobi_base::PentobiTree;
+using libpentobi_base::PentobiTreeWriter;
+using libpentobi_base::Piece;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::PiecePoints;
+using libpentobi_base::PieceSet;
+using libpentobi_base::Point;
+using libpentobi_base::tree_util::get_position_info;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+// Game coordinates are fractional because they refer to the center of a piece.
+// This function is used to compare game coordinates of moves with the same
+// piece, so we could even compare the rounded values (?), but comparing
+// against epsilon is also safe.
+bool compareGameCoord(const QPointF& p1, const QPointF& p2)
+{
+ return (p1 - p2).manhattanLength() < 0.01f;
+}
+
+bool compareTransform(const PieceInfo& pieceInfo, const Transform* t1,
+ const Transform* t2)
+{
+ return pieceInfo.get_equivalent_transform(t1) ==
+ pieceInfo.get_equivalent_transform(t2);
+}
+
+QPointF getGameCoord(const Board& bd, Move mv)
+{
+ auto& geo = bd.get_geometry();
+ PiecePoints movePoints;
+ for (Point p : bd.get_move_points(mv))
+ movePoints.push_back(CoordPoint(geo.get_x(p), geo.get_y(p)));
+ return PieceModel::findCenter(bd, movePoints, false);
+}
+
+} //namespace
+
+//-----------------------------------------------------------------------------
+
+GameModel::GameModel(QObject* parent)
+ : QObject(parent),
+ m_game(getInitialGameVariant()),
+ m_gameVariant(to_string_id(m_game.get_variant())),
+ m_nuColors(getBoard().get_nu_colors())
+{
+ createPieceModels();
+ updateProperties();
+}
+
+void GameModel::autoSave()
+{
+ // Don't autosave if game was not modified because it could have been
+ // loaded from a file, but autosave if not modified and empty to ensure
+ // that we start with the same game variant next time.
+ if (! m_game.is_modified()
+ && ! libboardgame_sgf::util::is_empty(m_game.get_tree()))
+ return;
+ ostringstream s;
+ PentobiTreeWriter writer(s, m_game.get_tree());
+ writer.set_indent(-1);
+ writer.write();
+ QSettings settings;
+ settings.setValue("variant", to_string_id(m_game.get_variant()));
+ settings.setValue("autosave", s.str().c_str());
+}
+
+void GameModel::backToMainVar()
+{
+ gotoNode(back_to_main_variation(m_game.get_current()));
+}
+
+void GameModel::createPieceModels()
+{
+ createPieceModels(Color(0), m_pieceModels0);
+ createPieceModels(Color(1), m_pieceModels1);
+ if (m_nuColors > 2)
+ createPieceModels(Color(2), m_pieceModels2);
+ else
+ m_pieceModels2.clear();
+ if (m_nuColors > 3)
+ createPieceModels(Color(3), m_pieceModels3);
+ else
+ m_pieceModels3.clear();
+}
+
+void GameModel::createPieceModels(Color c, QList<PieceModel*>& pieceModels)
+{
+ auto& bd = getBoard();
+ auto nuPieces = bd.get_nu_uniq_pieces();
+ pieceModels.clear();
+ pieceModels.reserve(nuPieces);
+ for (Piece::IntType i = 0; i < nuPieces; ++i)
+ {
+ Piece piece(i);
+ for (unsigned j = 0; j < bd.get_piece_info(piece).get_nu_instances();
+ ++j)
+ pieceModels.append(new PieceModel(this, bd, piece, c));
+ }
+}
+
+void GameModel::deleteAllVar()
+{
+ if (! is_main_variation(m_game.get_current()))
+ emit positionAboutToChange();
+ m_game.delete_all_variations();
+ updateProperties();
+}
+
+bool GameModel::findMove(const PieceModel& pieceModel, const QString& state,
+ QPointF coord, Move& mv) const
+{
+ auto piece = pieceModel.getPiece();
+ auto& bd = getBoard();
+ if (piece.to_int() >= bd.get_nu_uniq_pieces())
+ {
+ qWarning("GameModel::findMove: pieceModel invalid in game variant");
+ return false;
+ }
+ auto transform = pieceModel.getTransform(state);
+ if (! transform)
+ {
+ qWarning("GameModel::findMove: transform not found");
+ return false;
+ }
+ auto& info = bd.get_piece_info(piece);
+ PiecePoints piecePoints = info.get_points();
+ transform->transform(piecePoints.begin(), piecePoints.end());
+ auto boardType = bd.get_board_type();
+ auto newPointType = transform->get_new_point_type();
+ bool pointTypeChanged =
+ ((boardType == BoardType::trigon && newPointType == 1)
+ || (boardType == BoardType::trigon_3 && newPointType == 0));
+ QPointF center(PieceModel::findCenter(bd, piecePoints, false));
+ // Round y of center to a multiple of 0.5, works better in Trigon
+ center.setY(round(2 * center.y()) / 2);
+ int offX = static_cast<int>(round(coord.x() - center.x()));
+ int offY = static_cast<int>(round(coord.y() - center.y()));
+ auto& geo = bd.get_geometry();
+ MovePoints points;
+ for (auto& p : piecePoints)
+ {
+ int x = p.x + offX;
+ int y = p.y + offY;
+ if (! geo.is_onboard(CoordPoint(x, y)))
+ return false;
+ auto pointType = geo.get_point_type(p);
+ auto boardPointType = geo.get_point_type(x, y);
+ if (! pointTypeChanged && pointType != boardPointType)
+ return false;
+ if (pointTypeChanged && pointType == boardPointType)
+ return false;
+ points.push_back(geo.get_point(x, y));
+ }
+ return bd.find_move(points, piece, mv);
+}
+
+QString GameModel::getResultMessage()
+{
+ auto& bd = getBoard();
+ auto nuPlayers = bd.get_nu_players();
+ bool breakTies = (bd.get_piece_set() == PieceSet::callisto);
+ if (m_nuColors == 2)
+ {
+ auto score = m_points0 - m_points1;
+ if (score == 1)
+ return tr("Blue wins with 1 point.");
+ if (score > 0)
+ return tr("Blue wins with %1 points.").arg(score);
+ if (score == -1)
+ return tr("Green wins with 1 point.");
+ if (score < 0)
+ return tr("Green wins with %1 points.").arg(-score);
+ if (breakTies)
+ return tr("Green wins (tie resolved).");
+ return tr("Game ends in a tie.");
+ }
+ if (m_nuColors == 4 && nuPlayers == 2)
+ {
+ auto score = m_points0 + m_points2 - m_points1 - m_points3;
+ if (score == 1)
+ return tr("Blue/Red wins with 1 point.");
+ if (score > 0)
+ return tr("Blue/Red wins with %1 points.").arg(score);
+ if (score == -1)
+ return tr("Yellow/Green wins with 1 point.");
+ if (score < 0)
+ return tr("Yellow/Green wins with %1 points.").arg(-score);
+ if (breakTies)
+ return tr("Yellow/Green wins (tie resolved).");
+ return tr("Game ends in a tie.");
+ }
+ if (nuPlayers == 3)
+ {
+ auto maxPoints = max(max(m_points0, m_points1), m_points2);
+ unsigned nuWinners = 0;
+ if (m_points0 == maxPoints)
+ ++nuWinners;
+ if (m_points1 == maxPoints)
+ ++nuWinners;
+ if (m_points2 == maxPoints)
+ ++nuWinners;
+ if (m_points0 == maxPoints && nuWinners == 1)
+ return tr("Blue wins.");
+ if (m_points1 == maxPoints && nuWinners == 1)
+ return tr("Yellow wins.");
+ if (m_points2 == maxPoints && nuWinners == 1)
+ return tr("Red wins.");
+ if (m_points2 == maxPoints && breakTies)
+ return tr("Red wins (tie resolved).");
+ if (m_points1 == maxPoints && breakTies)
+ return tr("Yellow wins (tie resolved).");
+ if (m_points0 == maxPoints && m_points1 == maxPoints && nuWinners == 2)
+ return tr("Game ends in a tie between Blue and Yellow.");
+ if (m_points0 == maxPoints && m_points2 == maxPoints && nuWinners == 2)
+ return tr("Game ends in a tie between Blue and Red.");
+ if (nuWinners == 2)
+ return tr("Game ends in a tie between Yellow and Red.");
+ return tr("Game ends in a tie between all players.");
+ }
+ auto maxPoints = max(max(m_points0, m_points1), max(m_points2, m_points3));
+ unsigned nuWinners = 0;
+ if (m_points0 == maxPoints)
+ ++nuWinners;
+ if (m_points1 == maxPoints)
+ ++nuWinners;
+ if (m_points2 == maxPoints)
+ ++nuWinners;
+ if (m_points3 == maxPoints)
+ ++nuWinners;
+ if (m_points0 == maxPoints && nuWinners == 1)
+ return tr("Blue wins.");
+ if (m_points1 == maxPoints && nuWinners == 1)
+ return tr("Yellow wins.");
+ if (m_points2 == maxPoints && nuWinners == 1)
+ return tr("Red wins.");
+ if (m_points3 == maxPoints && nuWinners == 1)
+ return tr("Green wins.");
+ if (m_points3 == maxPoints && breakTies)
+ return tr("Green wins (tie resolved).");
+ if (m_points2 == maxPoints && breakTies)
+ return tr("Red wins (tie resolved).");
+ if (m_points1 == maxPoints && breakTies)
+ return tr("Yellow wins (tie resolved).");
+ if (m_points0 == maxPoints && m_points1 == maxPoints
+ && m_points2 == maxPoints && nuWinners == 3)
+ return tr("Game ends in a tie between Blue, Yellow and Red.");
+ if (m_points0 == maxPoints && m_points1 == maxPoints
+ && m_points3 == maxPoints && nuWinners == 3)
+ return tr("Game ends in a tie between Blue, Yellow and Green.");
+ if (m_points0 == maxPoints && m_points2 == maxPoints
+ && m_points3 == maxPoints && nuWinners == 3)
+ return tr("Game ends in a tie between Blue, Red and Green.");
+ if (nuWinners == 3)
+ return tr("Game ends in a tie between Yellow, Red and Green.");
+ if (m_points0 == maxPoints && m_points1 == maxPoints && nuWinners == 2)
+ return tr("Game ends in a tie between Blue and Yellow.");
+ if (m_points0 == maxPoints && m_points2 == maxPoints && nuWinners == 2)
+ return tr("Game ends in a tie between Blue and Red.");
+ if (nuWinners == 2)
+ return tr("Game ends in a tie between Yellow and Red.");
+ return tr("Game ends in a tie between all players.");
+}
+
+Variant GameModel::getInitialGameVariant()
+{
+ QSettings settings;
+ auto variantString = settings.value("variant", "").toString();
+ Variant variant;
+ if (! parse_variant_id(variantString.toLocal8Bit().constData(), variant))
+ variant = Variant::duo;
+ return variant;
+}
+
+QList<PieceModel*>& GameModel::getPieceModels(Color c)
+{
+ if (c == Color(0))
+ return m_pieceModels0;
+ else if (c == Color(1))
+ return m_pieceModels1;
+ else if (c == Color(2))
+ return m_pieceModels2;
+ else
+ return m_pieceModels3;
+}
+
+void GameModel::goBackward()
+{
+ gotoNode(m_game.get_current().get_parent_or_null());
+}
+
+void GameModel::goBeginning()
+{
+ gotoNode(m_game.get_root());
+}
+
+void GameModel::goEnd()
+{
+ gotoNode(get_last_node(m_game.get_current()));
+}
+
+void GameModel::goForward()
+{
+ gotoNode(m_game.get_current().get_first_child_or_null());
+}
+
+void GameModel::goNextVar()
+{
+ gotoNode(m_game.get_current().get_sibling());
+}
+
+void GameModel::goPrevVar()
+{
+ gotoNode(m_game.get_current().get_previous_sibling());
+}
+
+void GameModel::gotoNode(const SgfNode& node)
+{
+ if (&node == &m_game.get_current())
+ return;
+ emit positionAboutToChange();
+ try
+ {
+ m_game.goto_node(node);
+ }
+ catch (const InvalidTree&)
+ {
+ }
+ updateProperties();
+}
+
+void GameModel::gotoNode(const SgfNode* node)
+{
+ if (node)
+ gotoNode(*node);
+}
+
+void GameModel::initGameVariant(const QString& gameVariant)
+{
+ Variant variant;
+ if (! parse_variant_id(gameVariant.toLocal8Bit().constData(), variant))
+ {
+ qWarning("GameModel: invalid game variant");
+ return;
+ }
+ if (m_game.get_variant() != variant)
+ m_game.init(variant);
+ auto& bd = getBoard();
+ set(m_nuColors, static_cast<int>(bd.get_nu_colors()),
+ &GameModel::nuColorsChanged);
+ m_lastMovePieceModel = nullptr;
+ createPieceModels();
+ m_gameVariant = gameVariant;
+ emit gameVariantChanged(gameVariant);
+ updateProperties();
+}
+
+bool GameModel::isLegalPos(PieceModel* pieceModel, const QString& state,
+ QPointF coord) const
+{
+ Move mv;
+ if (! findMove(*pieceModel, state, coord, mv))
+ return false;
+ Color c(static_cast<Color::IntType>(pieceModel->color()));
+ bool result = getBoard().is_legal(c, mv);
+ return result;
+}
+
+bool GameModel::loadAutoSave()
+{
+ QSettings settings;
+ auto s = settings.value("autosave", "").toByteArray();
+ istringstream in(s.constData());
+ if (! open(in))
+ return false;
+ m_game.set_modified();
+ return true;
+}
+
+void GameModel::makeMainVar()
+{
+ m_game.make_main_variation();
+ updateProperties();
+}
+
+void GameModel::moveDownVar()
+{
+ m_game.move_down_variation();
+ updateProperties();
+}
+
+void GameModel::moveUpVar()
+{
+ m_game.move_up_variation();
+ updateProperties();
+}
+
+void GameModel::nextColor()
+{
+ emit positionAboutToChange();
+ auto& bd = getBoard();
+ m_game.set_to_play(bd.get_next(bd.get_to_play()));
+ updateProperties();
+}
+
+void GameModel::newGame()
+{
+ emit positionAboutToChange();
+ m_game.init();
+ for (auto pieceModel : m_pieceModels0)
+ pieceModel->setDefaultState();
+ for (auto pieceModel : m_pieceModels1)
+ pieceModel->setDefaultState();
+ for (auto pieceModel : m_pieceModels2)
+ pieceModel->setDefaultState();
+ for (auto pieceModel : m_pieceModels3)
+ pieceModel->setDefaultState();
+ updateProperties();
+}
+
+bool GameModel::open(istream& in)
+{
+ try
+ {
+ TreeReader reader;
+ reader.read(in);
+ auto root = reader.get_tree_transfer_ownership();
+ emit positionAboutToChange();
+ m_game.init(root);
+ auto variant = to_string_id(m_game.get_variant());
+ if (variant != m_gameVariant)
+ initGameVariant(variant);
+ goEnd();
+ updateProperties();
+ QSettings settings;
+ settings.remove("autosave");
+ }
+ catch (const runtime_error& e)
+ {
+ m_lastInputOutputError = QString::fromLocal8Bit(e.what());
+ return false;
+ }
+ return true;
+}
+
+bool GameModel::open(const QString& file)
+{
+ ifstream in(file.toLocal8Bit().constData());
+ if (! in)
+ {
+ m_lastInputOutputError = QString::fromLocal8Bit(strerror(errno));
+ return false;
+ }
+ return open(in);
+}
+
+QQmlListProperty<PieceModel> GameModel::pieceModels0()
+{
+ return QQmlListProperty<PieceModel>(this, m_pieceModels0);
+}
+
+QQmlListProperty<PieceModel> GameModel::pieceModels1()
+{
+ return QQmlListProperty<PieceModel>(this, m_pieceModels1);
+}
+
+QQmlListProperty<PieceModel> GameModel::pieceModels2()
+{
+ return QQmlListProperty<PieceModel>(this, m_pieceModels2);
+}
+
+QQmlListProperty<PieceModel> GameModel::pieceModels3()
+{
+ return QQmlListProperty<PieceModel>(this, m_pieceModels3);
+}
+
+void GameModel::playMove(int move)
+{
+ Move mv(static_cast<Move::IntType>(move));
+ if (mv.is_null())
+ return;
+ emit positionAboutToChange();
+ m_game.play(m_game.get_to_play(), mv, false);
+ updateProperties();
+}
+
+void GameModel::playPiece(PieceModel* pieceModel, QPointF coord)
+{
+ Color c(static_cast<Color::IntType>(pieceModel->color()));
+ Move mv;
+ if (! findMove(*pieceModel, pieceModel->state(), coord, mv))
+ {
+ qWarning("GameModel::play: illegal move");
+ return;
+ }
+ emit positionAboutToChange();
+ preparePieceGameCoord(pieceModel, mv);
+ pieceModel->setIsPlayed(true);
+ preparePieceTransform(pieceModel, mv);
+ m_game.play(c, mv, false);
+ updateProperties();
+}
+
+PieceModel* GameModel::preparePiece(int color, int move)
+{
+ Move mv(static_cast<Move::IntType>(move));
+ Color c(static_cast<Color::IntType>(color));
+ Piece piece = getBoard().get_move_piece(mv);
+ for (auto pieceModel : getPieceModels(c))
+ if (pieceModel->getPiece() == piece && ! pieceModel->isPlayed())
+ {
+ preparePieceTransform(pieceModel, mv);
+ preparePieceGameCoord(pieceModel, mv);
+ return pieceModel;
+ }
+ return nullptr;
+}
+
+void GameModel::preparePieceGameCoord(PieceModel* pieceModel, Move mv)
+{
+ pieceModel->setGameCoord(getGameCoord(getBoard(), mv));
+}
+
+void GameModel::preparePieceTransform(PieceModel* pieceModel, Move mv)
+{
+ auto& bd = getBoard();
+ auto transform = bd.find_transform(mv);
+ auto& pieceInfo = bd.get_piece_info(bd.get_move_piece(mv));
+ if (! compareTransform(pieceInfo, pieceModel->getTransform(), transform))
+ pieceModel->setTransform(transform);
+}
+
+bool GameModel::save(const QString& file)
+{
+ ofstream out(file.toLocal8Bit().constData());
+ PentobiTreeWriter writer(out, m_game.get_tree());
+ writer.set_indent(1);
+ writer.write();
+ if (! out)
+ {
+ m_lastInputOutputError = QString::fromLocal8Bit(strerror(errno));
+ return false;
+ }
+ m_game.clear_modified();
+ return true;
+}
+
+template<typename T>
+void GameModel::set(T& target, const T& value,
+ void (GameModel::*changedSignal)(T))
+{
+ if (target != value)
+ {
+ target = value;
+ emit (this->*changedSignal)(value);
+ }
+}
+
+void GameModel::truncate()
+{
+ if (! m_game.get_current().has_parent())
+ return;
+ emit positionAboutToChange();
+ m_game.truncate();
+ updateProperties();
+}
+
+void GameModel::truncateChildren()
+{
+ m_game.truncate_children();
+ updateProperties();
+}
+
+void GameModel::undo()
+{
+ if (! m_canUndo)
+ return;
+ emit positionAboutToChange();
+ m_game.undo();
+ updateProperties();
+}
+
+/** Helper function for updateProperties() */
+PieceModel* GameModel::updatePiece(Color c, Move mv,
+ array<bool, Board::max_pieces>& isPlayed)
+{
+ auto& bd = getBoard();
+ Piece piece = bd.get_move_piece(mv);
+ auto& pieceInfo = bd.get_piece_info(piece);
+ auto gameCoord = getGameCoord(bd, mv);
+ auto transform = bd.find_transform(mv);
+ auto& pieceModels = getPieceModels(c);
+ // Prefer piece models already played with the given gameCoord and
+ // transform because class Board doesn't make a distinction between
+ // instances of the same piece (in Junior) and we want to avoid
+ // unwanted piece movement animations to switch instances.
+ for (int i = 0; i < pieceModels.length(); ++i)
+ if (pieceModels[i]->getPiece() == piece
+ && pieceModels[i]->isPlayed()
+ && compareGameCoord(pieceModels[i]->gameCoord(), gameCoord)
+ && compareTransform(pieceInfo, pieceModels[i]->getTransform(),
+ transform))
+ {
+ isPlayed[i] = true;
+ return pieceModels[i];
+ }
+ for (int i = 0; i < pieceModels.length(); ++i)
+ if (pieceModels[i]->getPiece() == piece && ! isPlayed[i])
+ {
+ isPlayed[i] = true;
+ // Order is important: isPlayed will trigger an animation to move
+ // the piece, so it needs to be set after gameCoord.
+ pieceModels[i]->setGameCoord(gameCoord);
+ pieceModels[i]->setIsPlayed(true);
+ pieceModels[i]->setTransform(transform);
+ return pieceModels[i];
+ }
+ LIBBOARDGAME_ASSERT(false);
+ return nullptr;
+}
+
+void GameModel::updateProperties()
+{
+ auto& bd = getBoard();
+ auto& geo = bd.get_geometry();
+ auto& tree = m_game.get_tree();
+ bool isTrigon = (bd.get_piece_set() == PieceSet::trigon);
+ set(m_points0, bd.get_points(Color(0)), &GameModel::points0Changed);
+ set(m_points1, bd.get_points(Color(1)), &GameModel::points1Changed);
+ set(m_bonus0, bd.get_bonus(Color(0)), &GameModel::bonus0Changed);
+ set(m_bonus1, bd.get_bonus(Color(1)), &GameModel::bonus1Changed);
+ set(m_hasMoves0, bd.has_moves(Color(0)), &GameModel::hasMoves0Changed);
+ set(m_hasMoves1, bd.has_moves(Color(1)), &GameModel::hasMoves1Changed);
+ bool isFirstPieceAny = false;
+ if (m_nuColors > 2)
+ {
+ set(m_points2, bd.get_points(Color(2)), &GameModel::points2Changed);
+ set(m_bonus2, bd.get_bonus(Color(2)), &GameModel::bonus2Changed);
+ set(m_hasMoves2, bd.has_moves(Color(2)), &GameModel::hasMoves2Changed);
+ }
+ if (m_nuColors > 3)
+ {
+ set(m_points3, bd.get_points(Color(3)), &GameModel::points3Changed);
+ set(m_bonus3, bd.get_bonus(Color(3)), &GameModel::bonus3Changed);
+ set(m_hasMoves3, bd.has_moves(Color(3)), &GameModel::hasMoves3Changed);
+ }
+ m_tmpPoints.clear();
+ if (bd.is_first_piece(Color(0)))
+ {
+ isFirstPieceAny = true;
+ if (! isTrigon)
+ for (Point p : bd.get_starting_points(Color(0)))
+ m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p)));
+ }
+ set(m_startingPoints0, m_tmpPoints, &GameModel::startingPoints0Changed);
+ m_tmpPoints.clear();
+ if (bd.is_first_piece(Color(1)))
+ {
+ isFirstPieceAny = true;
+ if (! isTrigon)
+ for (Point p : bd.get_starting_points(Color(1)))
+ m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p)));
+ }
+ set(m_startingPoints1, m_tmpPoints, &GameModel::startingPoints1Changed);
+ m_tmpPoints.clear();
+ if (m_nuColors > 2 && bd.is_first_piece(Color(2)))
+ {
+ isFirstPieceAny = true;
+ if (! isTrigon)
+ for (Point p : bd.get_starting_points(Color(2)))
+ m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p)));
+ }
+ set(m_startingPoints2, m_tmpPoints, &GameModel::startingPoints2Changed);
+ m_tmpPoints.clear();
+ if (m_nuColors > 3 && bd.is_first_piece(Color(3)))
+ {
+ isFirstPieceAny = true;
+ if (! isTrigon)
+ for (Point p : bd.get_starting_points(Color(3)))
+ m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p)));
+ }
+ set(m_startingPoints3, m_tmpPoints, &GameModel::startingPoints3Changed);
+ m_tmpPoints.clear();
+ if (isTrigon && isFirstPieceAny)
+ for (Point p : bd.get_starting_points(Color(0)))
+ m_tmpPoints.append(QPointF(geo.get_x(p), geo.get_y(p)));
+ set(m_startingPointsAll, m_tmpPoints,
+ &GameModel::startingPointsAllChanged);
+ auto& current = m_game.get_current();
+ set(m_canUndo,
+ ! current.has_children() && tree.has_move_ignore_invalid(current)
+ && current.has_parent(),
+ &GameModel::canUndoChanged);
+ set(m_canGoForward, current.has_children(),
+ &GameModel::canGoForwardChanged);
+ set(m_canGoBackward, current.has_parent(),
+ &GameModel::canGoBackwardChanged);
+ set(m_hasPrevVar, (current.get_previous_sibling() != nullptr),
+ &GameModel::hasPrevVarChanged);
+ set(m_hasNextVar, (current.get_sibling() != nullptr),
+ &GameModel::hasNextVarChanged);
+ set(m_hasVariations, tree.has_variations(),
+ &GameModel::hasVariationsChanged);
+ set(m_isMainVar, is_main_variation(current),
+ &GameModel::isMainVarChanged);
+ auto positionInfo
+ = QString::fromLocal8Bit(get_position_info(tree, current).c_str());
+ if (positionInfo.isEmpty())
+ positionInfo = bd.has_setup() ? tr("(Setup)") : tr("(No moves)");
+ else
+ {
+ positionInfo = tr("Move %1").arg(positionInfo);
+ if (bd.get_nu_moves() == 0 && bd.has_setup())
+ {
+ positionInfo.append(' ');
+ positionInfo.append(tr("(Setup)"));
+ }
+ }
+ set(m_positionInfo, positionInfo, &GameModel::positionInfoChanged);
+ bool isGameOver = true;
+ for (Color c : bd.get_colors())
+ if (bd.has_moves(c))
+ {
+ isGameOver = false;
+ break;
+ }
+ set(m_isGameOver, isGameOver, &GameModel::isGameOverChanged);
+ set(m_isGameEmpty, libboardgame_sgf::util::is_empty(tree),
+ &GameModel::isGameEmptyChanged);
+
+ ColorMap<array<bool, Board::max_pieces>> isPlayed;
+ for (Color c : bd.get_colors())
+ {
+ isPlayed[c].fill(false);
+ for (Move mv : bd.get_setup().placements[c])
+ updatePiece(c, mv, isPlayed[c]);
+ }
+ PieceModel* lastMovePieceModel = nullptr;
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ {
+ auto mv = bd.get_move(i);
+ auto c = mv.color;
+ lastMovePieceModel = updatePiece(c, mv.move, isPlayed[c]);
+ }
+ if (lastMovePieceModel != m_lastMovePieceModel)
+ {
+ if (m_lastMovePieceModel != nullptr)
+ m_lastMovePieceModel->setIsLastMove(false);
+ if (lastMovePieceModel != nullptr)
+ lastMovePieceModel->setIsLastMove(true);
+ m_lastMovePieceModel = lastMovePieceModel;
+ }
+ for (Color c : bd.get_colors())
+ {
+ auto& pieceModels = getPieceModels(c);
+ for (int i = 0; i < pieceModels.length(); ++i)
+ if (! isPlayed[c][i] && pieceModels[i]->isPlayed())
+ {
+ pieceModels[i]->setDefaultState();
+ pieceModels[i]->setIsPlayed(false);
+ }
+ }
+
+ set(m_toPlay, m_isGameOver ? 0 : bd.get_effective_to_play().to_int(),
+ &GameModel::toPlayChanged);
+ set(m_altPlayer,
+ bd.get_variant() == Variant::classic_3 ? bd.get_alt_player() : 0,
+ &GameModel::altPlayerChanged);
+
+ emit positionChanged();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_qml/GameModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_QML_GAME_MODEL_H
+#define PENTOBI_QML_GAME_MODEL_H
+
+#include <QQmlListProperty>
+#include "PieceModel.h"
+#include "libpentobi_base/Game.h"
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libpentobi_base::Board;
+using libpentobi_base::Game;
+using libpentobi_base::Move;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class GameModel
+ : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString gameVariant MEMBER m_gameVariant
+ NOTIFY gameVariantChanged)
+ Q_PROPERTY(QString positionInfo MEMBER m_positionInfo
+ NOTIFY positionInfoChanged)
+ Q_PROPERTY(QString lastInputOutputError MEMBER m_lastInputOutputError)
+ Q_PROPERTY(int nuColors MEMBER m_nuColors NOTIFY nuColorsChanged)
+ Q_PROPERTY(int toPlay MEMBER m_toPlay NOTIFY toPlayChanged)
+ Q_PROPERTY(int altPlayer MEMBER m_altPlayer NOTIFY altPlayerChanged)
+ Q_PROPERTY(float points0 MEMBER m_points0 NOTIFY points0Changed)
+ Q_PROPERTY(float points1 MEMBER m_points1 NOTIFY points1Changed)
+ Q_PROPERTY(float points2 MEMBER m_points2 NOTIFY points2Changed)
+ Q_PROPERTY(float points3 MEMBER m_points3 NOTIFY points3Changed)
+ Q_PROPERTY(float bonus0 MEMBER m_bonus0 NOTIFY bonus0Changed)
+ Q_PROPERTY(float bonus1 MEMBER m_bonus1 NOTIFY bonus1Changed)
+ Q_PROPERTY(float bonus2 MEMBER m_bonus2 NOTIFY bonus2Changed)
+ Q_PROPERTY(float bonus3 MEMBER m_bonus3 NOTIFY bonus3Changed)
+ Q_PROPERTY(bool hasMoves0 MEMBER m_hasMoves0 NOTIFY hasMoves0Changed)
+ Q_PROPERTY(bool hasMoves1 MEMBER m_hasMoves1 NOTIFY hasMoves1Changed)
+ Q_PROPERTY(bool hasMoves2 MEMBER m_hasMoves2 NOTIFY hasMoves2Changed)
+ Q_PROPERTY(bool hasMoves3 MEMBER m_hasMoves3 NOTIFY hasMoves3Changed)
+ Q_PROPERTY(bool isGameOver MEMBER m_isGameOver NOTIFY isGameOverChanged)
+ Q_PROPERTY(bool isGameEmpty MEMBER m_isGameEmpty NOTIFY isGameEmptyChanged)
+ Q_PROPERTY(bool canUndo MEMBER m_canUndo NOTIFY canUndoChanged)
+ Q_PROPERTY(bool canGoBackward MEMBER m_canGoBackward
+ NOTIFY canGoBackwardChanged)
+ Q_PROPERTY(bool canGoForward MEMBER m_canGoForward
+ NOTIFY canGoForwardChanged)
+ Q_PROPERTY(bool hasPrevVar MEMBER m_hasPrevVar NOTIFY hasPrevVarChanged)
+ Q_PROPERTY(bool hasNextVar MEMBER m_hasNextVar NOTIFY hasNextVarChanged)
+ Q_PROPERTY(bool hasVariations MEMBER m_hasVariations
+ NOTIFY hasVariationsChanged)
+ Q_PROPERTY(bool isMainVar MEMBER m_isMainVar NOTIFY isMainVarChanged)
+ Q_PROPERTY(QQmlListProperty<PieceModel> pieceModels0 READ pieceModels0)
+ Q_PROPERTY(QQmlListProperty<PieceModel> pieceModels1 READ pieceModels1)
+ Q_PROPERTY(QQmlListProperty<PieceModel> pieceModels2 READ pieceModels2)
+ Q_PROPERTY(QQmlListProperty<PieceModel> pieceModels3 READ pieceModels3)
+ Q_PROPERTY(QVariantList startingPoints0 MEMBER m_startingPoints0
+ NOTIFY startingPoints0Changed)
+ Q_PROPERTY(QVariantList startingPoints1 MEMBER m_startingPoints1
+ NOTIFY startingPoints1Changed)
+ Q_PROPERTY(QVariantList startingPoints2 MEMBER m_startingPoints2
+ NOTIFY startingPoints2Changed)
+ Q_PROPERTY(QVariantList startingPoints3 MEMBER m_startingPoints3
+ NOTIFY startingPoints3Changed)
+ Q_PROPERTY(QVariantList startingPointsAll MEMBER m_startingPointsAll
+ NOTIFY startingPointsAllChanged)
+
+public:
+ static Variant getInitialGameVariant();
+
+ explicit GameModel(QObject* parent = nullptr);
+
+ Q_INVOKABLE void deleteAllVar();
+
+ Q_INVOKABLE bool isLegalPos(PieceModel* pieceModel, const QString& state,
+ QPointF coord) const;
+
+ Q_INVOKABLE void nextColor();
+
+ Q_INVOKABLE void playPiece(PieceModel* pieceModel, QPointF coord);
+
+ Q_INVOKABLE void playMove(int move);
+
+ Q_INVOKABLE void newGame();
+
+ Q_INVOKABLE void undo();
+
+ Q_INVOKABLE void goBeginning();
+
+ Q_INVOKABLE void goBackward();
+
+ Q_INVOKABLE void goForward();
+
+ Q_INVOKABLE void goEnd();
+
+ Q_INVOKABLE void goNextVar();
+
+ Q_INVOKABLE void goPrevVar();
+
+ Q_INVOKABLE void backToMainVar();
+
+ Q_INVOKABLE void initGameVariant(const QString& gameVariant);
+
+ Q_INVOKABLE void autoSave();
+
+ Q_INVOKABLE bool loadAutoSave();
+
+ /** Find the piece model for a given move and set its transform and game
+ coordinates accordingly but do not set its status to played yet. */
+ Q_INVOKABLE PieceModel* preparePiece(int color, int move);
+
+ Q_INVOKABLE bool save(const QString& file);
+
+ Q_INVOKABLE bool open(const QString& file);
+
+ Q_INVOKABLE void makeMainVar();
+
+ Q_INVOKABLE void moveDownVar();
+
+ Q_INVOKABLE void moveUpVar();
+
+ Q_INVOKABLE void truncate();
+
+ Q_INVOKABLE void truncateChildren();
+
+ Q_INVOKABLE QString getResultMessage();
+
+ QQmlListProperty<PieceModel> pieceModels0();
+
+ QQmlListProperty<PieceModel> pieceModels1();
+
+ QQmlListProperty<PieceModel> pieceModels2();
+
+ QQmlListProperty<PieceModel> pieceModels3();
+
+ const Board& getBoard() const { return m_game.get_board(); }
+
+signals:
+ /** Position is about to change due to new game or navigation or editing of
+ the game tree. */
+ void positionAboutToChange();
+
+ /** Position changed due to new game or navigation or editing of the
+ game tree. */
+ void positionChanged();
+
+ void toPlayChanged(int);
+
+ void altPlayerChanged(int);
+
+ void points0Changed(float);
+
+ void points1Changed(float);
+
+ void points2Changed(float);
+
+ void points3Changed(float);
+
+ void bonus0Changed(float);
+
+ void bonus1Changed(float);
+
+ void bonus2Changed(float);
+
+ void bonus3Changed(float);
+
+ void hasMoves0Changed(bool);
+
+ void hasMoves1Changed(bool);
+
+ void hasMoves2Changed(bool);
+
+ void hasMoves3Changed(bool);
+
+ void hasVariationsChanged(bool);
+
+ void isGameOverChanged(bool);
+
+ void isGameEmptyChanged(bool);
+
+ void isMainVarChanged(bool);
+
+ void canUndoChanged(bool);
+
+ void canGoBackwardChanged(bool);
+
+ void canGoForwardChanged(bool);
+
+ void hasPrevVarChanged(bool);
+
+ void hasNextVarChanged(bool);
+
+ void gameVariantChanged(QString);
+
+ void positionInfoChanged(QString);
+
+ void nuColorsChanged(int);
+
+ void startingPoints0Changed(QVariantList);
+
+ void startingPoints1Changed(QVariantList);
+
+ void startingPoints2Changed(QVariantList);
+
+ void startingPoints3Changed(QVariantList);
+
+ void startingPointsAllChanged(QVariantList);
+
+private:
+ Game m_game;
+
+ QString m_gameVariant;
+
+ QString m_positionInfo;
+
+ QString m_lastInputOutputError;
+
+ int m_nuColors;
+
+ int m_toPlay = 0;
+
+ int m_altPlayer = 0;
+
+ float m_points0 = 0;
+
+ float m_points1 = 0;
+
+ float m_points2 = 0;
+
+ float m_points3 = 0;
+
+ float m_bonus0 = 0;
+
+ float m_bonus1 = 0;
+
+ float m_bonus2 = 0;
+
+ float m_bonus3 = 0;
+
+ bool m_hasMoves0 = true;
+
+ bool m_hasMoves1 = true;
+
+ bool m_hasMoves2 = true;
+
+ bool m_hasMoves3 = true;
+
+ bool m_hasVariations = false;
+
+ bool m_isGameOver = false;
+
+ bool m_isGameEmpty = true;
+
+ bool m_canUndo = false;
+
+ bool m_canGoForward = false;
+
+ bool m_canGoBackward = false;
+
+ bool m_hasPrevVar = false;
+
+ bool m_hasNextVar = false;
+
+ bool m_isMainVar = true;
+
+ QList<PieceModel*> m_pieceModels0;
+
+ QList<PieceModel*> m_pieceModels1;
+
+ QList<PieceModel*> m_pieceModels2;
+
+ QList<PieceModel*> m_pieceModels3;
+
+ PieceModel* m_lastMovePieceModel = nullptr;
+
+ QVariantList m_startingPoints0;
+
+ QVariantList m_startingPoints1;
+
+ QVariantList m_startingPoints2;
+
+ QVariantList m_startingPoints3;
+
+ QVariantList m_startingPointsAll;
+
+ QVariantList m_tmpPoints;
+
+
+ void createPieceModels();
+
+ void createPieceModels(Color c, QList<PieceModel*>& pieceModels);
+
+ bool findMove(const PieceModel& pieceModel, const QString& state,
+ QPointF coord, Move& mv) const;
+
+ QList<PieceModel*>& getPieceModels(Color c);
+
+ void gotoNode(const SgfNode& node);
+
+ void gotoNode(const SgfNode* node);
+
+ bool open(istream& in);
+
+ void preparePieceGameCoord(PieceModel* pieceModel, Move mv);
+
+ void preparePieceTransform(PieceModel* pieceModel, Move mv);
+
+ template<typename T>
+ void set(T& target, const T& value, void (GameModel::*changedSignal)(T));
+
+ PieceModel* updatePiece(Color c, Move mv,
+ array<bool, Board::max_pieces>& isPlayed);
+
+ void updateProperties();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_QML_GAME_MODEL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_qml/Main.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <QApplication>
+#include <QCommandLineParser>
+#include <QMessageBox>
+#include <QTranslator>
+#include <QtQml>
+#include "GameModel.h"
+#include "PlayerModel.h"
+#include "libboardgame_util/Log.h"
+
+using libboardgame_util::RandomGenerator;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char *argv[])
+{
+ libboardgame_util::LogInitializer log_initializer;
+ QApplication app(argc, argv);
+ app.setOrganizationName("Pentobi");
+ app.setApplicationName("Pentobi");
+#ifdef VERSION
+ app.setApplicationVersion(VERSION);
+#endif
+ qmlRegisterType<GameModel>("pentobi", 1, 0, "GameModel");
+ qmlRegisterType<PlayerModel>("pentobi", 1, 0, "PlayerModel");
+ qmlRegisterInterface<PieceModel>("PieceModel");
+ QString locale = QLocale::system().name();
+ QTranslator translatorPentobi;
+ translatorPentobi.load("qml_" + locale, ":qml/i18n");
+ app.installTranslator(&translatorPentobi);
+ // The translation of standard buttons in QtQuick.Dialogs.MessageDialog
+ // is broken on Android (tested with Qt 5.5; QTBUG-43353), so we
+ // created our own file, which contains the translations we need.
+ QTranslator translatorQt;
+ translatorQt.load("replace_qtbase_" + locale, ":qml/i18n");
+ app.installTranslator(&translatorQt);
+ QCommandLineParser parser;
+ QCommandLineOption optionNoBook("nobook");
+ parser.addOption(optionNoBook);
+ QCommandLineOption optionNoDelay("nodelay");
+ parser.addOption(optionNoDelay);
+ QCommandLineOption optionSeed("seed", "Set random seed to <n>.", "n");
+ parser.addOption(optionSeed);
+ QCommandLineOption optionThreads("threads", "Use <n> threads (0=auto).",
+ "n");
+ parser.addOption(optionThreads);
+ QCommandLineOption optionVerbose("verbose");
+ parser.addOption(optionVerbose);
+ parser.process(app);
+ try
+ {
+#if LIBBOARDGAME_DISABLE_LOG
+ if (parser.isSet(optionVerbose))
+ throw runtime_error("This version of Pentobi was compiled"
+ " without support for logging.");
+#else
+ if (! parser.isSet(optionVerbose))
+ libboardgame_util::disable_logging();
+#endif
+ if (parser.isSet(optionNoBook))
+ PlayerModel::noBook = true;
+ if (parser.isSet(optionNoDelay))
+ PlayerModel::noDelay = true;
+ bool ok;
+ if (parser.isSet(optionSeed))
+ {
+ auto seed = parser.value(optionSeed).toUInt(&ok);
+ if (! ok)
+ throw runtime_error("--seed must be a positive number");
+ RandomGenerator::set_global_seed(seed);
+ }
+ if (parser.isSet(optionThreads))
+ {
+ auto nuThreads = parser.value(optionThreads).toUInt(&ok);
+ if (! ok)
+ throw runtime_error("--threads must be a positive number");
+ PlayerModel::nuThreads = nuThreads;
+ }
+ QQmlApplicationEngine engine(QUrl("qrc:///qml/Main.qml"));
+ return app.exec();
+ }
+ catch (const bad_alloc&)
+ {
+ // bad_alloc is an expected error because the player requires a larger
+ // amount of memory.
+ QMessageBox::critical(nullptr, app.translate("main", "Pentobi"),
+ app.translate("main", "Not enough memory."));
+ return 1;
+ }
+ catch (const exception& e)
+ {
+ cerr << "Error: " << e.what() << '\n';
+ return 1;
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+TEMPLATE = app
+
+QT += qml quick svg concurrent
+
+INCLUDEPATH += ..
+CONFIG += c++11
+QMAKE_CXXFLAGS += -DVERSION=\"\\\"12.2\\\"\"
+QMAKE_CXXFLAGS += -DPENTOBI_LOW_RESOURCES
+android {
+ QMAKE_CXXFLAGS_RELEASE += -DLIBBOARDGAME_DISABLE_LOG
+}
+QMAKE_CXXFLAGS_DEBUG += -DLIBBOARDGAME_DEBUG
+gcc {
+ QMAKE_CXXFLAGS_RELEASE -= -O
+ QMAKE_CXXFLAGS_RELEASE -= -O1
+ QMAKE_CXXFLAGS_RELEASE -= -O2
+ QMAKE_CXXFLAGS_RELEASE -= -O3
+ QMAKE_CXXFLAGS_RELEASE -= -Os
+ QMAKE_CXXFLAGS_RELEASE *= -Ofast
+}
+
+SOURCES += \
+ GameModel.cpp \
+ Main.cpp \
+ PieceModel.cpp \
+ PlayerModel.cpp \
+ ../libboardgame_base/CoordPoint.cpp \
+ ../libboardgame_base/Rating.cpp \
+ ../libboardgame_base/RectTransform.cpp \
+ ../libboardgame_base/StringRep.cpp \
+ ../libboardgame_base/Transform.cpp \
+ ../libboardgame_util/Abort.cpp \
+ ../libboardgame_util/Assert.cpp \
+ ../libboardgame_util/Barrier.cpp \
+ ../libboardgame_util/CpuTimeSource.cpp \
+ ../libboardgame_util/IntervalChecker.cpp \
+ ../libboardgame_util/Log.cpp \
+ ../libboardgame_util/RandomGenerator.cpp \
+ ../libboardgame_util/StringUtil.cpp \
+ ../libboardgame_util/TimeIntervalChecker.cpp \
+ ../libboardgame_util/Timer.cpp \
+ ../libboardgame_util/TimeSource.cpp \
+ ../libboardgame_util/WallTimeSource.cpp \
+ ../libboardgame_sgf/MissingProperty.cpp \
+ ../libboardgame_sgf/Reader.cpp \
+ ../libboardgame_sgf/SgfNode.cpp \
+ ../libboardgame_sgf/SgfTree.cpp \
+ ../libboardgame_sgf/SgfUtil.cpp \
+ ../libboardgame_sgf/TreeReader.cpp \
+ ../libboardgame_sgf/TreeWriter.cpp \
+ ../libboardgame_sgf/Writer.cpp \
+ ../libboardgame_sys/CpuTime.cpp \
+ ../libboardgame_sys/Memory.cpp \
+ ../libpentobi_base/Board.cpp \
+ ../libpentobi_base/BoardConst.cpp \
+ ../libpentobi_base/BoardUpdater.cpp \
+ ../libpentobi_base/BoardUtil.cpp \
+ ../libpentobi_base/Book.cpp \
+ ../libpentobi_base/CallistoGeometry.cpp \
+ ../libpentobi_base/Color.cpp \
+ ../libpentobi_base/Game.cpp \
+ ../libpentobi_base/NexosGeometry.cpp \
+ ../libpentobi_base/NodeUtil.cpp \
+ ../libpentobi_base/PentobiSgfUtil.cpp \
+ ../libpentobi_base/PentobiTreeWriter.cpp \
+ ../libpentobi_base/PieceInfo.cpp \
+ ../libpentobi_base/PieceTransforms.cpp \
+ ../libpentobi_base/PieceTransformsClassic.cpp \
+ ../libpentobi_base/PieceTransformsTrigon.cpp \
+ ../libpentobi_base/PointState.cpp \
+ ../libpentobi_base/StartingPoints.cpp \
+ ../libpentobi_base/SymmetricPoints.cpp \
+ ../libpentobi_base/TreeUtil.cpp \
+ ../libpentobi_base/TrigonGeometry.cpp \
+ ../libpentobi_base/TrigonTransform.cpp \
+ ../libpentobi_base/Variant.cpp \
+ ../libpentobi_base/PlayerBase.cpp \
+ ../libpentobi_base/PentobiTree.cpp \
+ ../libpentobi_mcts/History.cpp \
+ ../libpentobi_mcts/Player.cpp \
+ ../libpentobi_mcts/PlayoutFeatures.cpp \
+ ../libpentobi_mcts/PriorKnowledge.cpp \
+ ../libpentobi_mcts/Search.cpp \
+ ../libpentobi_mcts/SharedConst.cpp \
+ ../libpentobi_mcts/State.cpp \
+ ../libpentobi_mcts/Util.cpp \
+ ../libpentobi_mcts/StateUtil.cpp
+
+RESOURCES += \
+ ../books/pentobi_books.qrc \
+ qml/themes/theme_shared.qrc \
+ resources.qrc \
+ translations.qrc
+
+android {
+ RESOURCES += \
+ icons_android.qrc \
+ qml/themes/theme_dark.qrc
+} else {
+ RESOURCES += \
+ ../pentobi/icons.qrc \
+ qml/themes/theme_light.qrc
+}
+
+# Default rules for deployment.
+include(deployment.pri)
+
+HEADERS += \
+ GameModel.h \
+ PieceModel.h \
+ PlayerModel.h \
+ ../libboardgame_base/CoordPoint.h \
+ ../libboardgame_base/Geometry.h \
+ ../libboardgame_base/GeometryUtil.h \
+ ../libboardgame_base/Grid.h \
+ ../libboardgame_base/Marker.h \
+ ../libboardgame_base/Point.h \
+ ../libboardgame_base/PointTransform.h \
+ ../libboardgame_base/Rating.h \
+ ../libboardgame_base/RectGeometry.h \
+ ../libboardgame_base/RectTransform.h \
+ ../libboardgame_base/StringRep.h \
+ ../libboardgame_base/Transform.h \
+ ../libboardgame_mcts/Atomic.h \
+ ../libboardgame_mcts/LastGoodReply.h \
+ ../libboardgame_mcts/Node.h \
+ ../libboardgame_mcts/PlayerMove.h \
+ ../libboardgame_mcts/SearchBase.h \
+ ../libboardgame_mcts/Tree.h \
+ ../libboardgame_mcts/TreeUtil.h \
+ ../libboardgame_util/Abort.h \
+ ../libboardgame_util/ArrayList.h \
+ ../libboardgame_util/Assert.h \
+ ../libboardgame_util/Barrier.h \
+ ../libboardgame_util/CpuTimeSource.h \
+ ../libboardgame_util/FmtSaver.h \
+ ../libboardgame_util/IntervalChecker.h \
+ ../libboardgame_util/Log.h \
+ ../libboardgame_util/MathUtil.h \
+ ../libboardgame_util/Options.h \
+ ../libboardgame_util/RandomGenerator.h \
+ ../libboardgame_util/Statistics.h \
+ ../libboardgame_util/StringUtil.h \
+ ../libboardgame_util/TimeIntervalChecker.h \
+ ../libboardgame_util/Timer.h \
+ ../libboardgame_util/TimeSource.h \
+ ../libboardgame_util/Unused.h \
+ ../libboardgame_util/WallTimeSource.h \
+ ../libboardgame_sgf/InvalidPropertyValue.h \
+ ../libboardgame_sgf/InvalidTree.h \
+ ../libboardgame_sgf/MissingProperty.h \
+ ../libboardgame_sgf/Reader.h \
+ ../libboardgame_sgf/SgfNode.h \
+ ../libboardgame_sgf/SgfTree.h \
+ ../libboardgame_sgf/SgfUtil.h \
+ ../libboardgame_sgf/TreeReader.h \
+ ../libboardgame_sgf/Writer.h \
+ ../libboardgame_sys/Compiler.h \
+ ../libboardgame_sys/CpuTime.h \
+ ../libboardgame_sys/Memory.h \
+ ../libpentobi_base/Board.h \
+ ../libpentobi_base/BoardConst.h \
+ ../libpentobi_base/BoardUpdater.h \
+ ../libpentobi_base/BoardUtil.h \
+ ../libpentobi_base/Book.h \
+ ../libpentobi_base/Color.h \
+ ../libpentobi_base/ColorMap.h \
+ ../libpentobi_base/ColorMove.h \
+ ../libpentobi_base/Game.h \
+ ../libpentobi_base/Geometry.h \
+ ../libpentobi_base/Grid.h \
+ ../libpentobi_base/Marker.h \
+ ../libpentobi_base/Move.h \
+ ../libpentobi_base/MoveInfo.h \
+ ../libpentobi_base/MoveList.h \
+ ../libpentobi_base/MoveMarker.h \
+ ../libpentobi_base/MovePoints.h \
+ ../libpentobi_base/NexosGeometry.h \
+ ../libpentobi_base/NodeUtil.h \
+ ../libpentobi_base/PentobiTree.h \
+ ../libpentobi_base/Piece.h \
+ ../libpentobi_base/PieceInfo.h \
+ ../libpentobi_base/PieceMap.h \
+ ../libpentobi_base/PieceTransforms.h \
+ ../libpentobi_base/PieceTransformsClassic.h \
+ ../libpentobi_base/PieceTransformsTrigon.h \
+ ../libpentobi_base/PlayerBase.h \
+ ../libpentobi_base/Point.h \
+ ../libpentobi_base/PointList.h \
+ ../libpentobi_base/PointState.h \
+ ../libpentobi_base/PrecompMoves.h \
+ ../libpentobi_base/Setup.h \
+ ../libpentobi_base/PentobiSgfUtil.h \
+ ../libpentobi_base/StartingPoints.h \
+ ../libpentobi_base/SymmetricPoints.h \
+ ../libpentobi_base/TreeUtil.h \
+ ../libpentobi_base/TrigonGeometry.h \
+ ../libpentobi_base/TrigonTransform.h \
+ ../libpentobi_base/Variant.h \
+ ../libpentobi_mcts/Float.h \
+ ../libpentobi_mcts/History.h \
+ ../libpentobi_mcts/Player.h \
+ ../libpentobi_mcts/PlayoutFeatures.h \
+ ../libpentobi_mcts/PriorKnowledge.h \
+ ../libpentobi_mcts/Search.h \
+ ../libpentobi_mcts/SearchParamConst.h \
+ ../libpentobi_mcts/SharedConst.h \
+ ../libpentobi_mcts/State.h \
+ ../libpentobi_mcts/StateUtil.h \
+ ../libpentobi_mcts/Util.h
+
+lupdate_only {
+SOURCES += \
+ qml/*.qml \
+ qml/*.js
+}
+
+TRANSLATIONS += \
+ qml/i18n/qml_de.ts \
+ qml/i18n/replace_qtbase_de.ts
+
+OTHER_FILES += \
+ android/AndroidManifest.xml
+
+ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_qml/PieceModel.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PieceModel.h"
+
+#include <QDebug>
+#include "libboardgame_base/RectTransform.h"
+#include "libpentobi_base/TrigonTransform.h"
+
+using namespace std;
+using libboardgame_base::ArrayList;
+using libboardgame_base::CoordPoint;
+using libboardgame_base::TransfIdentity;
+using libboardgame_base::TransfRectRot90;
+using libboardgame_base::TransfRectRot180;
+using libboardgame_base::TransfRectRot270;
+using libboardgame_base::TransfRectRefl;
+using libboardgame_base::TransfRectRot90Refl;
+using libboardgame_base::TransfRectRot180Refl;
+using libboardgame_base::TransfRectRot270Refl;
+using libpentobi_base::BoardType;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::PieceSet;
+using libpentobi_base::TransfTrigonIdentity;
+using libpentobi_base::TransfTrigonRefl;
+using libpentobi_base::TransfTrigonReflRot60;
+using libpentobi_base::TransfTrigonReflRot120;
+using libpentobi_base::TransfTrigonReflRot180;
+using libpentobi_base::TransfTrigonReflRot240;
+using libpentobi_base::TransfTrigonReflRot300;
+using libpentobi_base::TransfTrigonRot60;
+using libpentobi_base::TransfTrigonRot120;
+using libpentobi_base::TransfTrigonRot180;
+using libpentobi_base::TransfTrigonRot240;
+using libpentobi_base::TransfTrigonRot300;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+PieceModel::PieceModel(QObject* parent, const Board& bd, Piece piece, Color c)
+ : QObject(parent),
+ m_bd(bd),
+ m_color(c),
+ m_piece(piece)
+{
+ auto& geo = bd.get_geometry();
+ bool isNexos = (bd.get_piece_set() == PieceSet::nexos);
+ bool isCallisto = (bd.get_piece_set() == PieceSet::callisto);
+ auto& info = bd.get_piece_info(piece);
+ auto& points = info.get_points();
+ m_elements.reserve(points.size());
+ for (auto& p : points)
+ {
+ if (isNexos && geo.get_point_type(p) == 0)
+ continue;
+ m_elements.append(QPointF(p.x, p.y));
+ }
+ if (isNexos)
+ {
+ ArrayList<CoordPoint, 2 * PieceInfo::max_scored_size> candidates;
+ for (auto& p : points)
+ {
+ auto pointType = geo.get_point_type(p);
+ if (pointType == 1)
+ {
+ candidates.include(CoordPoint(p.x - 1, p. y));
+ candidates.include(CoordPoint(p.x + 1, p. y));
+ }
+ else if (pointType == 2)
+ {
+ candidates.include(CoordPoint(p.x, p. y - 1));
+ candidates.include(CoordPoint(p.x, p. y + 1));
+ }
+ }
+ m_junctions.reserve(candidates.size());
+ m_junctionType.reserve(candidates.size());
+ for (auto& p : candidates)
+ {
+ bool hasLeft = points.contains(CoordPoint(p.x - 1, p. y));
+ bool hasRight = points.contains(CoordPoint(p.x + 1, p. y));
+ bool hasUp = points.contains(CoordPoint(p.x, p. y - 1));
+ bool hasDown = points.contains(CoordPoint(p.x, p. y + 1));
+ int junctionType;
+ if (hasLeft && hasRight && hasUp && hasDown)
+ junctionType = 0;
+ else if (hasRight && hasUp && hasDown)
+ junctionType = 1;
+ else if (hasLeft && hasUp && hasDown)
+ junctionType = 2;
+ else if (hasLeft && hasRight && hasDown)
+ junctionType = 3;
+ else if (hasLeft && hasRight && hasUp)
+ junctionType = 4;
+ else if (hasLeft && hasRight)
+ junctionType = 5;
+ else if (hasUp && hasDown)
+ junctionType = 6;
+ else if (hasLeft && hasUp)
+ junctionType = 7;
+ else if (hasLeft && hasDown)
+ junctionType = 8;
+ else if (hasRight && hasUp)
+ junctionType = 9;
+ else if (hasRight && hasDown)
+ junctionType = 10;
+ else
+ continue;
+ m_junctions.append(QPointF(p.x, p.y));
+ m_junctionType.append(junctionType);
+ }
+ }
+ if (isCallisto)
+ for (auto& p : points)
+ {
+ bool hasRight = points.contains(CoordPoint(p.x + 1, p. y));
+ bool hasDown = points.contains(CoordPoint(p.x, p. y + 1));
+ int junctionType;
+ if (hasRight && hasDown)
+ junctionType = 0;
+ else if (hasRight)
+ junctionType = 1;
+ else if (hasDown)
+ junctionType = 2;
+ else
+ junctionType = 3;
+ m_junctionType.append(junctionType);
+ }
+ bool isOriginDownward = (m_bd.get_board_type() == BoardType::trigon_3);
+ m_center = findCenter(bd, points, isOriginDownward);
+ m_labelPos = QPointF(info.get_label_pos().x, info.get_label_pos().y);
+}
+
+QPointF PieceModel::center() const
+{
+ return m_center;
+}
+
+int PieceModel::color()
+{
+ return m_color.to_int();
+}
+
+QVariantList PieceModel::elements()
+{
+ return m_elements;
+}
+
+void PieceModel::flipAcrossX()
+{
+ setTransform(m_bd.get_transforms().get_mirrored_vertically(getTransform()));
+}
+
+void PieceModel::flipAcrossY()
+{
+ setTransform(m_bd.get_transforms().get_mirrored_horizontally(getTransform()));
+}
+
+QPointF PieceModel::gameCoord() const
+{
+ return m_gameCoord;
+}
+
+const Transform* PieceModel::getTransform(const QString& state) const
+{
+ auto variant = m_bd.get_variant();
+ bool isTrigon = (variant == Variant::trigon || variant == Variant::trigon_2
+ || variant == Variant::trigon_3);
+ auto& transforms = m_bd.get_transforms();
+ // See comment in getTransform() about the mapping between states and
+ // transform classes.
+ if (state.isEmpty())
+ return isTrigon ? transforms.find<TransfTrigonIdentity>()
+ : transforms.find<TransfIdentity>();
+ if (state == QLatin1String("rot60"))
+ return transforms.find<TransfTrigonRot60>();
+ if (state == QLatin1String("rot90"))
+ return transforms.find<TransfRectRot90>();
+ if (state == QLatin1String("rot120"))
+ return transforms.find<TransfTrigonRot120>();
+ if (state == QLatin1String("rot180"))
+ return isTrigon ? transforms.find<TransfTrigonRot180>()
+ : transforms.find<TransfRectRot180>();
+ if (state == QLatin1String("rot240"))
+ return transforms.find<TransfTrigonRot240>();
+ if (state == QLatin1String("rot270"))
+ return transforms.find<TransfRectRot270>();
+ if (state == QLatin1String("rot300"))
+ return transforms.find<TransfTrigonRot300>();
+ if (state == QLatin1String("flip"))
+ return isTrigon ? transforms.find<TransfTrigonReflRot180>()
+ : transforms.find<TransfRectRot180Refl>();
+ if (state == QLatin1String("rot60Flip"))
+ return transforms.find<TransfTrigonReflRot120>();
+ if (state == QLatin1String("rot90Flip"))
+ return transforms.find<TransfRectRot90Refl>();
+ if (state == QLatin1String("rot120Flip"))
+ return transforms.find<TransfTrigonReflRot60>();
+ if (state == QLatin1String("rot180Flip"))
+ return isTrigon ? transforms.find<TransfTrigonRefl>()
+ : transforms.find<TransfRectRefl>();
+ if (state == QLatin1String("rot240Flip"))
+ return transforms.find<TransfTrigonReflRot300>();
+ if (state == QLatin1String("rot270Flip"))
+ return transforms.find<TransfRectRot270Refl>();
+ if (state == QLatin1String("rot300Flip"))
+ return transforms.find<TransfTrigonReflRot240>();
+ qWarning() << "PieceModel: unknown state " << m_state;
+ return transforms.find<TransfIdentity>();
+}
+
+QPointF PieceModel::findCenter(const Board& bd, const PiecePoints& points,
+ bool isOriginDownward)
+{
+ auto pieceSet = bd.get_piece_set();
+ bool isTrigon = (pieceSet == PieceSet::trigon);
+ bool isNexos = (pieceSet == PieceSet::nexos);
+ auto& geo = bd.get_geometry();
+ qreal sumX = 0;
+ qreal sumY = 0;
+ qreal n = 0;
+ for (auto& p : points)
+ {
+ if (isNexos && geo.get_point_type(p) == 0)
+ continue;
+ ++n;
+ qreal centerX = p.x + 0.5;
+ qreal centerY;
+ if (isTrigon)
+ {
+ bool isDownward =
+ (geo.get_point_type(p) == (isOriginDownward ? 0 : 1));
+ if (isDownward)
+ centerY = static_cast<qreal>(p.y) + 1.f / 3;
+ else
+ centerY = static_cast<qreal>(p.y) + 2.f / 3;
+ }
+ else
+ centerY = p.y + 0.5;
+ sumX += centerX;
+ sumY += centerY;
+ }
+ return QPointF(sumX / n, sumY / n);
+}
+
+bool PieceModel::isLastMove() const
+{
+ return m_isLastMove;
+}
+
+bool PieceModel::isPlayed() const
+{
+ return m_isPlayed;
+}
+
+QVariantList PieceModel::junctions()
+{
+ return m_junctions;
+}
+
+QVariantList PieceModel::junctionType()
+{
+ return m_junctionType;
+}
+
+QPointF PieceModel::labelPos() const
+{
+ return m_labelPos;
+}
+
+void PieceModel::rotateLeft()
+{
+ setTransform(m_bd.get_transforms().get_rotated_anticlockwise(getTransform()));
+}
+
+void PieceModel::rotateRight()
+{
+ setTransform(m_bd.get_transforms().get_rotated_clockwise(getTransform()));
+}
+
+void PieceModel::setGameCoord(QPointF gameCoord)
+{
+ if (m_gameCoord == gameCoord)
+ return;
+ m_gameCoord = gameCoord;
+ emit gameCoordChanged(gameCoord);
+}
+
+void PieceModel::setIsLastMove(bool isLastMove)
+{
+ if (m_isLastMove == isLastMove)
+ return;
+ m_isLastMove = isLastMove;
+ emit isLastMoveChanged(isLastMove);
+}
+
+void PieceModel::setIsPlayed(bool isPlayed)
+{
+ if (m_isPlayed == isPlayed)
+ return;
+ m_isPlayed = isPlayed;
+ emit isPlayedChanged(isPlayed);
+}
+
+void PieceModel::setDefaultState()
+{
+ if (m_state.isEmpty())
+ return;
+ m_state.clear();
+ emit stateChanged(m_state);
+}
+
+void PieceModel::setTransform(const Transform* transform)
+{
+ QString state;
+ // libboardgame_base uses a different convention for the order of flipping
+ // and rotation, so the names of the states and transform classes differ
+ // for flipped states.
+ if (dynamic_cast<const TransfIdentity*>(transform)
+ || dynamic_cast<const TransfTrigonIdentity*>(transform))
+ ;
+ else if (dynamic_cast<const TransfTrigonRot60*>(transform))
+ state = QLatin1String("rot60");
+ else if (dynamic_cast<const TransfRectRot90*>(transform))
+ state = QLatin1String("rot90");
+ else if (dynamic_cast<const TransfTrigonRot120*>(transform))
+ state = QLatin1String("rot120");
+ else if (dynamic_cast<const TransfRectRot180*>(transform)
+ || dynamic_cast<const TransfTrigonRot180*>(transform))
+ state = QLatin1String("rot180");
+ else if (dynamic_cast<const TransfTrigonRot240*>(transform))
+ state = QLatin1String("rot240");
+ else if (dynamic_cast<const TransfRectRot270*>(transform))
+ state = QLatin1String("rot270");
+ else if (dynamic_cast<const TransfTrigonRot300*>(transform))
+ state = QLatin1String("rot300");
+ else if (dynamic_cast<const TransfRectRot180Refl*>(transform)
+ || dynamic_cast<const TransfTrigonReflRot180*>(transform))
+ state = QLatin1String("flip");
+ else if (dynamic_cast<const TransfTrigonReflRot120*>(transform))
+ state = QLatin1String("rot60Flip");
+ else if (dynamic_cast<const TransfRectRot90Refl*>(transform))
+ state = QLatin1String("rot90Flip");
+ else if (dynamic_cast<const TransfTrigonReflRot60*>(transform))
+ state = QLatin1String("rot120Flip");
+ else if (dynamic_cast<const TransfRectRefl*>(transform)
+ || dynamic_cast<const TransfTrigonRefl*>(transform))
+ state = QLatin1String("rot180Flip");
+ else if (dynamic_cast<const TransfTrigonReflRot300*>(transform))
+ state = QLatin1String("rot240Flip");
+ else if (dynamic_cast<const TransfRectRot270Refl*>(transform))
+ state = QLatin1String("rot270Flip");
+ else if (dynamic_cast<const TransfTrigonReflRot240*>(transform))
+ state = QLatin1String("rot300Flip");
+ else
+ {
+ qWarning() << "Invalid Transform " << typeid(*transform).name();
+ return;
+ }
+ if (m_state == state)
+ return;
+ m_state = state;
+ emit stateChanged(m_state);
+}
+
+QString PieceModel::state() const
+{
+ return m_state;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_qml/PieceModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_QML_PIECE_MODEL_H
+#define PENTOBI_QML_PIECE_MODEL_H
+
+#include <QObject>
+#include <QPointF>
+#include <QVariant>
+#include "libpentobi_base/Board.h"
+
+using libboardgame_base::Transform;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::Piece;
+using libpentobi_base::PiecePoints;
+
+//-----------------------------------------------------------------------------
+
+class PieceModel
+ : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int color READ color CONSTANT)
+ Q_PROPERTY(QVariantList elements READ elements CONSTANT)
+ Q_PROPERTY(QVariantList junctions READ junctions CONSTANT)
+ Q_PROPERTY(QVariantList junctionType READ junctionType CONSTANT)
+ Q_PROPERTY(QPointF center READ center CONSTANT)
+ Q_PROPERTY(QPointF labelPos READ labelPos CONSTANT)
+ Q_PROPERTY(QString state READ state NOTIFY stateChanged)
+ Q_PROPERTY(bool isPlayed READ isPlayed NOTIFY isPlayedChanged)
+ Q_PROPERTY(bool isLastMove READ isLastMove NOTIFY isLastMoveChanged)
+ Q_PROPERTY(QPointF gameCoord READ gameCoord NOTIFY gameCoordChanged)
+
+public:
+ static QPointF findCenter(const Board& bd, const PiecePoints& points,
+ bool isOriginDownward);
+
+ PieceModel(QObject* parent, const Board& bd, Piece piece, Color c);
+
+ int color();
+
+ /** List of QPointF instances with coordinates of piece elements. */
+ QVariantList elements();
+
+ /** List of QPointF instances with coordinates of piece junctions.
+ Only used in Nexos. */
+ QVariantList junctions();
+
+ /** List of integers determining the type of junctions.
+ In Nexos, this is the type of junction in junction(). In Callisto, it
+ is the information if the squares in elements() have a right and/or
+ down neighbor. See implementation for the meaning of the numbers. */
+ QVariantList junctionType();
+
+ QPointF center() const;
+
+ QPointF labelPos() const;
+
+ QString state() const;
+
+ bool isPlayed() const;
+
+ bool isLastMove() const;
+
+ QPointF gameCoord() const;
+
+ Piece getPiece() const { return m_piece; }
+
+ const Transform* getTransform(const QString& state) const;
+
+ const Transform* getTransform() const { return getTransform(m_state); }
+
+ void setDefaultState();
+
+ void setTransform(const Transform* transform);
+
+ void setIsPlayed(bool isPlayed);
+
+ void setIsLastMove(bool isLastMove);
+
+ void setGameCoord(QPointF gameCoord);
+
+ Q_INVOKABLE void rotateLeft();
+
+ Q_INVOKABLE void rotateRight();
+
+ Q_INVOKABLE void flipAcrossX();
+
+ Q_INVOKABLE void flipAcrossY();
+
+signals:
+ void stateChanged(QString);
+
+ void isPlayedChanged(bool);
+
+ void isLastMoveChanged(bool);
+
+ void gameCoordChanged(QPointF);
+
+private:
+ const Board& m_bd;
+
+ Color m_color;
+
+ Piece m_piece;
+
+ bool m_isPlayed = false;
+
+ bool m_isLastMove = false;
+
+ QPointF m_gameCoord;
+
+ QPointF m_center;
+
+ QPointF m_labelPos;
+
+ QVariantList m_elements;
+
+ QVariantList m_junctions;
+
+ QVariantList m_junctionType;
+
+ QString m_state;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_QML_PIECE_MODEL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_qml/PlayerModel.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PlayerModel.h"
+
+#include <QElapsedTimer>
+#include <QFile>
+#include <QtConcurrentRun>
+#include <QSettings>
+
+using namespace std;
+using libboardgame_util::clear_abort;
+using libboardgame_util::set_abort;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+unsigned maxLevel = 7;
+
+void getLevel(QSettings& settings, const char* key, unsigned& level)
+{
+ level = settings.value(key, 1).toUInt();
+ if (level < 1)
+ {
+ qDebug() << "PlayerModel: invalid level in settings:" << level;
+ level = 1;
+ }
+ else if (level > maxLevel)
+ {
+ qDebug() << "PlayerModel: level in settings too high, using level"
+ << maxLevel;
+ level = maxLevel;
+ }
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+bool PlayerModel::noBook = false;
+
+bool PlayerModel::noDelay = false;
+
+unsigned PlayerModel::nuThreads = 0;
+
+PlayerModel::PlayerModel(QObject* parent)
+ : QObject(parent),
+ m_player(GameModel::getInitialGameVariant(), maxLevel, "", nuThreads)
+{
+ if (noBook)
+ m_player.set_use_book(false);
+ QSettings settings;
+ getLevel(settings, "level_classic", m_levelClassic);
+ getLevel(settings, "level_classic_2", m_levelClassic2);
+ getLevel(settings, "level_classic_3", m_levelClassic3);
+ getLevel(settings, "level_duo", m_levelDuo);
+ getLevel(settings, "level_trigon", m_levelTrigon);
+ getLevel(settings, "level_trigon_2", m_levelTrigon2);
+ getLevel(settings, "level_trigon_3", m_levelTrigon3);
+ getLevel(settings, "level_junior", m_levelJunior);
+ getLevel(settings, "level_nexos", m_levelNexos);
+ getLevel(settings, "level_nexos_2", m_levelNexos2);
+ getLevel(settings, "level_callisto", m_levelCallisto);
+ getLevel(settings, "level_callisto_2", m_levelCallisto2);
+ getLevel(settings, "level_callisto_3", m_levelCallisto3);
+ connect(&m_genMoveWatcher, SIGNAL(finished()), SLOT(genMoveFinished()));
+}
+
+PlayerModel::~PlayerModel()
+{
+ cancelGenMove();
+ QSettings settings;
+ settings.setValue("level_classic", m_levelClassic);
+ settings.setValue("level_classic_2", m_levelClassic2);
+ settings.setValue("level_classic_3", m_levelClassic3);
+ settings.setValue("level_duo", m_levelDuo);
+ settings.setValue("level_trigon", m_levelTrigon);
+ settings.setValue("level_trigon_2", m_levelTrigon2);
+ settings.setValue("level_trigon_3", m_levelTrigon3);
+ settings.setValue("level_junior", m_levelJunior);
+ settings.setValue("level_nexos", m_levelNexos);
+ settings.setValue("level_nexos_2", m_levelNexos2);
+ settings.setValue("level_callisto", m_levelCallisto);
+ settings.setValue("level_callisto_2", m_levelCallisto2);
+ settings.setValue("level_callisto_3", m_levelCallisto3);
+}
+
+PlayerModel::GenMoveResult PlayerModel::asyncGenMove(GameModel* gm,
+ unsigned genMoveId)
+{
+ QElapsedTimer timer;
+ timer.start();
+ auto& bd = gm->getBoard();
+ GenMoveResult result;
+ result.genMoveId = genMoveId;
+ result.gameModel = gm;
+ result.move = m_player.genmove(bd, bd.get_effective_to_play());
+ auto elapsed = timer.elapsed();
+ // Enforce minimum thinking time of 1 sec
+ if (elapsed < 1000 && ! noDelay)
+ QThread::msleep(1000 - elapsed);
+ return result;
+}
+
+void PlayerModel::cancelGenMove()
+{
+ if (! m_isGenMoveRunning)
+ return;
+ // After waitForFinished() returns, we can be sure that the move generation
+ // is no longer running, but we will still receive the finished event.
+ // Increasing m_genMoveId will make genMoveFinished() ignore the event.
+ ++m_genMoveId;
+ set_abort();
+ m_genMoveWatcher.waitForFinished();
+ setIsGenMoveRunning(false);
+}
+
+void PlayerModel::genMoveFinished()
+{
+ auto result = m_genMoveWatcher.future().result();
+ if (result.genMoveId != m_genMoveId)
+ // Callback from a canceled move generation
+ return;
+ setIsGenMoveRunning(false);
+ auto& bd = result.gameModel->getBoard();
+ auto mv = result.move;
+ if (mv.is_null())
+ {
+ qWarning("PlayerModel: failed to generate move");
+ return;
+ }
+ Color c = bd.get_effective_to_play();
+ if (! bd.is_legal(c, mv))
+ {
+ qWarning("PlayerModel: player generated illegal move");
+ return;
+ }
+ emit moveGenerated(mv.to_int());
+}
+
+void PlayerModel::loadBook(Variant variant)
+{
+ QFile file(QString(":/pentobi_books/book_%1.blksgf")
+ .arg(to_string_id(variant)));
+ if (! file.open(QIODevice::ReadOnly))
+ {
+ qWarning() << "PlayerModel: could not open " << file.fileName();
+ return;
+ }
+ QTextStream stream(&file);
+ QString text = stream.readAll();
+ istringstream in(text.toLocal8Bit().constData());
+ m_player.load_book(in);
+}
+
+void PlayerModel::setIsGenMoveRunning(bool isGenMoveRunning)
+{
+ if (m_isGenMoveRunning == isGenMoveRunning)
+ return;
+ m_isGenMoveRunning = isGenMoveRunning;
+ emit isGenMoveRunningChanged(isGenMoveRunning);
+}
+
+void PlayerModel::startGenMove(GameModel* gm)
+{
+ unsigned level;
+ switch (gm->getBoard().get_variant())
+ {
+ case Variant::classic_2:
+ level = m_levelClassic2;
+ break;
+ case Variant::classic_3:
+ level = m_levelClassic3;
+ break;
+ case Variant::duo:
+ level = m_levelDuo;
+ break;
+ case Variant::trigon:
+ level = m_levelTrigon;
+ break;
+ case Variant::trigon_2:
+ level = m_levelTrigon2;
+ break;
+ case Variant::trigon_3:
+ level = m_levelTrigon3;
+ break;
+ case Variant::nexos:
+ level = m_levelNexos;
+ break;
+ case Variant::nexos_2:
+ level = m_levelNexos2;
+ break;
+ case Variant::callisto:
+ level = m_levelCallisto;
+ break;
+ case Variant::callisto_2:
+ level = m_levelCallisto2;
+ break;
+ case Variant::callisto_3:
+ level = m_levelCallisto3;
+ break;
+ default:
+ level = m_levelClassic;
+ }
+ startGenMoveAtLevel(gm, level);
+}
+
+void PlayerModel::startGenMoveAtLevel(GameModel* gm, unsigned level)
+{
+ cancelGenMove();
+ m_player.set_level(level);
+ auto variant = gm->getBoard().get_variant();
+ if (! m_player.is_book_loaded(variant))
+ loadBook(variant);
+ clear_abort();
+ ++m_genMoveId;
+ QFuture<GenMoveResult> future =
+ QtConcurrent::run(this, &PlayerModel::asyncGenMove, gm,
+ m_genMoveId);
+ m_genMoveWatcher.setFuture(future);
+ setIsGenMoveRunning(true);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_qml/PlayerModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_QML_PLAYER_MODEL_H
+#define PENTOBI_QML_PLAYER_MODEL_H
+
+#include <QFutureWatcher>
+#include "GameModel.h"
+#include "libpentobi_mcts/Player.h"
+
+using namespace std;
+using libpentobi_mcts::Player;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class PlayerModel
+ : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(unsigned levelClassic MEMBER m_levelClassic
+ NOTIFY levelClassicChanged)
+ Q_PROPERTY(unsigned levelClassic2 MEMBER m_levelClassic2
+ NOTIFY levelClassic2Changed)
+ Q_PROPERTY(unsigned levelClassic3 MEMBER m_levelClassic3
+ NOTIFY levelClassic3Changed)
+ Q_PROPERTY(unsigned levelDuo MEMBER m_levelDuo NOTIFY levelDuoChanged)
+ Q_PROPERTY(unsigned levelTrigon MEMBER m_levelTrigon
+ NOTIFY levelTrigonChanged)
+ Q_PROPERTY(unsigned levelTrigon2 MEMBER m_levelTrigon2
+ NOTIFY levelTrigon2Changed)
+ Q_PROPERTY(unsigned levelTrigon3 MEMBER m_levelTrigon3
+ NOTIFY levelTrigon3Changed)
+ Q_PROPERTY(unsigned levelJunior MEMBER m_levelJunior
+ NOTIFY levelJuniorChanged)
+ Q_PROPERTY(unsigned levelNexos MEMBER m_levelNexos NOTIFY
+ levelNexosChanged)
+ Q_PROPERTY(unsigned levelNexos2 MEMBER m_levelNexos2 NOTIFY
+ levelNexos2Changed)
+ Q_PROPERTY(unsigned levelCallisto MEMBER m_levelCallisto
+ NOTIFY levelCallistoChanged)
+ Q_PROPERTY(unsigned levelCallisto2 MEMBER m_levelCallisto2
+ NOTIFY levelCallisto2Changed)
+ Q_PROPERTY(unsigned levelCallisto3 MEMBER m_levelCallisto3
+ NOTIFY levelCallisto3Changed)
+ Q_PROPERTY(bool isGenMoveRunning MEMBER m_isGenMoveRunning
+ NOTIFY isGenMoveRunningChanged)
+
+public:
+ /** Global variable to disable opening books. */
+ static bool noBook;
+
+ /** Global variable to disable the minimum thinking time. */
+ static bool noDelay;
+
+ /** Global variable to set the number of threads the player is constructed
+ with.
+ The default value 0 means that the number of threads depends on the
+ hardware. */
+ static unsigned nuThreads;
+
+
+ explicit PlayerModel(QObject* parent = nullptr);
+
+ ~PlayerModel();
+
+
+ /** Start a move generation in a background thread.
+ The state of the board model may not be changed until the move
+ generation was finished (computerPlayed signal) or aborted
+ with cancelGenMove() */
+ Q_INVOKABLE void startGenMove(GameModel* gameModel);
+
+ Q_INVOKABLE void startGenMoveAtLevel(GameModel* gameModel, unsigned level);
+
+ /** Cancel the move generation in the background thread if one is
+ running. */
+ Q_INVOKABLE void cancelGenMove();
+
+signals:
+ void levelCallistoChanged(unsigned);
+
+ void levelCallisto2Changed(unsigned);
+
+ void levelCallisto3Changed(unsigned);
+
+ void levelClassicChanged(unsigned);
+
+ void levelClassic2Changed(unsigned);
+
+ void levelClassic3Changed(unsigned);
+
+ void levelDuoChanged(unsigned);
+
+ void levelTrigonChanged(unsigned);
+
+ void levelTrigon2Changed(unsigned);
+
+ void levelTrigon3Changed(unsigned);
+
+ void levelJuniorChanged(unsigned);
+
+ void levelNexosChanged(unsigned);
+
+ void levelNexos2Changed(unsigned);
+
+ void isGenMoveRunningChanged(bool);
+
+ void moveGenerated(int move);
+
+private:
+ struct GenMoveResult
+ {
+ Color color;
+
+ Move move;
+
+ unsigned genMoveId;
+
+ GameModel* gameModel;
+ };
+
+ bool m_isGenMoveRunning = false;
+
+ unsigned m_levelCallisto;
+
+ unsigned m_levelCallisto2;
+
+ unsigned m_levelCallisto3;
+
+ unsigned m_levelClassic;
+
+ unsigned m_levelClassic2;
+
+ unsigned m_levelClassic3;
+
+ unsigned m_levelDuo;
+
+ unsigned m_levelTrigon;
+
+ unsigned m_levelTrigon2;
+
+ unsigned m_levelTrigon3;
+
+ unsigned m_levelJunior;
+
+ unsigned m_levelNexos;
+
+ unsigned m_levelNexos2;
+
+ unsigned m_genMoveId = 0;
+
+ Player m_player;
+
+ QFutureWatcher<GenMoveResult> m_genMoveWatcher;
+
+
+ GenMoveResult asyncGenMove(GameModel* gm, unsigned genMoveId);
+
+ void loadBook(Variant variant);
+
+ void setIsGenMoveRunning(bool isGenMoveRunning);
+
+private slots:
+ void genMoveFinished();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_QML_PLAYER_MODEL_H
--- /dev/null
+<?xml version="1.0"?>
+<manifest package="net.sf.pentobi" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="12.2" android:versionCode="12002" android:installLocation="auto">
+ <application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="Pentobi" android:theme="@style/AppTheme" android:icon="@drawable/icon">
+ <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|locale|fontScale|keyboard|keyboardHidden|navigation" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:launchMode="singleTop">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ <meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
+ <meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
+ <meta-data android:name="android.app.repository" android:value="default"/>
+ <meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
+ <meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
+ <!-- Deploy Qt libs as part of package -->
+ <meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
+ <meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
+ <meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
+ <!-- Run with local libs -->
+ <meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
+ <meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
+ <meta-data android:name="android.app.load_local_libs" android:value="-- %%INSERT_LOCAL_LIBS%% --"/>
+ <meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
+ <meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
+ <!-- Messages maps -->
+ <meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
+ <meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
+ <meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
+ <!-- Messages maps -->
+
+ <!-- Splash screen -->
+ <meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splash"/>
+ <!-- Splash screen -->
+ </activity>
+ </application>
+ <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="9"/>
+ <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
+
+ <!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
+ Remove the comment if you do not require these default permissions. -->
+
+
+ <!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
+ Remove the comment if you do not require these default features. -->
+
+
+<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+
+</manifest>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle" >
+ <solid android:color="#131313"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap android:src="@drawable/icon" android:gravity="center" />
+ </item>
+</layer-list>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="AppTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
+ <item name="android:windowBackground">@drawable/splash</item>
+ </style>
+</resources>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="e" transform="matrix(1.099991 0 0 1.099983 18.49986 -17.799)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909098-0.909578h8.181886v-8.181946l0.9093462-0.909106v10z"/>
+<path d="m-15 28v-10h10l-0.9090162 0.908476h-8.181886v8.181946z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#e" transform="translate(10.99991)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(10.99991 10.99983)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(21.99982 10.99983)" height="48" width="48" y="0" x="0"/>
+<g id="f" transform="matrix(1.099991 0 0 1.099983 18.49986 -6.79917)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909098-0.90942h8.181886v-8.181947l0.9093462-0.909263v10z"/>
+<path d="m-15 28v-10h10l-0.9090162 0.908633h-8.181886v8.181947z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#f" transform="translate(1.222212e-8 10.99983)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(1.222212e-8 21.99965)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(10.99991 21.99965)" height="72" width="72" y="0" x="0"/>
+<g id="g" transform="matrix(1.099991 0 0 1.099983 29.49977 4.200657)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909181-0.909262h8.181886v-8.181947l0.9092634-0.909421v10z"/>
+<path d="m-15 28v-10h10l-0.9089334 0.908791h-8.181886v8.181947z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#g" transform="translate(10.99991 10.99983)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(10.99991)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(21.99982 10.99983)" height="72" width="72" y="0" x="0"/>
+<g id="h" transform="matrix(1.099991 0 0 1.099983 40.49968 -17.799)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+<path fill-opacity=".1568628" d="m-15 28 0.909263-0.909578h8.181886v-8.181946l0.9091807-0.909106v10z"/>
+<path d="m-15 28v-10h10l-0.9088507 0.908476h-8.181886v8.181946z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#h" transform="translate(10.99991)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(10.99991 10.99983)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(10.99991 21.99965)" height="72" width="72" y="0" x="0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="72" width="72" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="e" transform="matrix(1.699996 0 0 1.699996 27.49994 -28.59993)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176473-1.176448h7.647078v-7.647078l1.176549-1.176374v10z"/>
+<path d="m-15 28v-10h10l-1.176449 1.176474h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#e" transform="translate(16.99996 2.0042e-7)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(16.99996 16.99996)" height="48" width="48" y="0" x="0"/>
+<use xlink:href="#e" transform="translate(33.99992 16.99996)" height="48" width="48" y="0" x="0"/>
+<g id="f" transform="matrix(1.699996 0 0 1.699996 27.49994 -11.59997)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176473-1.176423h7.647078v-7.647078l1.176549-1.176399v10z"/>
+<path d="m-15 28v-10h10l-1.176449 1.176499h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#f" transform="translate(1.888884e-8 16.99996)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(1.888884e-8 33.99992)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#f" transform="translate(16.99996 33.99992)" height="72" width="72" y="0" x="0"/>
+<g id="g" transform="matrix(1.699996 0 0 1.699996 44.49989 5.39999)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176498-1.176398h7.647078v-7.647078l1.176524-1.176424v10z"/>
+<path d="m-15 28v-10h10l-1.176424 1.176524h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#g" transform="translate(16.99996 16.99996)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(16.99996 4.0042e-7)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#g" transform="translate(33.99992 16.99996)" height="72" width="72" y="0" x="0"/>
+<g id="h" transform="matrix(1.699996 0 0 1.699996 61.49985 -28.59993)">
+<rect opacity="0.99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+<path fill-opacity=".1568628" d="m-15 28 1.176523-1.176448h7.647078v-7.647078l1.176499-1.176374v10z"/>
+<path d="m-15 28v-10h10l-1.176399 1.176474h-7.647078v7.647078z" fill-opacity=".1568628" fill="#fff"/>
+</g>
+<use xlink:href="#h" transform="translate(16.99996 2.0042e-7)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(16.99996 16.99996)" height="72" width="72" y="0" x="0"/>
+<use xlink:href="#h" transform="translate(16.99996 33.99992)" height="72" width="72" y="0" x="0"/>
+</svg>
--- /dev/null
+android-no-sdk {
+ target.path = /data/user/qt
+ export(target.path)
+ INSTALLS += target
+} else:android {
+ x86 {
+ target.path = /libs/x86
+ } else: armeabi-v7a {
+ target.path = /libs/armeabi-v7a
+ } else {
+ target.path = /libs/armeabi
+ }
+ export(target.path)
+ INSTALLS += target
+} else:unix {
+ isEmpty(target.path) {
+ qnx {
+ target.path = /tmp/$${TARGET}/bin
+ } else {
+ target.path = /opt/$${TARGET}/bin
+ }
+ export(target.path)
+ }
+ INSTALLS += target
+}
+
+export(INSTALLS)
--- /dev/null
+<RCC>
+ <qresource prefix="/">
+ <file>qml/icons/menu.svg</file>
+ <file>qml/icons/pentobi-backward.svg</file>
+ <file>qml/icons/pentobi-beginning.svg</file>
+ <file>qml/icons/pentobi-computer-colors.svg</file>
+ <file>qml/icons/pentobi-end.svg</file>
+ <file>qml/icons/pentobi-forward.svg</file>
+ <file>qml/icons/pentobi-newgame.svg</file>
+ <file>qml/icons/pentobi-next-variation.svg</file>
+ <file>qml/icons/pentobi-play.svg</file>
+ <file>qml/icons/pentobi-previous-variation.svg</file>
+ <file>qml/icons/pentobi-undo.svg</file>
+ </qresource>
+</RCC>
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.0
+import "Main.js" as Logic
+
+RowLayout {
+ function popupMenu() { menu.popup() }
+
+ spacing: 0
+
+ Item { Layout.fillWidth: true }
+ AndroidToolButton {
+ imageSource: "icons/pentobi-newgame.svg"
+ visible: ! gameModel.isGameEmpty
+ onClicked: Logic.newGame()
+ }
+ AndroidToolButton {
+ visible: gameModel.canUndo
+ imageSource: "icons/pentobi-undo.svg"
+ onClicked: Logic.undo()
+ }
+ AndroidToolButton {
+ imageSource: "icons/pentobi-computer-colors.svg"
+ onClicked: Logic.showComputerColorDialog()
+ }
+ AndroidToolButton {
+ visible: ! gameModel.isGameOver
+ imageSource: "icons/pentobi-play.svg"
+ onClicked: Logic.computerPlay()
+ }
+ AndroidToolButton {
+ imageSource: "icons/menu.svg"
+ menu: menu
+ }
+ Menu {
+ id: menu
+
+ MenuGame { }
+ MenuGo { }
+ MenuEdit { }
+ MenuComputer { }
+ MenuView { }
+ MenuHelp { }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import QtQuick.Window 2.0
+
+ToolButton {
+ property string imageSource
+
+ Image {
+ // We currently use 22x22 SVG files, try to use 22x22 or 44x44, unless
+ // very high DPI
+ width: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity
+ height: width
+ sourceSize { width: width; height: height }
+ anchors.centerIn: parent
+ source: imageSource
+ cache: false
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+Item {
+ id: root
+
+ property string gameVariant
+ property bool isTrigon: gameVariant.indexOf("trigon") === 0
+ property bool isNexos: gameVariant.indexOf("nexos") === 0
+ property bool isCallisto: gameVariant.indexOf("callisto") === 0
+ property int columns: {
+ switch (gameVariant) {
+ case "duo":
+ case "junior":
+ return 14
+ case "callisto_2":
+ return 16
+ case "trigon":
+ case "trigon_2":
+ return 35
+ case "trigon_3":
+ return 31
+ case "nexos":
+ case "nexos_2":
+ return 25
+ default:
+ return 20
+ }
+ }
+ property int rows: {
+ switch (gameVariant) {
+ case "duo":
+ case "junior":
+ return 14
+ case "callisto_2":
+ return 16
+ case "trigon":
+ case "trigon_2":
+ return 18
+ case "trigon_3":
+ return 16
+ case "nexos":
+ case "nexos_2":
+ return 25
+ default:
+ return 20
+ }
+ }
+ // Avoid fractional piece element sizes if the piece elements are squares
+ property real gridWidth: {
+ var sideLength
+ if (isTrigon) sideLength = Math.min(width, Math.sqrt(3) * height)
+ else sideLength = Math.min(width, height)
+ if (isTrigon) return sideLength / (columns + 1)
+ else if (isNexos) Math.floor(sideLength / (columns - 0.5))
+ else return Math.floor(sideLength / columns)
+ }
+ property real gridHeight: {
+ if (isTrigon) return Math.sqrt(3) * gridWidth
+ else return gridWidth
+ }
+ property real startingPointSize: {
+ if (isTrigon) return 0.27 * gridHeight
+ if (isNexos) return 0.3 * gridHeight
+ return 0.35 * gridHeight
+ }
+
+ function mapFromGameX(x) {
+ if (isTrigon) return image.x + (x + 0.5) * gridWidth
+ else if (isNexos) return image.x + (x - 0.25) * gridWidth
+ else return image.x + x * gridWidth
+ }
+ function mapFromGameY(y) {
+ if (isNexos) return image.y + (y - 0.25) * gridHeight
+ else return image.y + y * gridHeight
+ }
+ function mapToGame(pos) {
+ if (isTrigon)
+ return Qt.point((pos.x - image.x - 0.5 * gridWidth) / gridWidth,
+ (pos.y - image.y) / gridHeight)
+ else if (isNexos)
+ return Qt.point((pos.x - image.x + 0.25 * gridWidth) / gridWidth,
+ (pos.y - image.y + 0.25 * gridHeight) / gridHeight)
+ else
+ return Qt.point((pos.x - image.x) / gridWidth,
+ (pos.y - image.y) / gridHeight)
+ }
+ function getCenterYTrigon(pos) {
+
+ var isDownward = ((pos.x % 2 == 0) != (pos.y % 2 == 0))
+ if (gameVariant === "trigon_3")
+ isDownward = ! isDownward
+ return (isDownward ? 1 : 2) / 3 * gridHeight
+ }
+
+ Image {
+ id: image
+
+ width: {
+ if (isTrigon) return gridWidth * (columns + 1)
+ else if (isNexos) return gridWidth * (columns - 0.5)
+ else return gridWidth * columns
+ }
+ height: {
+ if (isNexos) return gridHeight * (rows - 0.5)
+ else return gridHeight * rows
+ }
+ anchors.centerIn: root
+ source: {
+ switch (gameVariant) {
+ case "trigon":
+ case "trigon_2":
+ return theme.getImage("board-trigon")
+ case "trigon_3":
+ return theme.getImage("board-trigon-3")
+ case "nexos":
+ case "nexos_2":
+ return theme.getImage("board-tile-nexos")
+ case "callisto":
+ return theme.getImage("board-callisto")
+ case "callisto_2":
+ return theme.getImage("board-callisto-2")
+ case "callisto_3":
+ return theme.getImage("board-callisto-3")
+ default:
+ return theme.getImage("board-tile-classic")
+ }
+ }
+ sourceSize {
+ width: {
+ if (isTrigon || isCallisto) return width
+ if (isNexos) return 2 * gridWidth
+ return gridWidth
+ }
+ height: {
+ if (isTrigon || isCallisto) return height
+ if (isNexos) return 2 * gridHeight
+ return gridHeight
+ }
+ }
+ // It should work to use Image.Tile for all game variants, but the
+ // Trigon board is not painted with Image.width/height even if
+ // sourceSize is bound to it (the Trigon SVG files have a different
+ // aspect ratio but that shouldn't matter). Bug in Qt 5.6?
+ fillMode: isTrigon? Image.Stretch : Image.Tile
+ horizontalAlignment: Image.AlignLeft
+ verticalAlignment: Image.AlignTop
+ cache: false
+ }
+ Repeater {
+ model: gameModel.startingPoints0
+
+ Rectangle {
+ color: theme.colorBlue
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: mapFromGameX(modelData.x) + (gridWidth - width) / 2
+ y: mapFromGameY(modelData.y) + (gridHeight - height) / 2
+ }
+ }
+ Repeater {
+ model: gameModel.startingPoints1
+
+ Rectangle {
+ color: gameModel.gameVariant == "duo"
+ || gameModel.gameVariant == "junior"
+ || gameModel.gameVariant == "callisto_2" ?
+ theme.colorGreen : theme.colorYellow
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: mapFromGameX(modelData.x) + (gridWidth - width) / 2
+ y: mapFromGameY(modelData.y) + (gridHeight - height) / 2
+ }
+ }
+ Repeater {
+ model: gameModel.startingPoints2
+
+ Rectangle {
+ color: theme.colorRed
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: mapFromGameX(modelData.x) + (gridWidth - width) / 2
+ y: mapFromGameY(modelData.y) + (gridHeight - height) / 2
+ }
+ }
+ Repeater {
+ model: gameModel.startingPoints3
+
+ Rectangle {
+ color: theme.colorGreen
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: mapFromGameX(modelData.x) + (gridWidth - width) / 2
+ y: mapFromGameY(modelData.y) + (gridHeight - height) / 2
+ }
+ }
+ Repeater {
+ model: gameModel.startingPointsAll
+
+ Rectangle {
+ color: theme.colorStartingPoint
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: mapFromGameX(modelData.x) + (gridWidth - width) / 2
+ y: mapFromGameY(modelData.y) + getCenterYTrigon(modelData)
+ - height / 2
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Window 2.0
+import Qt.labs.controls 1.0 as Controls2
+
+/** Button that supports an automatically scaled image.
+ The image source should be a SVG file with size 22x22. */
+Controls2.Button {
+ id: root
+
+ property string imageSource
+
+ label: Image {
+ sourceSize {
+ // We currently use 22x22 SVG files, try to use 22x22 or 44x44, unless
+ // very high DPI
+ width: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity
+ height: Screen.pixelDensity < 5 ? 22 : Screen.pixelDensity < 10 ? 44 : 5 * Screen.pixelDensity
+ }
+ fillMode: Image.PreserveAspectFit
+ source: imageSource
+ opacity: root.enabled ? 1 : 0.4
+ cache: false
+ }
+ background: Rectangle {
+ anchors.fill: root
+ visible: pressed
+ color: theme.backgroundButtonPressed
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import QtQuick.Dialogs 1.2
+
+Dialog {
+ property string gameVariant
+ property alias computerPlays0: checkBox0.checked
+ property alias computerPlays1: checkBox1.checked
+ property alias computerPlays2: checkBox2.checked
+ property alias computerPlays3: checkBox3.checked
+
+ title: qsTr("Computer Colors")
+ standardButtons: StandardButton.Ok | StandardButton.Cancel
+
+ GroupBox {
+ title: qsTr("Computer plays:")
+ flat: true
+
+ Column {
+ CheckBox {
+ id: checkBox0
+
+ text: {
+ switch (gameVariant) {
+ case "classic_2":
+ case "trigon_2":
+ case "nexos_2":
+ return qsTr("Blue/Red")
+ default:
+ qsTr("Blue")
+ }
+ }
+ onClicked: {
+ if (gameVariant == "classic_2" || gameVariant == "trigon_2"
+ || gameVariant == "nexos_2")
+ computerPlays2 = checked
+ }
+ }
+ CheckBox {
+ id: checkBox1
+
+ text: {
+ switch (gameVariant) {
+ case "classic_2":
+ case "trigon_2":
+ case "nexos_2":
+ return qsTr("Yellow/Green")
+ case "duo":
+ case "junior":
+ case "callisto_2":
+ return qsTr("Green")
+ default:
+ qsTr("Yellow")
+ }
+ }
+ onClicked: {
+ if (gameVariant == "classic_2" || gameVariant == "trigon_2"
+ || gameVariant == "nexos_2")
+ computerPlays3 = checked
+ }
+ }
+ CheckBox {
+ id: checkBox2
+
+ text: qsTr("Red")
+ visible: gameVariant == "classic" || gameVariant == "trigon"
+ || gameVariant == "trigon_3"
+ || gameVariant == "classic_3"
+ || gameVariant == "nexos"
+ || gameVariant == "callisto_3"
+ || gameVariant == "callisto"
+ }
+ CheckBox {
+ id: checkBox3
+
+ text: qsTr("Green")
+ visible: gameVariant == "classic" || gameVariant == "trigon"
+ || gameVariant == "nexos" || gameVariant == "callisto"
+ }
+ }
+ }
+}
--- /dev/null
+function createColorPieces(component, pieceModels) {
+ if (pieceModels.length === 0)
+ return []
+ var colorName
+ switch (pieceModels[0].color) {
+ case 0: colorName = "blue"; break
+ case 1:
+ colorName = gameModel.gameVariant == "duo"
+ || gameModel.gameVariant == "junior"
+ || gameModel.gameVariant == "callisto_2" ?
+ "green" : "yellow"; break
+ case 2: colorName = "red"; break
+ case 3: colorName = "green"; break
+ }
+ var properties = {
+ "colorName": colorName,
+ "isPicked": Qt.binding(function() { return this === pickedPiece }),
+ "isMarked": Qt.binding(function() {
+ return markLastMove && this.pieceModel.isLastMove })
+ }
+ var pieces = []
+ for (var i = 0; i < pieceModels.length; ++i) {
+ properties["pieceModel"] = pieceModels[i]
+ pieces.push(component.createObject(gameDisplay, properties))
+ }
+ return pieces
+}
+
+function createPieces() {
+ var file
+ if (gameModel.gameVariant.indexOf("trigon") === 0)
+ file = "PieceTrigon.qml"
+ else if (gameModel.gameVariant.indexOf("nexos") === 0)
+ file = "PieceNexos.qml"
+ else if (gameModel.gameVariant.indexOf("callisto") === 0)
+ file = "PieceCallisto.qml"
+ else
+ file = "PieceClassic.qml"
+ var component = Qt.createComponent(file)
+ pieces0 = createColorPieces(component, gameModel.pieceModels0)
+ pieces1 = createColorPieces(component, gameModel.pieceModels1)
+ pieces2 = createColorPieces(component, gameModel.pieceModels2)
+ pieces3 = createColorPieces(component, gameModel.pieceModels3)
+ pieceSelector.transitionsEnabled =
+ Qt.binding(function() { return enableAnimations })
+}
+
+function destroyColorPieces(pieces) {
+ if (pieces === undefined)
+ return
+ for (var i = 0; i < pieces.length; ++i) {
+ pieces[i].visible = false
+ pieces[i].destroy(1000)
+ }
+}
+
+function destroyPieces() {
+ pieceSelector.transitionsEnabled = false
+ pickedPiece = null
+ destroyColorPieces(pieces0); pieces0 = []
+ destroyColorPieces(pieces1); pieces1 = []
+ destroyColorPieces(pieces2); pieces2 = []
+ destroyColorPieces(pieces3); pieces3 = []
+}
+
+function findPiece(pieceModel, color) {
+ var pieces
+ switch (color) {
+ case 0: pieces = pieces0; break
+ case 1: pieces = pieces1; break
+ case 2: pieces = pieces2; break
+ case 3: pieces = pieces3; break
+ }
+ if (pieces === undefined)
+ return null // Pieces haven't been created yet
+ for (var i = 0; i < pieces.length; ++i)
+ if (pieces[i].pieceModel === pieceModel)
+ return pieces[i]
+ return null
+}
+
+function pickPiece(piece) {
+ if (playerModel.isGenMoveRunning || gameModel.isGameOver
+ || piece.pieceModel.color !== gameModel.toPlay)
+ return
+ if (! pieceManipulator.visible) {
+ // Position pieceManipulator at center of piece if possible, but
+ // make sure it is completely visible
+ var newCoord = mapFromItem(piece, 0, 0)
+ var x = newCoord.x - pieceManipulator.width / 2
+ var y = newCoord.y - pieceManipulator.height / 2
+ x = Math.max(Math.min(x, width - pieceManipulator.width), 0)
+ y = Math.max(Math.min(y, height - pieceManipulator.height), 0)
+ pieceManipulator.x = x
+ pieceManipulator.y = y
+ }
+ pickedPiece = piece
+}
+
+function showMoveHint(move) {
+ var pieceModel = gameModel.preparePiece(gameModel.toPlay, move)
+ var pos = board.mapToItem(pieceManipulator.parent,
+ board.mapFromGameX(pieceModel.gameCoord.x),
+ board.mapFromGameY(pieceModel.gameCoord.y))
+ pieceManipulator.x = pos.x - pieceManipulator.width / 2
+ pieceManipulator.y = pos.y - pieceManipulator.height / 2
+ pickedPiece = findPiece(pieceModel, gameModel.toPlay)
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import "GameDisplay.js" as Logic
+
+Item
+{
+ id: gameDisplay // Referenced by Piece*.qml
+
+ property var pickedPiece: null
+ property bool markLastMove: true
+ property bool enableAnimations: true
+ property alias busyIndicatorRunning: busyIndicator.running
+ property size imageSourceSize: {
+ var width = board.gridWidth, height = board.gridHeight
+ if (board.isTrigon)
+ return Qt.size(2 * width, height)
+ if (board.isNexos)
+ return Qt.size(1.5 * width, 1.5 * height)
+ if (board.isCallisto)
+ return Qt.size(0.9 * width, 0.9 * height)
+ return Qt.size(width, height)
+ }
+ property alias pieces0: pieceSelector.pieces0
+ property alias pieces1: pieceSelector.pieces1
+ property alias pieces2: pieceSelector.pieces2
+ property alias pieces3: pieceSelector.pieces3
+
+ signal play(var pieceModel, point gameCoord)
+
+ function createPieces() { Logic.createPieces() }
+ function destroyPieces() { Logic.destroyPieces() }
+ function showToPlay() { pieceSelector.contentY = 0 }
+ function showMoveHint(move) { Logic.showMoveHint(move) }
+
+ onWidthChanged: pickedPiece = null
+ onHeightChanged: pickedPiece = null
+
+ Column {
+ id: column
+
+ width: gameDisplay.width
+ anchors.centerIn: gameDisplay
+ spacing: 0.01 * board.width
+
+ Board {
+ id: board
+
+ gameVariant: gameModel.gameVariant
+ width: Math.min(
+ parent.width,
+ gameDisplay.height / (1.07 + 2.7 / pieceSelector.columns))
+ height: isTrigon ? Math.sqrt(3) / 2 * width : width
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ ScoreDisplay {
+ id: scoreDisplay
+
+ gameVariant: gameModel.gameVariant
+ points0: gameModel.points0
+ points1: gameModel.points1
+ points2: gameModel.points2
+ points3: gameModel.points3
+ bonus0: gameModel.bonus0
+ bonus1: gameModel.bonus1
+ bonus2: gameModel.bonus2
+ bonus3: gameModel.bonus3
+ hasMoves0: gameModel.hasMoves0
+ hasMoves1: gameModel.hasMoves1
+ hasMoves2: gameModel.hasMoves2
+ hasMoves3: gameModel.hasMoves3
+ toPlay: gameModel.isGameOver ? -1 : gameModel.toPlay
+ altPlayer: gameModel.altPlayer
+ height: board.width / 20
+ pointSize: 0.6 * height
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ Flickable {
+ id: flickable
+
+ width: 0.9 * board.width
+ height: width / pieceSelector.columns * pieceSelector.rows
+ contentWidth: 2 * width
+ contentHeight: height
+ anchors.horizontalCenter: board.horizontalCenter
+ clip: true
+ onMovementEnded: {
+ snapAnimation.to = contentX > width / 2 ? width : 0
+ snapAnimation.restart()
+ }
+
+ Row {
+ id: flickableContent
+
+ PieceSelector {
+ id: pieceSelector
+
+ columns: gameModel.gameVariant.indexOf("classic") == 0
+ || gameModel.gameVariant.indexOf("callisto") == 0
+ || gameModel.gameVariant == "duo" ? 7 : 8
+ width: flickable.width
+ height: flickable.height
+ rows: 3
+ gameVariant: gameModel.gameVariant
+ toPlay: gameModel.toPlay
+ nuColors: gameModel.nuColors
+ transitionsEnabled: false
+ onPiecePicked: Logic.pickPiece(piece)
+ }
+ NavigationPanel {
+ width: flickable.width
+ height: flickable.height
+ }
+ }
+ SmoothedAnimation {
+ id: snapAnimation
+
+ target: flickable
+ property: "contentX"
+ duration: 200
+ }
+ }
+ }
+ BusyIndicator {
+ id: busyIndicator
+
+ x: (gameDisplay.width - width) / 2
+ y: column.y + flickable.y + (flickable.height - height) / 2
+ }
+ PieceManipulator {
+ id: pieceManipulator
+
+ legal: {
+ if (pickedPiece === null)
+ return false
+ // Don't use mapToItem(board, width / 2, height / 2), we want a
+ // dependency on x, y.
+ var pos = parent.mapToItem(board, x + width / 2, y + height / 2)
+ return gameModel.isLegalPos(pickedPiece.pieceModel,
+ pickedPiece.pieceModel.state,
+ board.mapToGame(pos))
+ }
+ width: 0.6 * board.width; height: width
+ visible: pickedPiece !== null
+ pieceModel: pickedPiece !== null ? pickedPiece.pieceModel : null
+ onPiecePlayed: {
+ var pos = mapToItem(board, width / 2, height / 2)
+ if (! board.contains(Qt.point(pos.x, pos.y)))
+ pickedPiece = null
+ else if (legal)
+ play(pieceModel, board.mapToGame(pos))
+ }
+ }
+ Connections {
+ target: gameModel
+ onPositionChanged: pickedPiece = null
+ }
+}
--- /dev/null
+import QtQuick 2.3
+
+// Piece element for Nexos. See Square.qml for comments.
+Item {
+ id: root
+
+ property bool isHorizontal
+
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component0
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity0
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component0
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ mirror: ! isHorizontal
+ rotation: isHorizontal ? 0 : -90
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component90
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity90
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component90
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ mirror: isHorizontal
+ rotation: isHorizontal ? -180 : -90
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component180
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity180
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component180
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ mirror: ! isHorizontal
+ rotation: isHorizontal ? -180 : -270
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component270
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity270
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component270
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ mirror: isHorizontal
+ rotation: isHorizontal ? 0 : -270
+ }
+ }
+ }
+}
--- /dev/null
+function about() {
+ var url = "http://pentobi.sourceforge.net"
+ showInfo("<h2>" + qsTr("Pentobi") + "</h2><p>" +
+ qsTr("Version %1").arg(Qt.application.version) + "</p><p>" +
+ qsTr("Computer opponent for the board game Blokus.") + "<br>" +
+ qsTr("© 2011–%1 Markus Enzenberger").arg(2017) +
+ "<br><a href=\"" + url + "\">" + url + "</a></p>")
+}
+
+function changeGameVariant(gameVariant) {
+ if (gameModel.gameVariant === gameVariant)
+ return
+ if (! gameModel.isGameEmpty && ! gameModel.isGameOver) {
+ showQuestion(qsTr("New game?"),
+ function() { changeGameVariantNoVerify(gameVariant) })
+ return
+ }
+ changeGameVariantNoVerify(gameVariant)
+}
+
+function changeGameVariantNoVerify(gameVariant) {
+ cancelGenMove()
+ lengthyCommand.run(function() {
+ gameDisplay.destroyPieces()
+ gameModel.initGameVariant(gameVariant)
+ gameDisplay.createPieces()
+ gameDisplay.showToPlay()
+ initComputerColors()
+ })
+}
+
+function checkComputerMove() {
+ if (gameModel.isGameOver) {
+ showInfo(gameModel.getResultMessage())
+ return
+ }
+ if (! isComputerToPlay())
+ return
+ switch (gameModel.toPlay) {
+ case 0: if (! gameModel.hasMoves0) return; break
+ case 1: if (! gameModel.hasMoves1) return; break
+ case 2: if (! gameModel.hasMoves2) return; break
+ case 3: if (! gameModel.hasMoves3) return; break
+ }
+ genMove();
+}
+
+/** If the computer already plays the current color to play, start generating
+ a move; if he doesn't, make him play the current color (and only the
+ current color). */
+function computerPlay() {
+ if (playerModel.isGenMoveRunning)
+ return
+ if (! isComputerToPlay()) {
+ computerPlays0 = false
+ computerPlays1 = false
+ computerPlays2 = false
+ computerPlays3 = false
+ var variant = gameModel.gameVariant
+ if (variant == "classic_3" && gameModel.toPlay == 3) {
+ switch (gameModel.altPlayer) {
+ case 0: computerPlays0 = true; break
+ case 1: computerPlays1 = true; break
+ case 2: computerPlays2 = true; break
+ }
+ }
+ else
+ {
+ var isMultiColor =
+ (variant == "classic_2" || variant == "trigon_2"
+ || variant == "nexos_2")
+ switch (gameModel.toPlay) {
+ case 0:
+ computerPlays0 = true
+ if (isMultiColor) computerPlays2 = true
+ break;
+ case 1:
+ computerPlays1 = true
+ if (isMultiColor) computerPlays3 = true
+ break;
+ case 2:
+ computerPlays2 = true
+ if (isMultiColor) computerPlays0 = true
+ break;
+ case 3:
+ computerPlays3 = true
+ if (isMultiColor) computerPlays1 = true
+ break;
+ }
+ }
+ }
+ checkComputerMove()
+}
+
+function computerPlays(color) {
+ switch (color) {
+ case 0: return computerPlays0
+ case 1: return computerPlays1
+ case 2: return computerPlays2
+ case 3: return computerPlays3
+ }
+}
+
+function createTheme(themeName) {
+ var source = "qrc:///qml/themes/" + themeName + "/Theme.qml"
+ return Qt.createComponent(source).createObject(root)
+}
+
+function deleteAllVar() {
+ showQuestion(qsTr("Delete all variations?"), gameModel.deleteAllVar)
+}
+
+function genMove() {
+ gameDisplay.pickedPiece = null
+ isMoveHintRunning = false
+ playerModel.startGenMove(gameModel)
+}
+
+function getFileFromUrl(fileUrl) {
+ var file = fileUrl.toString()
+ file = file.replace(/^(file:\/{3})/,"/")
+ return decodeURIComponent(file)
+}
+
+function init() {
+ // Settings might contain unusable geometry
+ var maxWidth = Screen.desktopAvailableWidth
+ var maxHeight = Screen.desktopAvailableHeight
+ if (x < 0 || x + width > maxWidth || y < 0 || y + height > maxHeight) {
+ if (width > maxWidth || height > Screen.maxHeight) {
+ width = defaultWidth
+ height = defaultHeight
+ }
+ x = (maxWidth - width) / 2
+ y = (maxHeight - height) / 2
+ }
+ if (! gameModel.loadAutoSave()) {
+ gameDisplay.createPieces()
+ initComputerColors()
+ }
+ else {
+ gameDisplay.createPieces()
+ if (! gameModel.isGameOver)
+ checkComputerMove()
+ }
+}
+
+function initComputerColors() {
+ // Default setting is that the computer plays all colors but the first
+ computerPlays0 = false
+ computerPlays1 = true
+ computerPlays2 = true
+ computerPlays3 = true
+ if (gameModel.gameVariant == "classic_2"
+ || gameModel.gameVariant == "trigon_2"
+ || gameModel.gameVariant == "nexos_2")
+ computerPlays2 = false
+}
+
+function isComputerToPlay() {
+ if (gameModel.gameVariant == "classic_3" && gameModel.toPlay == 3)
+ return computerPlays(gameModel.altPlayer)
+ return computerPlays(gameModel.toPlay)
+}
+
+function moveGenerated(move) {
+ if (isMoveHintRunning) {
+ gameDisplay.showMoveHint(move)
+ isMoveHintRunning = false
+ return
+ }
+ gameModel.playMove(move)
+ delayedCheckComputerMove.restart()
+}
+
+function moveHint() {
+ if (gameModel.isGameOver)
+ return
+ isMoveHintRunning = true
+ playerModel.startGenMoveAtLevel(gameModel, 1)
+}
+
+function newGameNoVerify()
+{
+ gameModel.newGame()
+ gameDisplay.showToPlay()
+ initComputerColors()
+}
+
+function newGame()
+{
+ if (! gameModel.isGameEmpty && ! gameModel.isGameOver) {
+ showQuestion(qsTr("New game?"), newGameNoVerify)
+ return
+ }
+ newGameNoVerify()
+}
+
+function openFileUrl() {
+ gameDisplay.destroyPieces()
+ if (! gameModel.open(getFileFromUrl(openDialog.item.fileUrl)))
+ showError(qsTr("Open failed.") + "\n" + gameModel.lastInputOutputError)
+ else {
+ computerPlays0 = false
+ computerPlays1 = false
+ computerPlays2 = false
+ computerPlays3 = false
+ }
+ gameDisplay.createPieces()
+ gameDisplay.showToPlay()
+}
+
+function play(pieceModel, gameCoord) {
+ var wasComputerToPlay = isComputerToPlay()
+ gameModel.playPiece(pieceModel, gameCoord)
+ // We don't continue automatic play if the human played a move for a color
+ // played by the computer.
+ if (! wasComputerToPlay)
+ delayedCheckComputerMove.restart()
+}
+
+function saveFileUrl(fileUrl) {
+ if (! gameModel.save(getFileFromUrl(fileUrl)))
+ showError(qsTr("Save failed.") + "\n" + gameModel.lastInputOutputError)
+}
+
+function showComputerColorDialog() {
+ if (computerColorDialogLoader.status === Loader.Null)
+ computerColorDialogLoader.sourceComponent =
+ computerColorDialogComponent
+ var dialog = computerColorDialogLoader.item
+ dialog.computerPlays0 = computerPlays0
+ dialog.computerPlays1 = computerPlays1
+ dialog.computerPlays2 = computerPlays2
+ dialog.computerPlays3 = computerPlays3
+ dialog.open()
+}
+
+function showError(text) {
+ if (errorMessageLoader.status === Loader.Null)
+ errorMessageLoader.sourceComponent = errorMessageComponent
+ var dialog = errorMessageLoader.item
+ dialog.text = text
+ dialog.open()
+}
+
+function showInfo(text) {
+ if (infoMessageLoader.status === Loader.Null)
+ infoMessageLoader.sourceComponent = infoMessageComponent
+ var dialog = infoMessageLoader.item
+ dialog.text = text
+ dialog.open()
+}
+
+function showQuestion(text, acceptedFunc) {
+ if (questionMessageLoader.status === Loader.Null)
+ questionMessageLoader.sourceComponent = questionMessageComponent
+ var dialog = questionMessageLoader.item
+ dialog.text = text
+ dialog.accepted.connect(acceptedFunc)
+ dialog.open()
+}
+
+function truncate() {
+ showQuestion(qsTr("Truncate this subtree?"), gameModel.truncate)
+}
+
+function truncateChildren() {
+ showQuestion(qsTr("Truncate children?"), gameModel.truncateChildren)
+}
+
+function undo() {
+ gameModel.undo()
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import QtQuick.Dialogs 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.1
+import Qt.labs.settings 1.0
+import pentobi 1.0
+import "." as Pentobi
+import "Main.js" as Logic
+
+ApplicationWindow {
+ id: root
+
+ property bool computerPlays0
+ property bool computerPlays1
+ property bool computerPlays2
+ property bool computerPlays3
+ property bool isMoveHintRunning
+ property bool isAndroid: Qt.platform.os === "android"
+ property string themeName: isAndroid ? "dark" : "light"
+ property QtObject theme: Logic.createTheme(themeName)
+ property url folder
+ property int defaultWidth:
+ isAndroid ? Screen.desktopAvailableWidth :
+ Math.min(Screen.desktopAvailableWidth,
+ Math.round(Screen.pixelDensity / 3.5 * 600))
+ property int defaultHeight:
+ isAndroid ? Screen.desktopAvailableWidth :
+ Math.min(Math.round(Screen.pixelDensity / 3.5 * 800))
+
+ function cancelGenMove() {
+ playerModel.cancelGenMove()
+ delayedCheckComputerMove.stop()
+ }
+
+ minimumWidth: 240; minimumHeight: 320
+ width: isAndroid ? Screen.desktopAvailableWidth : defaultWidth
+ height: isAndroid ? Screen.desktopAvailableHeight : defaultHeight
+ color: theme.backgroundColor
+ title: qsTr("Pentobi")
+ onClosing: Qt.quit()
+ // Currently, we don't use the QtQuick ToolBar/MenuBar on Android. The file
+ // dialog is unusable with dark themes (QTBUG-48324) and a white toolbar is
+ // too distracting with the dark background we use on Android.
+ menuBar: menuBarLoader.item
+ toolBar: toolBarLoader.item
+ Component.onCompleted: {
+ Logic.init()
+ show()
+ }
+ Component.onDestruction: gameModel.autoSave()
+
+ ColumnLayout {
+ anchors.fill: parent
+ Keys.onReleased: if (isAndroid && event.key === Qt.Key_Menu) {
+ androidToolBarLoader.item.popupMenu()
+ event.accepted = true
+ }
+
+ Loader {
+ id: androidToolBarLoader
+
+ sourceComponent: isAndroid ? androidToolBarComponent : undefined
+ Layout.fillWidth: true
+
+ Component {
+ id: androidToolBarComponent
+
+ AndroidToolBar { }
+ }
+ }
+ GameDisplay {
+ id: gameDisplay
+
+ busyIndicatorRunning: pieces0 === undefined
+ || lengthyCommand.isRunning
+ || playerModel.isGenMoveRunning
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ focus: true
+ onPlay: Logic.play(pieceModel, gameCoord)
+ }
+ }
+ Loader {
+ id: menuBarLoader
+
+ sourceComponent: isAndroid ? undefined : menuBarComponent
+
+ Component {
+ id: menuBarComponent
+
+ MenuBar {
+ MenuGame { }
+ MenuGo { }
+ MenuEdit { }
+ MenuComputer { }
+ MenuView { }
+ MenuHelp { }
+ }
+ }
+ }
+ Loader {
+ id: toolBarLoader
+
+ sourceComponent: isAndroid ? undefined : toolBarComponent
+
+ Component {
+ id: toolBarComponent
+
+ Pentobi.ToolBar { }
+ }
+ }
+ Settings {
+ id: settings
+
+ property alias x: root.x
+ property alias y: root.y
+ property alias width: root.width
+ property alias height: root.height
+ property alias folder: root.folder
+ property alias enableAnimations: gameDisplay.enableAnimations
+ property alias markLastMove: gameDisplay.markLastMove
+ property alias computerPlays0: root.computerPlays0
+ property alias computerPlays1: root.computerPlays1
+ property alias computerPlays2: root.computerPlays2
+ property alias computerPlays3: root.computerPlays3
+ }
+ GameModel {
+ id: gameModel
+
+ onPositionAboutToChange: cancelGenMove()
+ }
+ PlayerModel {
+ id: playerModel
+
+ onMoveGenerated: Logic.moveGenerated(move)
+ }
+ Loader { id: computerColorDialogLoader }
+ Component {
+ id: computerColorDialogComponent
+
+ ComputerColorDialog {
+ id: computerColorDialog
+
+ gameVariant: gameModel.gameVariant
+ onAccepted: {
+ root.computerPlays0 = computerColorDialog.computerPlays0
+ root.computerPlays1 = computerColorDialog.computerPlays1
+ root.computerPlays2 = computerColorDialog.computerPlays2
+ root.computerPlays3 = computerColorDialog.computerPlays3
+ if (! Logic.isComputerToPlay())
+ cancelGenMove()
+ else if (! gameModel.isGameOver)
+ Logic.checkComputerMove()
+ gameDisplay.forceActiveFocus() // QTBUG-48456
+ }
+ onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456
+ }
+ }
+ Loader {
+ id: openDialog
+
+ function open() {
+ if (status === Loader.Null)
+ setSource("OpenDialog.qml")
+ item.open()
+ }
+ }
+ Loader {
+ id: saveDialog
+
+ function open() {
+ if (status === Loader.Null)
+ source = "SaveDialog.qml"
+ item.open()
+ }
+ }
+ Loader { id: errorMessageLoader }
+ Component {
+ id: errorMessageComponent
+
+ MessageDialog {
+ icon: StandardIcon.Critical
+ }
+ }
+ Loader { id: infoMessageLoader }
+ Component {
+ id: infoMessageComponent
+
+ MessageDialog { }
+ }
+ Loader { id: questionMessageLoader }
+ Component {
+ id: questionMessageComponent
+
+ MessageDialog {
+ standardButtons: StandardButton.Ok | StandardButton.Cancel
+ }
+ }
+ // Used to delay calls to Logic.checkComputerMove such that the computer
+ // starts thinking and the busy indicator is visible after the current move
+ // placement animation has finished
+ Timer {
+ id: delayedCheckComputerMove
+
+ interval: 400
+ onTriggered: Logic.checkComputerMove()
+ }
+ // Delay lengthy function calls such that the busy indicator is visible
+ Timer {
+ id: lengthyCommand
+
+ property bool isRunning
+ property var func
+
+ function run(func) {
+ lengthyCommand.func = func
+ isRunning = true
+ restart()
+ }
+
+ interval: 400
+ onTriggered: {
+ func()
+ isRunning = false
+ }
+ }
+ Connections {
+ target: Qt.application
+ onStateChanged:
+ if (Qt.application.state === Qt.ApplicationSuspended)
+ gameModel.autoSave()
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import "Main.js" as Logic
+
+Menu {
+ title: qsTr("&Computer")
+
+ MenuItem {
+ text: qsTr("Computer &Colors")
+ visible: ! isAndroid
+ onTriggered: Logic.showComputerColorDialog()
+ }
+ MenuItem {
+ text: qsTr("&Play")
+ enabled: ! gameModel.isGameOver
+ visible: ! isAndroid
+ onTriggered: Logic.computerPlay()
+ }
+ Menu {
+ title:
+ switch (gameModel.gameVariant)
+ {
+ case "classic": return qsTr("&Level (Classic, 4 Players)")
+ case "classic_2": return qsTr("&Level (Classic, 2 Players)")
+ case "classic_3": return qsTr("&Level (Classic, 3 Players)")
+ case "duo": return qsTr("&Level (Duo)")
+ case "junior": return qsTr("&Level (Junior)")
+ case "trigon": return qsTr("&Level (Trigon, 4 Players)")
+ case "trigon_2": return qsTr("&Level (Trigon, 2 Players)")
+ case "trigon_3": return qsTr("&Level (Trigon, 3 Players)")
+ case "nexos": return qsTr("&Level (Nexos, 4 Players)")
+ case "nexos_2": return qsTr("&Level (Nexos, 2 Players)")
+ case "callisto": return qsTr("&Level (Callisto, 4 Players)")
+ case "callisto_2": return qsTr("&Level (Callisto, 2 Players)")
+ case "callisto_3": return qsTr("&Level (Callisto, 3 Players)")
+ }
+
+ ExclusiveGroup { id: levelGroup }
+ MenuItemLevel { level: 1 }
+ MenuItemLevel { level: 2 }
+ MenuItemLevel { level: 3 }
+ MenuItemLevel { level: 4 }
+ MenuItemLevel { level: 5 }
+ MenuItemLevel { level: 6 }
+ MenuItemLevel { level: 7 }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import "Main.js" as Logic
+
+Menu {
+ title: qsTr("&Edit")
+
+ MenuItem {
+ text: qsTr("Make &Main Variation")
+ enabled: ! gameModel.isMainVar
+ visible: ! isAndroid || enabled
+ onTriggered: gameModel.makeMainVar()
+ }
+ MenuItem {
+ text: qsTr("Move Variation &Up")
+ enabled: gameModel.hasPrevVar
+ visible: ! isAndroid || enabled
+ onTriggered: gameModel.moveUpVar()
+ }
+ MenuItem {
+ text: qsTr("Move Variation &Down")
+ enabled: gameModel.hasNextVar
+ visible: ! isAndroid || enabled
+ onTriggered: gameModel.moveDownVar()
+ }
+ MenuSeparator { }
+ MenuItem {
+ text: qsTr("&Delete All Variations")
+ enabled: gameModel.hasVariations
+ visible: ! isAndroid || enabled
+ onTriggered: Logic.deleteAllVar()
+ }
+ MenuItem {
+ text: qsTr("&Truncate")
+ enabled: gameModel.canGoBackward
+ visible: ! isAndroid || enabled
+ onTriggered: Logic.truncate()
+ }
+ MenuItem {
+ text: qsTr("Truncate &Children")
+ enabled: gameModel.canGoForward
+ visible: ! isAndroid || enabled
+ onTriggered: Logic.truncateChildren()
+ }
+ MenuSeparator { }
+ MenuItem {
+ text: qsTr("&Next Color")
+ onTriggered: gameModel.nextColor()
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import "Main.js" as Logic
+
+Menu {
+ title: qsTr("&Game")
+
+ MenuItem {
+ text: qsTr("&New")
+ enabled: ! gameModel.isGameEmpty
+ visible: ! isAndroid
+ onTriggered: Logic.newGame()
+ }
+ MenuSeparator {
+ visible: ! isAndroid
+ }
+ Menu {
+ title: qsTr("Game &Variant")
+
+ ExclusiveGroup { id: groupGameVariant }
+ Menu {
+ title: qsTr("&Classic")
+
+ MenuItemGameVariant {
+ gameVariant: "classic_2"
+ text: qsTr("Classic (&2 Players)")
+ }
+ MenuItemGameVariant {
+ gameVariant: "classic_3"
+ text: qsTr("Classic (&3 Players)")
+ }
+ MenuItemGameVariant {
+ gameVariant: "classic"
+ text: qsTr("Classic (&4 Players)")
+ }
+ }
+ MenuItemGameVariant {
+ gameVariant: "duo"
+ text: qsTr("&Duo")
+ }
+ MenuItemGameVariant {
+ gameVariant: "junior"
+ text: qsTr("&Junior")
+ }
+ Menu {
+ title: qsTr("&Trigon")
+
+ MenuItemGameVariant {
+ gameVariant: "trigon_2"
+ text: qsTr("Trigon (&2 Players)")
+ }
+ MenuItemGameVariant {
+ gameVariant: "trigon_3"
+ text: qsTr("Trigon (&3 Players)")
+ }
+ MenuItemGameVariant {
+ gameVariant: "trigon"
+ text: qsTr("Trigon (&4 Players)")
+ }
+ }
+ Menu {
+ title: qsTr("&Nexos")
+
+ MenuItemGameVariant {
+ gameVariant: "nexos_2"
+ text: qsTr("Nexos (&2 Players)")
+ }
+ MenuItemGameVariant {
+ gameVariant: "nexos"
+ text: qsTr("Nexos (&4 Players)")
+ }
+ }
+ Menu {
+ title: qsTr("C&allisto")
+
+ MenuItemGameVariant {
+ gameVariant: "callisto_2"
+ text: qsTr("Callisto (&2 Players)")
+ }
+ MenuItemGameVariant {
+ gameVariant: "callisto_3"
+ text: qsTr("Callisto (&3 Players)")
+ }
+ MenuItemGameVariant {
+ gameVariant: "callisto"
+ text: qsTr("Callisto (&4 Players)")
+ }
+ }
+ }
+ MenuSeparator { }
+ MenuItem {
+ text: qsTr("&Undo Move")
+ enabled: gameModel.canUndo
+ visible: ! isAndroid
+ onTriggered: Logic.undo()
+ }
+ MenuItem {
+ text: qsTr("&Find Move")
+ enabled: ! gameModel.isGameOver
+ visible: ! isAndroid || enabled
+ onTriggered: Logic.moveHint()
+ }
+ MenuSeparator { }
+ MenuItem {
+ text: qsTr("&Open...")
+ onTriggered: openDialog.open()
+ }
+ MenuItem {
+ text: qsTr("&Save As...")
+ enabled: ! gameModel.isGameEmpty
+ visible: ! isAndroid || enabled
+ onTriggered: saveDialog.open()
+ }
+ MenuSeparator { }
+ MenuItem {
+ text: qsTr("&Quit")
+ onTriggered: Qt.quit()
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import "Main.js" as Logic
+
+Menu {
+ title: qsTr("G&o")
+ visible: ! isAndroid || backToMainVar.enabled
+
+ MenuItem {
+ id: backToMainVar
+
+ text: qsTr("Back to &Main Variation")
+ enabled: ! gameModel.isMainVar
+ visible: ! isAndroid || enabled
+ onTriggered: gameModel.backToMainVar()
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import "Main.js" as Logic
+
+Menu {
+ title: qsTr("&Help")
+
+ MenuItem {
+ text: qsTr("&About Pentobi")
+ onTriggered: Logic.about()
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import "Main.js" as Logic
+
+MenuItem {
+ property string gameVariant
+
+ checkable: true
+ checked: gameModel.gameVariant == gameVariant
+ exclusiveGroup: groupGameVariant
+ onTriggered: Logic.changeGameVariant(gameVariant)
+}
--- /dev/null
+import QtQuick.Controls 1.1
+
+MenuItem {
+ property int level
+
+ text: "&" + level
+ checkable: true
+ exclusiveGroup: levelGroup
+ checked: {
+ switch (gameModel.gameVariant) {
+ case "classic_2": return playerModel.levelClassic2 === level
+ case "classic_3": return playerModel.levelClassic3 === level
+ case "duo": return playerModel.levelDuo === level
+ case "trigon": return playerModel.levelTrigon === level
+ case "trigon_2": return playerModel.levelTrigon2 === level
+ case "trigon_3": return playerModel.levelTrigon3 === level
+ case "junior": return playerModel.levelJunior === level
+ case "nexos": return playerModel.levelNexos === level
+ case "nexos_2": return playerModel.levelNexos2 === level
+ case "callisto": return playerModel.levelCallisto === level
+ case "callisto_2": return playerModel.levelCallisto2 === level
+ case "callisto_3": return playerModel.levelCallisto3 === level
+ default: return playerModel.levelClassic === level
+ }
+ }
+ onTriggered: {
+ switch (gameModel.gameVariant) {
+ case "classic_2": playerModel.levelClassic2 = level; break
+ case "classic_3": playerModel.levelClassic3 = level; break
+ case "duo": playerModel.levelDuo = level; break
+ case "trigon": playerModel.levelTrigon = level; break
+ case "trigon_2": playerModel.levelTrigon2 = level; break
+ case "trigon_3": playerModel.levelTrigon3 = level; break
+ case "junior": playerModel.levelJunior = level; break
+ case "nexos": playerModel.levelNexos = level; break
+ case "nexos_2": playerModel.levelNexos2 = level; break
+ case "callisto": playerModel.levelCallisto = level; break
+ case "callisto_2": playerModel.levelCallisto2 = level; break
+ case "callisto_3": playerModel.levelCallisto3 = level; break
+ default: playerModel.levelClassic = level
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+
+Menu {
+ title: qsTr("&View")
+
+ MenuItem {
+ text: qsTr("Mark &Last Move")
+ checkable: true
+ checked: gameDisplay.markLastMove
+ onTriggered: gameDisplay.markLastMove = checked
+ }
+ MenuItem {
+ text: qsTr("&Animate Pieces")
+ checkable: true
+ checked: gameDisplay.enableAnimations
+ onTriggered: gameDisplay.enableAnimations = checked
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.4
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+
+ColumnLayout {
+ id: root
+
+ Text {
+ text: gameModel.positionInfo
+ color: theme.fontColorPosInfo
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ }
+ RowLayout
+ {
+ width: root.width; height: width / 6
+
+ Pentobi.Button {
+ enabled: gameModel.canGoBackward
+ imageSource: "icons/pentobi-beginning.svg"
+ Layout.fillWidth: true
+ onClicked: gameModel.goBeginning()
+ }
+ Pentobi.Button {
+ enabled: gameModel.canGoBackward
+ imageSource: "icons/pentobi-backward.svg"
+ Layout.fillWidth: true
+ onClicked: gameModel.goBackward()
+ autoRepeat: true
+ }
+ Pentobi.Button {
+ enabled: gameModel.canGoForward
+ imageSource: "icons/pentobi-forward.svg"
+ Layout.fillWidth: true
+ onClicked: gameModel.goForward()
+ autoRepeat: true
+ }
+ Pentobi.Button {
+ enabled: gameModel.canGoForward
+ imageSource: "icons/pentobi-end.svg"
+ Layout.fillWidth: true
+ onClicked: gameModel.goEnd()
+ }
+ Pentobi.Button {
+ enabled: gameModel.hasPrevVar
+ imageSource: "icons/pentobi-previous-variation.svg"
+ Layout.fillWidth: true
+ onClicked: gameModel.goPrevVar()
+ }
+ Pentobi.Button {
+ enabled: gameModel.hasNextVar
+ imageSource: "icons/pentobi-next-variation.svg"
+ Layout.fillWidth: true
+ onClicked: gameModel.goNextVar()
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Dialogs 1.2
+import "Main.js" as Logic
+
+FileDialog {
+ title: qsTr("Open")
+ nameFilters: [ qsTr("Blokus games (*.blksgf)"), qsTr("All files (*)") ]
+ folder: root.folder == "" ? shortcuts.desktop : root.folder
+ onAccepted: {
+ root.folder = folder
+ gameDisplay.forceActiveFocus() // QTBUG-48456
+ lengthyCommand.run(Logic.openFileUrl)
+ }
+ onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456
+}
--- /dev/null
+import QtQuick 2.3
+
+// See PieceClassic.qml for comments
+Item
+{
+ id: root
+
+ property var pieceModel
+ property string colorName
+ property bool isPicked
+ property Item parentUnplayed
+ property real gridWidth: board.gridWidth
+ property real gridHeight: board.gridHeight
+ property bool isMarked
+ property string imageName: pieceModel.elements.length === 1 ?
+ theme.getImage("frame-" + colorName) :
+ theme.getImage("square-" + colorName)
+ property real pieceAngle: {
+ var flX = Math.abs(flipX.angle % 360 - 180) < 90
+ var flY = Math.abs(flipY.angle % 360 - 180) < 90
+ var angle = rotation
+ if (flX && flY) angle += 180
+ else if (flX) angle += 90
+ else if (flY) angle += 270
+ return angle
+ }
+ property real imageOpacity0: imageOpacity(pieceAngle, 0)
+ property real imageOpacity90: imageOpacity(pieceAngle, 90)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180)
+ property real imageOpacity270: imageOpacity(pieceAngle, 270)
+
+ z: 1
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ }
+ ]
+
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360
+ return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180))
+ }
+
+ Repeater {
+ model: pieceModel.elements
+
+ Item {
+ Square {
+ width: 0.9 * gridWidth
+ height: 0.9 * gridHeight
+ x: (modelData.x - pieceModel.center.x) * gridWidth
+ + (gridWidth - width) / 2
+ y: (modelData.y - pieceModel.center.y) * gridHeight
+ + (gridHeight - height) / 2
+ }
+ // Right junction
+ Image {
+ visible: pieceModel.junctionType[index] === 0
+ || pieceModel.junctionType[index] === 1
+ source: theme.getImage("junction-all-" + colorName)
+ width: 0.1 * gridWidth
+ height: 0.85 * gridHeight
+ x: (modelData.x - pieceModel.center.x + 1) * gridWidth
+ - width / 2
+ y: (modelData.y - pieceModel.center.y) * gridHeight
+ + (gridHeight - height) / 2
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ }
+ // Down junction
+ Image {
+ visible: pieceModel.junctionType[index] === 0
+ || pieceModel.junctionType[index] === 2
+ source: theme.getImage("junction-all-" + colorName)
+ width: 0.85 * gridWidth
+ height: 0.1 * gridHeight
+ x: (modelData.x - pieceModel.center.x) * gridWidth
+ + (gridWidth - width) / 2
+ y: (modelData.y - pieceModel.center.y + 1) * gridHeight
+ - height / 2
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ }
+ }
+ }
+ Rectangle {
+ opacity: isMarked ? 0.5 : 0
+ color: colorName == "blue" || colorName == "red"
+ || pieceModel.elements.length === 1 ? "white" : "#333333"
+ width: 0.3 * gridHeight
+ height: width
+ radius: width / 2
+ x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5)
+ * gridWidth - width / 2
+ y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5)
+ * gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: 80 } }
+ }
+ StateGroup {
+ state: pieceModel.state
+
+ states: [
+ State {
+ name: "rot90"
+ PropertyChanges { target: root; rotation: 90 }
+ },
+ State {
+ name: "rot180"
+ PropertyChanges { target: root; rotation: 180 }
+ },
+ State {
+ name: "rot270"
+ PropertyChanges { target: root; rotation: 270 }
+ },
+ State {
+ name: "flip"
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot90Flip"
+ PropertyChanges { target: root; rotation: 90 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot180Flip"
+ PropertyChanges { target: root; rotation: 180 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot270Flip"
+ PropertyChanges { target: root; rotation: 270 }
+ PropertyChanges { target: flipX; angle: 180 }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ from: ",rot90,rot180,rot270"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: ",flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot90,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot180,rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot270,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot90,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot270,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: isPicked
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board
+ x: board.mapFromGameX(pieceModel.gameCoord.x)
+ y: board.mapFromGameY(pieceModel.gameCoord.y)
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ // Avoid fractional sizes for square piece elements
+ scale: Math.floor(0.25 * parentUnplayed.width) / gridWidth
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ ParentAnimation {
+ via: gameDisplay
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: 300
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+Item
+{
+ id: root
+
+ property var pieceModel
+ property string colorName
+ property bool isPicked
+ property Item parentUnplayed
+ property real gridWidth: board.gridWidth
+ property real gridHeight: board.gridHeight
+ property bool isMarked
+ property string imageName: theme.getImage("square-" + colorName)
+ property real pieceAngle: {
+ var flX = Math.abs(flipX.angle % 360 - 180) < 90
+ var flY = Math.abs(flipY.angle % 360 - 180) < 90
+ var angle = rotation
+ if (flX && flY) angle += 180
+ else if (flX) angle += 90
+ else if (flY) angle += 270
+ return angle
+ }
+ property real imageOpacity0: imageOpacity(pieceAngle, 0)
+ property real imageOpacity90: imageOpacity(pieceAngle, 90)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180)
+ property real imageOpacity270: imageOpacity(pieceAngle, 270)
+
+ z: 1 // Must be above board and piece manipulator during transition
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ }
+ ]
+
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360 // JS modulo bug
+ return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180))
+ }
+
+ Repeater {
+ model: pieceModel.elements
+
+ Square {
+ width: gridWidth
+ height: gridHeight
+ x: (modelData.x - pieceModel.center.x) * gridWidth
+ y: (modelData.y - pieceModel.center.y) * gridHeight
+ }
+ }
+ Rectangle {
+ opacity: isMarked ? 0.5 : 0
+ color: colorName == "blue" || colorName == "red" ?
+ "white" : "#333333"
+ width: 0.3 * gridHeight
+ height: width
+ radius: width / 2
+ x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5)
+ * gridWidth - width / 2
+ y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5)
+ * gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: 80 } }
+ }
+ StateGroup {
+ state: pieceModel.state
+
+ states: [
+ State {
+ name: "rot90"
+ PropertyChanges { target: root; rotation: 90 }
+ },
+ State {
+ name: "rot180"
+ PropertyChanges { target: root; rotation: 180 }
+ },
+ State {
+ name: "rot270"
+ PropertyChanges { target: root; rotation: 270 }
+ },
+ State {
+ name: "flip"
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot90Flip"
+ PropertyChanges { target: root; rotation: 90 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot180Flip"
+ PropertyChanges { target: root; rotation: 180 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot270Flip"
+ PropertyChanges { target: root; rotation: 270 }
+ PropertyChanges { target: flipX; angle: 180 }
+ }
+ ]
+
+ // Unique states are defined by rotating and flipping around the x axis
+ // but for some transitions, the shortest visual animation is flipping
+ // around the y axis.
+ transitions: [
+ Transition {
+ from: ",rot90,rot180,rot270"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: ",flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot90,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot180,rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot270,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot90,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot270,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: isPicked
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board
+ x: board.mapFromGameX(pieceModel.gameCoord.x)
+ y: board.mapFromGameY(pieceModel.gameCoord.y)
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ // Avoid fractional sizes for square piece elements
+ scale : Math.floor(0.2 * parentUnplayed.width) / gridWidth
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ ParentAnimation {
+ via: gameDisplay
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: 300
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+RotationAnimation {
+ duration: 300
+ direction: RotationAnimation.Shortest
+ property: "angle"
+}
--- /dev/null
+import QtQuick 2.0
+
+Grid {
+ id: root
+
+ property var pieces
+
+ signal piecePicked(var piece)
+
+ opacity: theme.pieceListOpacity
+
+ Repeater {
+ model: pieces
+
+ MouseArea {
+ id: mouseArea
+
+ property var piece: modelData
+
+ width: root.width / columns; height: width
+ visible: ! piece.pieceModel.isPlayed
+ onClicked: piecePicked(piece)
+ Component.onCompleted: piece.parentUnplayed = mouseArea
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+Item {
+ id: root
+
+ property var pieceModel
+ // True if piece manipulator is at a board location that is a legal move
+ property bool legal
+
+ signal piecePlayed
+
+ Image {
+ anchors.fill: root
+ source: theme.getImage("piece-manipulator")
+ sourceSize { width: width; height: height }
+ opacity: ! legal ? 0.4 : 0
+ Behavior on opacity { NumberAnimation { duration: 100 } }
+ }
+ Image {
+ anchors.fill: root
+ source: theme.getImage("piece-manipulator-legal")
+ sourceSize { width: width; height: height }
+ opacity: legal ? 0.4 : 0
+ Behavior on opacity { NumberAnimation { duration: 100 } }
+ }
+ MouseArea {
+ id: dragArea
+
+ anchors.fill: root
+ drag {
+ target: root
+ filterChildren: true
+ minimumX: -width / 2; maximumX: root.parent.width - width / 2
+ minimumY: -height / 2; maximumY: root.parent.height - height / 2
+ }
+
+ MouseArea {
+ anchors.centerIn: dragArea
+ width: 0.5 * root.width; height: width
+ onClicked: piecePlayed()
+ }
+ MouseArea {
+ anchors {
+ top: dragArea.top
+ horizontalCenter: dragArea.horizontalCenter
+ }
+ width: 0.2 * root.width; height: width
+ onClicked: pieceModel.rotateRight()
+ }
+ MouseArea {
+ anchors {
+ right: dragArea.right
+ verticalCenter: dragArea.verticalCenter
+ }
+ width: 0.2 * root.width; height: width
+ onClicked: pieceModel.flipAcrossX()
+ }
+ MouseArea {
+ anchors {
+ bottom: dragArea.bottom
+ horizontalCenter: dragArea.horizontalCenter
+ }
+ width: 0.2 * root.width; height: width
+ onClicked: pieceModel.flipAcrossY()
+ }
+ MouseArea {
+ anchors { left: dragArea.left; verticalCenter: dragArea.verticalCenter }
+ width: 0.2 * root.width; height: width
+ onClicked: pieceModel.rotateLeft()
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.3
+
+// Piece for Nexos. See PieceClassic for comments.
+Item
+{
+ id: root
+
+ property var pieceModel
+ property string colorName
+ property bool isPicked
+ property Item parentUnplayed
+ property real gridWidth: board.gridWidth
+ property real gridHeight: board.gridHeight
+ property bool isMarked
+ property string imageName: theme.getImage("linesegment-" + colorName)
+ property real pieceAngle: {
+ var flX = Math.abs(flipX.angle % 360 - 180) < 90
+ var flY = Math.abs(flipY.angle % 360 - 180) < 90
+ var angle = rotation
+ if (flX && flY) angle += 180
+ else if (flX) angle += 90
+ else if (flY) angle += 270
+ return angle
+ }
+ property real imageOpacity0: imageOpacity(pieceAngle, 0)
+ property real imageOpacity90: imageOpacity(pieceAngle, 90)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180)
+ property real imageOpacity270: imageOpacity(pieceAngle, 270)
+
+ z: 1
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ }
+ ]
+
+ function isHorizontal(pos) { return (pos.x % 2 != 0) }
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360
+ return (angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180))
+ }
+
+ Repeater {
+ model: pieceModel.elements
+
+ LineSegment {
+ isHorizontal: root.isHorizontal(modelData)
+ width: 1.5 * gridWidth
+ height: 0.5 * gridHeight
+ x: (modelData.x - pieceModel.center.x - 0.25) * gridWidth
+ y: (modelData.y - pieceModel.center.y + 0.25) * gridHeight
+ }
+ }
+ Repeater {
+ model: pieceModel.junctions
+
+ Image {
+ source: {
+ switch (pieceModel.junctionType[index]) {
+ case 0:
+ return theme.getImage("junction-all-" + colorName)
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ return theme.getImage("junction-t-" + colorName)
+ case 5:
+ case 6:
+ return theme.getImage("junction-straight-" + colorName)
+ case 7:
+ case 8:
+ case 9:
+ case 10:
+ return theme.getImage("junction-rect-" + colorName)
+ }
+ }
+ rotation: {
+ switch (pieceModel.junctionType[index]) {
+ case 0:
+ case 3:
+ case 5:
+ case 10:
+ return 0
+ case 1:
+ case 9:
+ return 270
+ case 2:
+ case 6:
+ case 8:
+ return 90
+ case 4:
+ case 7:
+ return 180
+ }
+ }
+ width: 0.5 * gridWidth
+ height: 0.5 * gridHeight
+ x: (modelData.x - pieceModel.center.x + 0.25) * gridWidth
+ y: (modelData.y - pieceModel.center.y + 0.25) * gridHeight
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ }
+ }
+ Rectangle {
+ opacity: isMarked ? 0.5 : 0
+ color: colorName == "blue" || colorName == "red" ?
+ "white" : "#333333"
+ width: 0.3 * gridHeight
+ height: width
+ radius: width / 2
+ x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5)
+ * gridWidth - width / 2
+ y: (pieceModel.labelPos.y - pieceModel.center.y + 0.5)
+ * gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: 80 } }
+ }
+ StateGroup {
+ state: pieceModel.state
+
+ states: [
+ State {
+ name: "rot90"
+ PropertyChanges { target: root; rotation: 90 }
+ },
+ State {
+ name: "rot180"
+ PropertyChanges { target: root; rotation: 180 }
+ },
+ State {
+ name: "rot270"
+ PropertyChanges { target: root; rotation: 270 }
+ },
+ State {
+ name: "flip"
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot90Flip"
+ PropertyChanges { target: root; rotation: 90 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot180Flip"
+ PropertyChanges { target: root; rotation: 180 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot270Flip"
+ PropertyChanges { target: root; rotation: 270 }
+ PropertyChanges { target: flipX; angle: 180 }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ from: ",rot90,rot180,rot270"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: "flip,rot90Flip,rot180Flip,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: ",flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot90,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot180,rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot270,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot90,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot270,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: isPicked
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board
+ x: board.mapFromGameX(pieceModel.gameCoord.x)
+ y: board.mapFromGameY(pieceModel.gameCoord.y)
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ // Avoid fractional sizes for square piece elements
+ scale: Math.floor(0.12 * parentUnplayed.width) / gridWidth
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ ParentAnimation {
+ via: gameDisplay
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: 300
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+RotationAnimation {
+ duration: 300
+ direction: RotationAnimation.Shortest
+ property: "rotation"
+}
--- /dev/null
+import QtQuick 2.0
+
+Flickable {
+ id: root
+
+ property string gameVariant
+ property int toPlay
+ property var pieces0
+ property var pieces1
+ property var pieces2
+ property var pieces3
+ property int nuColors
+ property int columns
+ property int rows
+ property bool transitionsEnabled
+
+ signal piecePicked(var piece)
+
+ contentHeight: pieceList0.height + pieceList1.height
+ + pieceList2.height + pieceList3.height
+ flickableDirection: Flickable.VerticalFlick
+ clip: true
+
+ PieceList {
+ id: pieceList0
+
+ width: root.width
+ columns: root.columns
+ pieces: pieces0
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList1
+
+ width: root.width
+ columns: root.columns
+ pieces: pieces1
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList2
+
+ width: root.width
+ columns: root.columns
+ pieces: pieces2
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList3
+
+ width: root.width
+ columns: root.columns
+ pieces: pieces3
+ onPiecePicked: root.piecePicked(piece)
+ }
+
+ states: [
+ State {
+ name: "toPlay0"
+ when: toPlay === 0
+
+ PropertyChanges {
+ target: pieceList0
+ y: 0
+ }
+ PropertyChanges {
+ target: pieceList1
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList0.height :
+ pieceList0.height + pieceList2.height
+ }
+ PropertyChanges {
+ target: pieceList2
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList0.height + pieceList1.height :
+ pieceList0.height
+ }
+ PropertyChanges {
+ target: pieceList3
+ y: pieceList0.height + pieceList1.height + pieceList2.height
+ }
+ },
+ State {
+ name: "toPlay1"
+ when: toPlay === 1
+
+ PropertyChanges {
+ target: pieceList1
+ y: 0
+ }
+ PropertyChanges {
+ target: pieceList2
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList1.height :
+ pieceList1.height + pieceList3.height
+ }
+ PropertyChanges {
+ target: pieceList3
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList1.height + pieceList2.height :
+ pieceList1.height
+ }
+ PropertyChanges {
+ target: pieceList0
+ y: pieceList1.height + pieceList2.height + pieceList3.height
+ }
+ },
+ State {
+ name: "toPlay2"
+ when: toPlay === 2
+
+ PropertyChanges {
+ target: pieceList2
+ y: 0
+ }
+ PropertyChanges {
+ target: pieceList3
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList2.height :
+ pieceList2.height + pieceList0.height
+ }
+ PropertyChanges {
+ target: pieceList0
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList2.height + pieceList3.height :
+ pieceList2.height
+ }
+ PropertyChanges {
+ target: pieceList1
+ y: pieceList2.height + pieceList3.height + pieceList0.height
+ }
+ },
+ State {
+ name: "toPlay3"
+ when: toPlay === 3
+
+ PropertyChanges {
+ target: pieceList3
+ y: 0
+ }
+ PropertyChanges {
+ target: pieceList0
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList3.height :
+ pieceList3.height + pieceList1.height
+ }
+ PropertyChanges {
+ target: pieceList1
+ y: gameVariant != "classic_2" && gameVariant != "trigon_2"
+ && gameVariant != "nexos_2" ?
+ pieceList3.height + pieceList0.height :
+ pieceList3.height
+ }
+ PropertyChanges {
+ target: pieceList2
+ y: pieceList3.height + pieceList0.height + pieceList1.height
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ enabled: transitionsEnabled
+
+ SequentialAnimation {
+ PropertyAction {
+ target: pieceList0; property: "y"; value: pieceList0.y }
+ PropertyAction {
+ target: pieceList1; property: "y"; value: pieceList1.y }
+ PropertyAction {
+ target: pieceList2; property: "y"; value: pieceList2.y }
+ PropertyAction {
+ target: pieceList3; property: "y"; value: pieceList3.y }
+ // Delay showing new color because of piece placement animation
+ PauseAnimation { duration: 200 }
+ NumberAnimation {
+ target: root; property: "opacity"; to: 0; duration: 100
+ }
+ PropertyAction { target: pieceList0; property: "y" }
+ PropertyAction { target: pieceList1; property: "y" }
+ PropertyAction { target: pieceList2; property: "y" }
+ PropertyAction { target: pieceList3; property: "y" }
+ PropertyAction { target: root; property: "contentY"; value: 0 }
+ NumberAnimation {
+ target: root; property: "opacity"; to: 1; duration: 100
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+// See PieceClassic.qml for comments
+Item
+{
+ id: root
+
+ property var pieceModel
+ property string colorName
+ property bool isPicked
+ property Item parentUnplayed
+ property real gridWidth: board.gridWidth
+ property real gridHeight: board.gridHeight
+ property bool isMarked
+ property string imageName: theme.getImage("triangle-" + colorName)
+ property string imageNameDownward:
+ theme.getImage("triangle-down-" + colorName)
+ property real pieceAngle: {
+ var flX = Math.abs(flipX.angle % 360 - 180) < 90
+ var flY = Math.abs(flipY.angle % 360 - 180) < 90
+ var angle = rotation
+ if (flX && flY) angle += 180
+ else if (flX) angle += 120
+ else if (flY) angle += 300
+ return angle
+ }
+ property real imageOpacity0: imageOpacity(pieceAngle, 0)
+ property real imageOpacity60: imageOpacity(pieceAngle, 60)
+ property real imageOpacity120: imageOpacity(pieceAngle, 120)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180)
+ property real imageOpacity240: imageOpacity(pieceAngle, 240)
+ property real imageOpacity300: imageOpacity(pieceAngle, 300)
+
+ z: 1
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ origin { x: width / 2; y: height / 2 }
+ }
+ ]
+
+ function _isDownward(pos) { return (pos.x % 2 == 0) != (pos.y % 2 == 0) }
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (((pieceAngle - imgAngle) % 360) + 360) % 360
+ return (angle >= 60 && angle <= 300 ? 0 : 2 * Math.cos(angle * Math.PI / 180) - 1)
+ }
+
+ Repeater {
+ model: pieceModel.elements
+
+ Triangle {
+ isDownward: _isDownward(modelData)
+ width: 2 * gridWidth
+ height: gridHeight
+ x: (modelData.x - pieceModel.center.x - 0.5) * gridWidth
+ y: (modelData.y - pieceModel.center.y) * gridHeight
+ }
+ }
+ Rectangle {
+ opacity: isMarked ? 0.5 : 0
+ color: colorName == "blue" || colorName == "red" ?
+ "white" : "#333333"
+ width: 0.3 * gridHeight
+ height: width
+ radius: width / 2
+ x: (pieceModel.labelPos.x - pieceModel.center.x + 0.5)
+ * gridWidth - width / 2
+ y: (pieceModel.labelPos.y - pieceModel.center.y
+ + (_isDownward(pieceModel.labelPos) ? 1 : 2) / 3)
+ * gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: 80 } }
+ }
+ StateGroup {
+ state: pieceModel.state
+
+ states: [
+ State {
+ name: "rot60"
+ PropertyChanges { target: root; rotation: 60 }
+ },
+ State {
+ name: "rot120"
+ PropertyChanges { target: root; rotation: 120 }
+ },
+ State {
+ name: "rot180"
+ PropertyChanges { target: root; rotation: 180 }
+ },
+ State {
+ name: "rot240"
+ PropertyChanges { target: root; rotation: 240 }
+ },
+ State {
+ name: "rot300"
+ PropertyChanges { target: root; rotation: 300 }
+ },
+ State {
+ name: "flip"
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot60Flip"
+ PropertyChanges { target: root; rotation: 60 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot120Flip"
+ PropertyChanges { target: root; rotation: 120 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot180Flip"
+ PropertyChanges { target: root; rotation: 180 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot240Flip"
+ PropertyChanges { target: root; rotation: 240 }
+ PropertyChanges { target: flipX; angle: 180 }
+ },
+ State {
+ name: "rot300Flip"
+ PropertyChanges { target: root; rotation: 300 }
+ PropertyChanges { target: flipX; angle: 180 }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ from: ",rot60,rot120,rot180,rot240,rot300"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: "flip,rot60Flip,rot120Flip,rot180Flip,rot240Flip,rot300Flip"; to: from
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ },
+ Transition {
+ from: ",flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot60,rot60Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot120,rot120Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot180,rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot240,rot240Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: "rot300,rot300Flip"; to: from
+ enabled: enableAnimations
+
+ PieceFlipAnimation { target: flipX }
+ },
+ Transition {
+ from: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot60,rot240Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot120,rot300Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot240,rot60Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ },
+ Transition {
+ from: "rot300,rot120Flip"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction { property: "rotation"; value: rotation }
+ PropertyAction {
+ target: flipX; property: "angle"; value: flipX.angle
+ }
+ PieceFlipAnimation { target: flipY; to: 180 }
+ PropertyAction { target: flipY; property: "angle"; value: 0 }
+ }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: isPicked
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board
+ x: board.mapFromGameX(pieceModel.gameCoord.x)
+ y: board.mapFromGameY(pieceModel.gameCoord.y)
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ scale: 0.13 * parentUnplayed.width / gridWidth
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ ParentAnimation {
+ via: gameDisplay
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: 300
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Dialogs 1.2
+import "Main.js" as Logic
+
+FileDialog {
+ title: qsTr("Save")
+ selectExisting: false
+ folder: root.folder == "" ? shortcuts.desktop : root.folder
+ nameFilters: [ qsTr("Blokus games (*.blksgf)"), qsTr("All files (*)") ]
+ onAccepted: {
+ Logic.saveFileUrl(fileUrl)
+ root.folder = folder
+ gameDisplay.forceActiveFocus() // QTBUG-48456
+ }
+ onRejected: gameDisplay.forceActiveFocus() // QTBUG-48456
+}
--- /dev/null
+import QtQuick 2.0
+
+Row {
+ id: root
+
+ property real pointSize
+ property int toPlay
+ property int altPlayer
+ property string gameVariant
+ property real points0
+ property real points1
+ property real points2
+ property real points3
+ property real bonus0
+ property real bonus1
+ property real bonus2
+ property real bonus3
+ property bool hasMoves0
+ property bool hasMoves1
+ property bool hasMoves2
+ property bool hasMoves3
+
+ ScoreElement2 {
+ visible: gameVariant == "classic_2" || gameVariant == "trigon_2"
+ || gameVariant == "nexos_2"
+ value: points0 + points2
+ isFinal: ! hasMoves0 && ! hasMoves2
+ pointSize: root.pointSize
+ height: root.height
+ width: 5.9 * pointSize
+ color1: theme.colorBlue
+ color2: theme.colorRed
+ }
+ ScoreElement2 {
+ visible: gameVariant == "classic_2" || gameVariant == "trigon_2"
+ || gameVariant == "nexos_2"
+ value: points1 + points3
+ isFinal: ! hasMoves1 && ! hasMoves3
+ pointSize: root.pointSize
+ height: root.height
+ width: 5.9 * pointSize
+ color1: theme.colorYellow
+ color2: theme.colorGreen
+ }
+ ScoreElement {
+ value: points0
+ bonus: bonus0
+ isFinal: ! hasMoves0
+ isToPlay: toPlay == 0
+ pointSize: root.pointSize
+ height: root.height
+ width: 5 * pointSize
+ color: theme.colorBlue
+ }
+ ScoreElement {
+ value: points1
+ bonus: bonus1
+ isFinal: ! hasMoves1
+ isToPlay: toPlay == 1
+ pointSize: root.pointSize
+ height: root.height
+ width: 5 * pointSize
+ color: gameModel.gameVariant == "duo"
+ || gameModel.gameVariant == "junior"
+ || gameModel.gameVariant == "callisto_2" ?
+ theme.colorGreen : theme.colorYellow
+ }
+ ScoreElement {
+ visible: gameVariant != "duo" && gameVariant != "junior"
+ && gameVariant != "callisto_2"
+ value: points2
+ bonus: bonus2
+ isFinal: ! hasMoves2
+ isToPlay: toPlay == 2
+ pointSize: root.pointSize
+ height: root.height
+ width: 5 * pointSize
+ color: theme.colorRed
+ }
+ ScoreElement {
+ visible: gameVariant != "duo" && gameVariant != "junior"
+ && gameVariant != "callisto_2" && gameVariant != "trigon_3"
+ && gameVariant != "classic_3" && gameVariant != "callisto_3"
+ value: points3
+ bonus: bonus3
+ isFinal: ! hasMoves3
+ isToPlay: toPlay == 3
+ pointSize: root.pointSize
+ height: root.height
+ width: 5 * pointSize
+ color: theme.colorGreen
+ }
+ ScoreElement2 {
+ visible: gameVariant == "classic_3"
+ value: points3
+ isAltColor: true
+ isToPlay: toPlay == 3
+ isFinal: ! hasMoves3
+ pointSize: root.pointSize
+ height: root.height
+ width: 5.9 * pointSize
+ color1: theme.colorGreen
+ color2:
+ switch (altPlayer) {
+ case 0: return theme.colorBlue
+ case 1: return theme.colorYellow
+ case 2: return theme.colorRed
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+Item {
+ id: root
+
+ property alias color: point.color
+ property bool isFinal
+ property bool isToPlay
+ property real value
+ property real bonus
+ property real pointSize
+
+ Rectangle {
+ id: point
+
+ width: (isToPlay ? 1.3 : 1) * pointSize
+ border {
+ color: Qt.lighter(color, theme.toPlayColorLighter)
+ width: isToPlay ? Math.max(0.15 * pointSize, 1) : 0
+ }
+ height: width
+ radius: width / 2
+ anchors.verticalCenter: root.verticalCenter
+ }
+ Text {
+ id: scoreText
+
+ text: ! isFinal ?
+ value : (bonus > 0 ? "*" : "") + "<u>" + value + "</u>"
+ color: theme.fontColorScore
+ anchors {
+ left: point.right
+ leftMargin: (isToPlay ? 0.2 : 0.4) * point.width
+ verticalCenter: root.verticalCenter
+ }
+ verticalAlignment: Text.AlignVCenter
+ renderType: Text.NativeRendering
+ font.pixelSize: 1.4 * pointSize
+ }
+}
--- /dev/null
+import QtQuick 2.0
+
+Item {
+ id: root
+
+ property color color1
+ property color color2
+ property bool isFinal
+ property bool isToPlay
+ property bool isAltColor
+ property real value
+ property real pointSize
+
+ Rectangle {
+ id: point1
+
+ color: color1
+ opacity: isAltColor && isFinal ? 0 : 1
+ width: (isToPlay ? 1.3 : 1) * pointSize
+ border {
+ color: Qt.lighter(color1, theme.toPlayColorLighter)
+ width: isToPlay ? Math.max(0.15 * pointSize, 1) : 0
+ }
+ height: width
+ radius: width / 2
+ anchors.verticalCenter: root.verticalCenter
+ }
+ Rectangle {
+ id: point2
+
+ color: isAltColor && isFinal ? color1 : color2
+ width: pointSize
+ height: width
+ radius: width / 2
+ anchors {
+ left: point1.right
+ verticalCenter: root.verticalCenter
+ }
+ }
+ Text {
+ text: {
+ if (isAltColor)
+ return isFinal ? "(<u>" + value + "</u>)" : "(" + value + ")"
+ else
+ return isFinal ? "<u>" + value + "</u>" : value
+ }
+ color: theme.fontColorScore
+ width: root.width - point1.width - point2.width - anchors.leftMargin
+ anchors {
+ left: point2.right
+ leftMargin: (isToPlay ? 0.2 : 0.4) * point1.width
+ verticalCenter: root.verticalCenter
+ }
+ verticalAlignment: Text.AlignVCenter
+ renderType: Text.NativeRendering
+ font.pixelSize: 1.4 * pointSize
+ }
+}
--- /dev/null
+import QtQuick 2.3
+
+// Piece element (square) with pseudo-3D effect.
+// Simulates lighting by using different images for the lighting at different
+// rotations and interpolating between them with an opacity animation.
+Item {
+ id: root
+
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component0
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity0
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component0
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component90
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity90
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component90
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ rotation: -90
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component180
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity180
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component180
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ rotation: -180
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component270
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity270
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component270
+
+ Image {
+ source: imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ rotation: -270
+ }
+ }
+ }
+}
--- /dev/null
+import QtQuick 2.0
+import QtQuick.Controls 1.1
+import QtQuick.Layouts 1.1
+import "Main.js" as Logic
+
+ToolBar {
+ RowLayout {
+ anchors.fill: parent
+
+ ToolButton {
+ iconSource: "icons/pentobi-newgame.svg"
+ enabled: ! gameModel.isGameEmpty
+ onClicked: Logic.newGame()
+ }
+ ToolButton {
+ iconSource: "icons/pentobi-undo.svg"
+ enabled: gameModel.canUndo
+ onClicked: Logic.undo()
+ }
+ ToolButton {
+ iconSource: "icons/pentobi-computer-colors.svg"
+ onClicked: Logic.showComputerColorDialog()
+ }
+ ToolButton {
+ iconSource: "icons/pentobi-play.svg"
+ onClicked: Logic.computerPlay()
+ }
+ Item { Layout.fillWidth: true }
+ }
+}
--- /dev/null
+import QtQuick 2.3
+
+// See Square.qml for comments
+Item {
+ id: root
+
+ property bool isDownward
+
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component0
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity0
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component0
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component60
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity60
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component60
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ transform: [
+ Rotation {
+ angle: -60
+ origin {
+ x: width / 2
+ y: isDownward ? 2 * height / 3 : height / 3
+ }
+ },
+ Translate { y: isDownward ? -height / 3 : height / 3 }
+ ]
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component120
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity120
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component120
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ transform: Rotation {
+ angle: -120
+ origin {
+ x: width / 2
+ y: isDownward ? height / 3 : 2 * height / 3
+ }
+ }
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component180
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity180
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component180
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ rotation: -180
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component240
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity240
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component240
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ transform: Rotation {
+ angle: -240
+ origin {
+ x: width / 2
+ y: isDownward ? height / 3 : 2 * height / 3
+ }
+ }
+ }
+ }
+ }
+ Loader {
+ function loadImage() {
+ if (opacity > 0 && status === Loader.Null)
+ sourceComponent = component300
+ }
+
+ anchors.fill: root
+ opacity: imageOpacity300
+ onOpacityChanged: loadImage()
+ Component.onCompleted: loadImage()
+
+ Component {
+ id: component300
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ sourceSize: imageSourceSize
+ mipmap: true
+ antialiasing: true
+ transform: [
+ Rotation {
+ angle: -300
+ origin {
+ x: width / 2
+ y: isDownward ? 2 * height / 3 : height / 3
+ }
+ },
+ Translate { y: isDownward ? -height / 3 : height / 3 }
+ ]
+ }
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+ <name>ComputerColorDialog</name>
+ <message>
+ <source>Computer Colors</source>
+ <translation>Computer-Farben</translation>
+ </message>
+ <message>
+ <source>Computer plays:</source>
+ <translation>Computer spielt:</translation>
+ </message>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Blau/Rot</translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation>Blau</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Gelb/Grün</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Grün</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Gelb</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rot</translation>
+ </message>
+</context>
+<context>
+ <name>GameModel</name>
+ <message>
+ <source>(Setup)</source>
+ <translation>(Stellung)</translation>
+ </message>
+ <message>
+ <source>(No moves)</source>
+ <translation>(Keine Züge)</translation>
+ </message>
+ <message>
+ <source>Move %1</source>
+ <translation>Zug %1</translation>
+ </message>
+ <message>
+ <source>Blue wins with 1 point.</source>
+ <translation>Blau gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Blue wins with %1 points.</source>
+ <translation>Blau gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Green wins with 1 point.</source>
+ <translation>Grün gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Green wins with %1 points.</source>
+ <translation>Grün gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Green wins (tie resolved).</source>
+ <translation>Grün gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie.</source>
+ <translation>Spiel endet unentschieden.</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with 1 point.</source>
+ <translation>Blau/Rot gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with %1 points.</source>
+ <translation>Blau/Rot gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with 1 point.</source>
+ <translation>Gelb/Grün gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with %1 points.</source>
+ <translation>Gelb/Grün gewinnt mit %1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins (tie resolved).</source>
+ <translation>Gelb/Grün gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>Blue wins.</source>
+ <translation>Blau gewinnt.</translation>
+ </message>
+ <message>
+ <source>Yellow wins.</source>
+ <translation>Gelb gewinnt.</translation>
+ </message>
+ <message>
+ <source>Red wins.</source>
+ <translation>Rot gewinnt.</translation>
+ </message>
+ <message>
+ <source>Red wins (tie resolved).</source>
+ <translation>Rot gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>Yellow wins (tie resolved).</source>
+ <translation>Gelb gewinnt (Unentschieden aufgelöst).</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Yellow.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen Blau und Gelb.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Red.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen Blau und Rot.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow and Red.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen Gelb und Rot.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between all players.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen allen Spielern.</translation>
+ </message>
+ <message>
+ <source>Green wins.</source>
+ <translation>Grün gewinnt.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Red.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen Blau, Gelb und Rot.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Green.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen Blau, Gelb und Grün.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Red and Green.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen Blau, Rot und Grün.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow, Red and Green.</source>
+ <translation>Spiel endet in einem Unentschieden zwischen Gelb, Rot und Grün.</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <source>Pentobi</source>
+ <translation>Pentobi</translation>
+ </message>
+ <message>
+ <source>New game?</source>
+ <translation>Neues Spiel?</translation>
+ </message>
+ <message>
+ <source>Open failed.</source>
+ <translation>Öffnen fehlgeschlagen.</translation>
+ </message>
+ <message>
+ <source>Save failed.</source>
+ <translation>Speichern fehlgeschlagen.</translation>
+ </message>
+ <message>
+ <source>Truncate this subtree?</source>
+ <translation>Diesen Teilbaum abschneiden?</translation>
+ </message>
+ <message>
+ <source>Truncate children?</source>
+ <translation>Kindknoten abschneiden?</translation>
+ </message>
+ <message>
+ <source>Delete all variations?</source>
+ <translation>Alle Varianten löschen?</translation>
+ </message>
+ <message>
+ <source>Version %1</source>
+ <translation>Version %1</translation>
+ </message>
+ <message>
+ <source>Computer opponent for the board game Blokus.</source>
+ <translation>Computer-Gegner für das Brettspiel Blokus.</translation>
+ </message>
+ <message>
+ <source>&copy; 2011&ndash;%1 Markus&nbsp;Enzenberger</source>
+ <translation>&copy; 2011&ndash;%1 Markus&nbsp;Enzenberger</translation>
+ </message>
+</context>
+<context>
+ <name>MenuComputer</name>
+ <message>
+ <source>&Computer</source>
+ <translation>&Computer</translation>
+ </message>
+ <message>
+ <source>Computer &Colors</source>
+ <translation>Computer-&Farben</translation>
+ </message>
+ <message>
+ <source>&Play</source>
+ <translation>&Spielen</translation>
+ </message>
+ <message>
+ <source>&Level (Classic, 4 Players)</source>
+ <translation>Spielst&ufe (Klassisch, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Classic, 2 Players)</source>
+ <translation>Spielst&ufe (Klassisch, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Classic, 3 Players)</source>
+ <translation>Spielst&ufe (Klassisch, 3 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Duo)</source>
+ <translation>Spielst&ufe (Duo)</translation>
+ </message>
+ <message>
+ <source>&Level (Junior)</source>
+ <translation>Spielst&ufe (Junior)</translation>
+ </message>
+ <message>
+ <source>&Level (Trigon, 4 Players)</source>
+ <translation>Spielst&ufe (Trigon, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Trigon, 2 Players)</source>
+ <translation>Spielst&ufe (Trigon, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Trigon, 3 Players)</source>
+ <translation>Spielst&ufe (Trigon, 3 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Nexos, 4 Players)</source>
+ <translation>Spielst&ufe (Nexos, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Nexos, 2 Players)</source>
+ <translation>Spielst&ufe (Nexos, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Callisto, 4 Players)</source>
+ <translation>Spielst&ufe (Callisto, 4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Callisto, 2 Players)</source>
+ <translation>Spielst&ufe (Callisto, 2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Level (Callisto, 3 Players)</source>
+ <translation>Spielst&ufe (Callisto, 3 Spieler)</translation>
+ </message>
+</context>
+<context>
+ <name>MenuEdit</name>
+ <message>
+ <source>&Edit</source>
+ <translation>&Bearbeiten</translation>
+ </message>
+ <message>
+ <source>Make &Main Variation</source>
+ <translation>Zu &Hauptvariante machen</translation>
+ </message>
+ <message>
+ <source>Move Variation &Up</source>
+ <translation>Variante nach &oben schieben</translation>
+ </message>
+ <message>
+ <source>Move Variation &Down</source>
+ <translation>Variante nach &unten schieben</translation>
+ </message>
+ <message>
+ <source>&Truncate</source>
+ <translation>&Abschneiden</translation>
+ </message>
+ <message>
+ <source>Truncate &Children</source>
+ <translation>&Kindknoten abschneiden</translation>
+ </message>
+ <message>
+ <source>&Delete All Variations</source>
+ <translation>Alle &Varianten löschen</translation>
+ </message>
+ <message>
+ <source>&Next Color</source>
+ <translation>&Nächste Farbe</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGame</name>
+ <message>
+ <source>&Game</source>
+ <translation>&Spiel</translation>
+ </message>
+ <message>
+ <source>&New</source>
+ <translation>&Neu</translation>
+ </message>
+ <message>
+ <source>Game &Variant</source>
+ <translation>Spiel&variante</translation>
+ </message>
+ <message>
+ <source>Classic (&3 Players)</source>
+ <translation>Klassisch (&3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Classic (&4 Players)</source>
+ <translation>Klassisch (&4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Duo</source>
+ <translation>&Duo</translation>
+ </message>
+ <message>
+ <source>&Junior</source>
+ <translation>&Junior</translation>
+ </message>
+ <message>
+ <source>&Undo Move</source>
+ <translation>Zug &rückgängig</translation>
+ </message>
+ <message>
+ <source>&Find Move</source>
+ <translation>Zug &finden</translation>
+ </message>
+ <message>
+ <source>&Open...</source>
+ <translation>Öffn&en ...</translation>
+ </message>
+ <message>
+ <source>&Save As...</source>
+ <translation>&Speichern unter ...</translation>
+ </message>
+ <message>
+ <source>&Quit</source>
+ <translation>&Beenden</translation>
+ </message>
+ <message>
+ <source>&Classic</source>
+ <translation>&Klassisch</translation>
+ </message>
+ <message>
+ <source>Classic (&2 Players)</source>
+ <translation>Klassisch (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Trigon</source>
+ <translation>&Trigon</translation>
+ </message>
+ <message>
+ <source>Trigon (&2 Players)</source>
+ <translation>Trigon (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Trigon (&3 Players)</source>
+ <translation>Trigon (&3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Trigon (&4 Players)</source>
+ <translation>Trigon (&4 Spieler)</translation>
+ </message>
+ <message>
+ <source>&Nexos</source>
+ <translation>&Nexos</translation>
+ </message>
+ <message>
+ <source>Nexos (&2 Players)</source>
+ <translation>Nexos (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Nexos (&4 Players)</source>
+ <translation>Nexos (&4 Spieler)</translation>
+ </message>
+ <message>
+ <source>C&allisto</source>
+ <translation>&Callisto</translation>
+ </message>
+ <message>
+ <source>Callisto (&2 Players)</source>
+ <translation>Callisto (&2 Spieler)</translation>
+ </message>
+ <message>
+ <source>Callisto (&3 Players)</source>
+ <translation>Callisto (&3 Spieler)</translation>
+ </message>
+ <message>
+ <source>Callisto (&4 Players)</source>
+ <translation>Callisto (&4 Spieler)</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGo</name>
+ <message>
+ <source>G&o</source>
+ <translation>&Gehe zu</translation>
+ </message>
+ <message>
+ <source>Back to &Main Variation</source>
+ <translation>Zurück zu &Hauptvariante</translation>
+ </message>
+</context>
+<context>
+ <name>MenuHelp</name>
+ <message>
+ <source>&Help</source>
+ <translation>&Hilfe</translation>
+ </message>
+ <message>
+ <source>&About Pentobi</source>
+ <translation>Über &Pentobi</translation>
+ </message>
+</context>
+<context>
+ <name>MenuView</name>
+ <message>
+ <source>&View</source>
+ <translation>&Ansicht</translation>
+ </message>
+ <message>
+ <source>Mark &Last Move</source>
+ <translation>&Letzten Zug markieren</translation>
+ </message>
+ <message>
+ <source>&Animate Pieces</source>
+ <translation>Spielsteine &animieren</translation>
+ </message>
+</context>
+<context>
+ <name>OpenDialog</name>
+ <message>
+ <source>Open</source>
+ <translation>Öffnen</translation>
+ </message>
+ <message>
+ <source>Blokus games (*.blksgf)</source>
+ <translation>Blokus-Partien (*.blksgf)</translation>
+ </message>
+ <message>
+ <source>All files (*)</source>
+ <translation>Alle Dateien (*)</translation>
+ </message>
+</context>
+<context>
+ <name>SaveDialog</name>
+ <message>
+ <source>Save</source>
+ <translation>Speichern</translation>
+ </message>
+ <message>
+ <source>Blokus games (*.blksgf)</source>
+ <translation>Blokus-Partien (*.blksgf)</translation>
+ </message>
+ <message>
+ <source>All files (*)</source>
+ <translation>Alle Dateien (*)</translation>
+ </message>
+</context>
+<context>
+ <name>main</name>
+ <message>
+ <source>Not enough memory.</source>
+ <translation>Nicht genügend Speicher.</translation>
+ </message>
+ <message>
+ <source>Pentobi</source>
+ <translation>Pentobi</translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+ <name>QPlatformTheme</name>
+ <message>
+ <source>Cancel</source>
+ <translation>Abbrechen</translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect id="a" height="4" width="4" y="3" x="9" fill="#888"/>
+<use xlink:href="#a" y="6"/>
+<use xlink:href="#a" y="12"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10.5 2.5s0 2.5 0 4h10v9h-10c0 1.5 0 4 0 4l-8.5-8.5 8.5-8.5z" fill="#888"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m4.5 2.5v8l4-4v-0h0l4-4s0 2.5 0 4l9 0v9l-9 0c0 1.5 0 4 0 4l-4-4l-4-4v8h-4v-17z" fill="#888"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m2.5 0c-1.35 0-2.5 1.1-2.5 2.44v15.1c0 1.35 1.1 2.44 2.44 2.44h7.56v2.02h12v-19.6c0-1.27-1.2-2.3-2.5-2.3zm16 3c0.3 0 0.522 0.258 0.522 0.562v6.44h-1v-6h-14v11h6v1l-6.42 0.002c-0.3 0-0.56-0.3-0.56-0.6v-11.9c0-0.26 0.18-0.5 0.48-0.5zm-7.5 8h10v10h-10zm1 1v4h4v-4zm4 4v4h4v-4zm-13 1h7v1h-7z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#888" d="m17.5 2.5v8l-4-4l-4-4s0 2.5 0 4l-9 0v9l9 0c0 1.5 0 4 0 4l4-4l4-4v8h4v-17z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#888" d="m12.5 2.5s0 2.5 0 4h-10v9h10c0 1.5 0 4 0 4l8.5-8.5-8.5-8.5z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m2 0c-1.11 0-2 0.894-2 2v17c0 1.11 0.892 2 2 2h17c1.11 0 2-0.894 2-2v-17c0-1.15-0.9-2-2-2-5.6 0-11.3 0-17 0zm0 1 12.6 0c-0.158 0.336-0.373 0.631-0.523 0.962h-3.1v1.38c-0.4 0.05-0.7 0.08-1 0.19v-1.57h-8v8h8l0.047-5.61c0.3 0.26 0.6 0.54 1 0.84v4.77h8l0.047-4.09c0.3-0.267 0.654-0.579 1-0.873v14c-0.0019 0.553-0.446 1-1 1h-17c-0.55 0-1-0.4-1-1v-17c0-0.55 0.45-1 1-1zm14.2 0.04 2.8-0.04c0.554-0.00749 1 0.447 1 1v1.37c-0.3-0.04-0.6-0.03-1-0.06v-1.31h-2.3c-0.166-0.346-0.31-0.623-0.486-0.962zm-0.875 0.343c0.512 0.0155 1.05 1.93 1.47 2.21 0.394 0.266 2.46 0.0956 2.59 0.53 0.143 0.458-1.65 1.51-1.81 1.97-0.15 0.43 0.653 2.21 0.25 2.46-0.424 0.267-2.05-1-2.56-0.998-0.487 0.000001-2.05 1.28-2.44 1-0.405-0.292 0.377-2.14 0.219-2.59-0.15-0.43-1.91-1.41-1.75-1.84 0.174-0.448 2.3-0.313 2.72-0.593 0.394-0.266 0.825-2.17 1.31-2.15zm-13.3 9.62v8h8v-8zm9 0v8h8v-8z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m19.61186 12.49993s-2.5481-0.000234-3.9688 0v-10h-9v10c-1.4428-0.000174-3.994 0.000778-4 0l8.5 8.5 8.4688-8.5z" fill="#888"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m2.5 0c-1.35 0-2.5 1.15-2.5 2.5v15c0 1.3 1.15 2.5 2.5 2.5h9.5v2l2.69-2h4.81c1.35 0 2.5-1.15 2.5-2.5v-15c0-1.35-1.2-2.5-2.5-2.5zm1.06 3h14.9c0.2 0 0.5 0.26 0.5 0.56v8.5l-1-0.8v-7.3h-14v11h8v1h-8.44c-0.3 0-0.56-0.3-0.56-0.6v-11.8c0-0.34 0.26-0.6 0.56-0.6zm9.44 6.3l7 5.2-7 5.1zm-10 7.7h9v1h-9zm16-0.1v1.1h-1.7z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m19.469 9.5s-2.5481 0.000234-3.9688 0v10h-9v-10c-1.4428 0.000174-3.994-0.000778-4 0l8.5-8.5 8.4688 8.5z" fill="#888"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="22" width="22" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <path fill="#888" d="m1 0c-0.603 0-1 0.397-1 1v19c0 0.603 0.397 1 1 1h19c0.603 0 1-0.397 1-1v-19c0-0.603-0.4-1-1-1zm0.6 1h17.7c0.546 0 0.7 0.154 0.7 0.7v17.6c0 0.546-0.154 0.7-0.7 0.7-3.49-0.0171-7.56-0.0022-10.7-0.000002l0.00586-1h1.38v-3.21c0.364 0.0398 0.721 0.118 1.03 0.27l-0.0254 2.94h8v-8h-8v0.193c-0.673-0.313-1.11-0.566-2-0.641v-0.553h1v-8h-8v8h2.28l-1.05 1h-1.23v1.18l-1 0.822 1 0.883v5.12h5.23l1.02 1h-6.65c-0.55 0-0.6-0.2-0.6-0.7v-6.3-11.3c0-0.55 0.05-0.7 0.6-0.7zm9.4 1v8h8v-8zm-3 5v4c3.07 0.258 4.71 2.24 4.84 2.46 1.38 1.52 2.07 3.83 2.16 5.48 0 0-2.15-2.57-3.11-3.09-0.9-0.5-1.8-0.8-3.9-0.8v4l-6-6z"/>
+</svg>
--- /dev/null
+import QtQuick 2.0
+
+QtObject {
+ property color backgroundColor: "#131313"
+ property color fontColorScore: "#C8C1BE"
+ property color fontColorPosInfo: "#C8C1BE"
+ property color colorBlue: "#0077D2"
+ property color colorYellow: "#EBCD23"
+ property color colorRed: "#E63E2C"
+ property color colorGreen: "#00C000"
+ property color colorStartingPoint: "#82777E"
+ property color backgroundButtonPressed: Qt.lighter(backgroundColor, 3)
+ property real pieceListOpacity: 0.94
+ property real toPlayColorLighter: 1.7
+
+ function getImage(name) {
+ if (name.lastIndexOf("frame-", 0) === 0
+ || name.lastIndexOf("junction-", 0) === 0
+ || name.lastIndexOf("linesegment-", 0) === 0
+ || name.lastIndexOf("piece-manipulator", 0) === 0
+ || name.lastIndexOf("square-", 0) === 0
+ || name.lastIndexOf("triangle-", 0) === 0)
+ return "themes/light/" + name + ".svg"
+ return "themes/dark/" + name + ".svg"
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="224" width="224" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#494347"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#3b3639"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 154)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 140)"/>
+<use xlink:href="#a" transform="translate(28 154)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 126)"/>
+<use xlink:href="#a" transform="translate(42 140)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<g id="b" transform="translate(98 70)">
+<rect height="14" width="14" fill="#494347"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(126)">
+<rect height="14" width="14" fill="#494347"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#3b3639"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="translate(-126,126)"/>
+<use xlink:href="#a" transform="translate(-126,140)"/>
+<use xlink:href="#a" transform="translate(-112,112)"/>
+<use xlink:href="#a" transform="translate(-112,126)"/>
+<use xlink:href="#a" transform="translate(-112,140)"/>
+<use xlink:href="#a" transform="translate(-112,154)"/>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-98 126)"/>
+<use xlink:href="#a" transform="translate(-98 140)"/>
+<use xlink:href="#a" transform="translate(-98 154)"/>
+<use xlink:href="#a" transform="translate(-98 168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-84 140)"/>
+<use xlink:href="#a" transform="translate(-84 154)"/>
+<use xlink:href="#a" transform="translate(-84 168)"/>
+<use xlink:href="#a" transform="translate(-84 182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-70 154)"/>
+<use xlink:href="#a" transform="translate(-70 168)"/>
+<use xlink:href="#a" transform="translate(-70 182)"/>
+<use xlink:href="#a" transform="translate(-70 196)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-56 168)"/>
+<use xlink:href="#a" transform="translate(-56 182)"/>
+<use xlink:href="#a" transform="translate(-56 196)"/>
+<use xlink:href="#a" transform="translate(-56 210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 98)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 98)"/>
+<use xlink:href="#a" transform="translate(42 112)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<use xlink:href="#a" transform="translate(112,126)"/>
+<use xlink:href="#a" transform="translate(112,140)"/>
+<use xlink:href="#a" transform="translate(112,154)"/>
+<use xlink:href="#a" transform="translate(112,168)"/>
+<use xlink:href="#a" transform="translate(126,112)"/>
+<use xlink:href="#a" transform="translate(126,126)"/>
+<use xlink:href="#a" transform="translate(126,140)"/>
+<use xlink:href="#a" transform="translate(126,154)"/>
+<use xlink:href="#a" transform="translate(140,126)"/>
+<use xlink:href="#a" transform="translate(140,140)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#494347"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#494347"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#3b3639"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98,112)"/>
+<use xlink:href="#a" transform="translate(-98,126)"/>
+<use xlink:href="#a" transform="translate(-98,140)"/>
+<use xlink:href="#a" transform="translate(-98,154)"/>
+<use xlink:href="#a" transform="translate(-98,168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84,112)"/>
+<use xlink:href="#a" transform="translate(-84,126)"/>
+<use xlink:href="#a" transform="translate(-84,140)"/>
+<use xlink:href="#a" transform="translate(-84,154)"/>
+<use xlink:href="#a" transform="translate(-84,168)"/>
+<use xlink:href="#a" transform="translate(-84,182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70,112)"/>
+<use xlink:href="#a" transform="translate(-70,126)"/>
+<use xlink:href="#a" transform="translate(-70,140)"/>
+<use xlink:href="#a" transform="translate(-70,154)"/>
+<use xlink:href="#a" transform="translate(-70,168)"/>
+<use xlink:href="#a" transform="translate(-70,182)"/>
+<use xlink:href="#a" transform="translate(-70,196)"/>
+<use xlink:href="#a" transform="translate(-56,56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56,112)"/>
+<use xlink:href="#a" transform="translate(-56,126)"/>
+<use xlink:href="#a" transform="translate(-56,140)"/>
+<use xlink:href="#a" transform="translate(-56,154)"/>
+<use xlink:href="#a" transform="translate(-56,168)"/>
+<use xlink:href="#a" transform="translate(-56,182)"/>
+<use xlink:href="#a" transform="translate(-56,196)"/>
+<use xlink:href="#a" transform="translate(-56,210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 112)"/>
+<use xlink:href="#a" transform="translate(-14 126)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 98)"/>
+<use xlink:href="#a" transform="translate(0 112)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 98)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(28 266)"/>
+<use xlink:href="#a" transform="translate(42)"/>
+<use xlink:href="#a" transform="translate(42 14)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(42 252)"/>
+<use xlink:href="#a" transform="translate(42 266)"/>
+<use xlink:href="#a" transform="translate(56)"/>
+<use xlink:href="#a" transform="translate(56 14)"/>
+<use xlink:href="#a" transform="translate(56 28)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(56 238)"/>
+<use xlink:href="#a" transform="translate(56 252)"/>
+<use xlink:href="#a" transform="translate(56 266)"/>
+<use xlink:href="#a" transform="translate(70)"/>
+<use xlink:href="#a" transform="translate(70 14)"/>
+<use xlink:href="#a" transform="translate(70 28)"/>
+<use xlink:href="#a" transform="translate(70 42)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(70 224)"/>
+<use xlink:href="#a" transform="translate(70 238)"/>
+<use xlink:href="#a" transform="translate(70 252)"/>
+<use xlink:href="#a" transform="translate(70 266)"/>
+<use xlink:href="#a" transform="translate(84 14)"/>
+<use xlink:href="#a" transform="translate(84 28)"/>
+<use xlink:href="#a" transform="translate(84 42)"/>
+<use xlink:href="#a" transform="translate(84 56)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(84 210)"/>
+<use xlink:href="#a" transform="translate(84 224)"/>
+<use xlink:href="#a" transform="translate(84 238)"/>
+<use xlink:href="#a" transform="translate(84 252)"/>
+<use xlink:href="#a" transform="translate(98 28)"/>
+<use xlink:href="#a" transform="translate(98 42)"/>
+<use xlink:href="#a" transform="translate(98 56)"/>
+<use xlink:href="#a" transform="translate(98 70)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(98 196)"/>
+<use xlink:href="#a" transform="translate(98 210)"/>
+<use xlink:href="#a" transform="translate(98 224)"/>
+<use xlink:href="#a" transform="translate(98 238)"/>
+<use xlink:href="#a" transform="translate(112 42)"/>
+<use xlink:href="#a" transform="translate(112 56)"/>
+<use xlink:href="#a" transform="translate(112 70)"/>
+<use xlink:href="#a" transform="translate(112 84)"/>
+<use xlink:href="#a" transform="translate(112 98)"/>
+<use xlink:href="#a" transform="translate(112 112)"/>
+<use xlink:href="#a" transform="translate(112 126)"/>
+<use xlink:href="#a" transform="translate(112 140)"/>
+<use xlink:href="#a" transform="translate(112 154)"/>
+<use xlink:href="#a" transform="translate(112 168)"/>
+<use xlink:href="#a" transform="translate(112 182)"/>
+<use xlink:href="#a" transform="translate(112 196)"/>
+<use xlink:href="#a" transform="translate(112 210)"/>
+<use xlink:href="#a" transform="translate(112 224)"/>
+<use xlink:href="#a" transform="translate(126 56)"/>
+<use xlink:href="#a" transform="translate(126 70)"/>
+<use xlink:href="#a" transform="translate(126 84)"/>
+<use xlink:href="#a" transform="translate(126 98)"/>
+<use xlink:href="#a" transform="translate(126 112)"/>
+<use xlink:href="#a" transform="translate(126 126)"/>
+<use xlink:href="#a" transform="translate(126 140)"/>
+<use xlink:href="#a" transform="translate(126 154)"/>
+<use xlink:href="#a" transform="translate(126 168)"/>
+<use xlink:href="#a" transform="translate(126 182)"/>
+<use xlink:href="#a" transform="translate(126 196)"/>
+<use xlink:href="#a" transform="translate(126 210)"/>
+<use xlink:href="#a" transform="translate(140 70)"/>
+<use xlink:href="#a" transform="translate(140 84)"/>
+<use xlink:href="#a" transform="translate(140 98)"/>
+<use xlink:href="#a" transform="translate(140 112)"/>
+<use xlink:href="#a" transform="translate(140 126)"/>
+<use xlink:href="#a" transform="translate(140 140)"/>
+<use xlink:href="#a" transform="translate(140 154)"/>
+<use xlink:href="#a" transform="translate(140 168)"/>
+<use xlink:href="#a" transform="translate(140 182)"/>
+<use xlink:href="#a" transform="translate(140 196)"/>
+<use xlink:href="#a" transform="translate(154 84)"/>
+<use xlink:href="#a" transform="translate(154 98)"/>
+<use xlink:href="#a" transform="translate(154 112)"/>
+<use xlink:href="#a" transform="translate(154 126)"/>
+<use xlink:href="#a" transform="translate(154 140)"/>
+<use xlink:href="#a" transform="translate(154 154)"/>
+<use xlink:href="#a" transform="translate(154 168)"/>
+<use xlink:href="#a" transform="translate(154 182)"/>
+<use xlink:href="#a" transform="translate(168 98)"/>
+<use xlink:href="#a" transform="translate(168 112)"/>
+<use xlink:href="#a" transform="translate(168 126)"/>
+<use xlink:href="#a" transform="translate(168 140)"/>
+<use xlink:href="#a" transform="translate(168 154)"/>
+<use xlink:href="#a" transform="translate(168 168)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#494347"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="14" width="14" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="14" height="14" fill="#494347"/>
+<path d="m14 0h-14v14l0.7-0.7v-12.6h12.6z" fill="#3b3639"/>
+<path d="m14 0-0.7 0.7v12.6h-12.6l-0.7 0.7h14z" fill="#6d686b"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="28" width="28" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="28" height="28" fill="#494347"/>
+<g id="a">
+<path d="m0 28v-21h7l-1 1h-5v19z" fill="#3b3639"/>
+<path d="m0 28 1-1h5v-19l1-1v21z" fill="#6d686b"/>
+</g>
+<use xlink:href="#a" transform="matrix(0,1,1,0,0,0)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="320" width="320" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m0 160 80-160h160l80 160-80 160h-160z" fill="#494347"/>
+<g id="c">
+<path d="m70 20 1.732-1.155l8.268-16.845v-2z" fill="#3B3639"/>
+<path d="m70 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#6D686B"/>
+</g>
+<g id="d">
+<path d="m100 0-1.732 1.155l-8.268 16.845v2z" fill="#6D686B"/>
+<path d="m100 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#3B3639"/>
+</g>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="140"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="360" width="360" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m90 0-90 180 90 180h180l90-180-90-180z" fill="#494347"/>
+<g id="c">
+<path d="m80 20 1.732-1.155l8.268-16.845v-2z" fill="#3B3639"/>
+<path d="m80 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#6D686B"/>
+</g>
+<g id="d">
+<path d="m110 0-1.732 1.155l-8.268 16.845v2z" fill="#6D686B"/>
+<path d="m110 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#3B3639"/>
+</g>
+<use xlink:href="#d" x="-90" y="180"/>
+<use xlink:href="#d" x="-80" y="200"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-70" y="220"/>
+<use xlink:href="#d" x="-60" y="240"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-50" y="260"/>
+<use xlink:href="#d" x="-40" y="280"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-30" y="300"/>
+<use xlink:href="#d" x="-20" y="320"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" y="320"/>
+<use xlink:href="#d" x="-10" y="340"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20" y="320"/>
+<use xlink:href="#d" x="10" y="340"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40" y="320"/>
+<use xlink:href="#d" x="30" y="340"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60" y="320"/>
+<use xlink:href="#d" x="50" y="340"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80" y="320"/>
+<use xlink:href="#d" x="70" y="340"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100" y="320"/>
+<use xlink:href="#d" x="90" y="340"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120" y="320"/>
+<use xlink:href="#d" x="110" y="340"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140" y="320"/>
+<use xlink:href="#d" x="130" y="340"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="160" y="320"/>
+<use xlink:href="#d" x="150" y="340"/>
+<use xlink:href="#d" x="160"/>
+<use xlink:href="#d" x="170" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="180" y="280"/>
+<use xlink:href="#d" x="170" y="300"/>
+<use xlink:href="#d" x="180" y="320"/>
+<use xlink:href="#d" x="170" y="340"/>
+<use xlink:href="#d" x="180" y="40"/>
+<use xlink:href="#d" x="190" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="200" y="240"/>
+<use xlink:href="#d" x="190" y="260"/>
+<use xlink:href="#d" x="200" y="280"/>
+<use xlink:href="#d" x="190" y="300"/>
+<use xlink:href="#d" x="200" y="80"/>
+<use xlink:href="#d" x="210" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="220" y="200"/>
+<use xlink:href="#d" x="210" y="220"/>
+<use xlink:href="#d" x="220" y="240"/>
+<use xlink:href="#d" x="210" y="260"/>
+<use xlink:href="#d" x="220" y="120"/>
+<use xlink:href="#d" x="230" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#d" x="230" y="180"/>
+<use xlink:href="#d" x="240" y="200"/>
+<use xlink:href="#d" x="230" y="220"/>
+<use xlink:href="#d" x="240" y="160"/>
+<use xlink:href="#d" x="250" y="180"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-80" y="160"/>
+<use xlink:href="#c" x="-70" y="180"/>
+<use xlink:href="#c" x="-60" y="200"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-50" y="220"/>
+<use xlink:href="#c" x="-40" y="240"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-30" y="260"/>
+<use xlink:href="#c" x="-20" y="280"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="-10" y="300"/>
+<use xlink:href="#c" y="320"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20" y="320"/>
+<use xlink:href="#c" x="10" y="340"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40" y="320"/>
+<use xlink:href="#c" x="30" y="340"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60" y="320"/>
+<use xlink:href="#c" x="50" y="340"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80" y="320"/>
+<use xlink:href="#c" x="70" y="340"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100" y="320"/>
+<use xlink:href="#c" x="90" y="340"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120" y="320"/>
+<use xlink:href="#c" x="110" y="340"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140" y="320"/>
+<use xlink:href="#c" x="130" y="340"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160" y="320"/>
+<use xlink:href="#c" x="150" y="340"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="180" y="280"/>
+<use xlink:href="#c" x="170" y="300"/>
+<use xlink:href="#c" x="180" y="320"/>
+<use xlink:href="#c" x="170" y="340"/>
+<use xlink:href="#c" x="180"/>
+<use xlink:href="#c" x="190" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="200" y="240"/>
+<use xlink:href="#c" x="190" y="260"/>
+<use xlink:href="#c" x="200" y="280"/>
+<use xlink:href="#c" x="190" y="300"/>
+<use xlink:href="#c" x="200" y="40"/>
+<use xlink:href="#c" x="210" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="220" y="200"/>
+<use xlink:href="#c" x="210" y="220"/>
+<use xlink:href="#c" x="220" y="240"/>
+<use xlink:href="#c" x="210" y="260"/>
+<use xlink:href="#c" x="220" y="80"/>
+<use xlink:href="#c" x="230" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="230" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="180"/>
+<use xlink:href="#c" x="240" y="200"/>
+<use xlink:href="#c" x="230" y="220"/>
+<use xlink:href="#c" x="240" y="120"/>
+<use xlink:href="#c" x="250" y="140"/>
+<use xlink:href="#c" x="240" y="160"/>
+<use xlink:href="#c" x="250" y="180"/>
+<use xlink:href="#c" x="260" y="160"/>
+</svg>
--- /dev/null
+import QtQuick 2.0
+
+QtObject {
+ property color backgroundColor: "#E6E5E5"
+ property color fontColorScore: "#5A5755"
+ property color fontColorPosInfo: "#282625"
+ property color colorBlue: "#0077D2"
+ property color colorYellow: "#EBCD23"
+ property color colorRed: "#E63E2C"
+ property color colorGreen: "#00C000"
+ property color colorStartingPoint: "#767074"
+ property color backgroundButtonPressed: Qt.lighter(backgroundColor)
+ property real pieceListOpacity: 1
+ property real toPlayColorLighter: 0.5
+
+ function getImage(name) { return "themes/light/" + name + ".svg" }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="224" width="224" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#767074"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 154)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 140)"/>
+<use xlink:href="#a" transform="translate(28 154)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 126)"/>
+<use xlink:href="#a" transform="translate(42 140)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<g id="b" transform="translate(98 70)">
+<rect height="14" width="14" fill="#9a9298"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(126)">
+<rect height="14" width="14" fill="#9a9298"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#767074"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="translate(-126,126)"/>
+<use xlink:href="#a" transform="translate(-126,140)"/>
+<use xlink:href="#a" transform="translate(-112,112)"/>
+<use xlink:href="#a" transform="translate(-112,126)"/>
+<use xlink:href="#a" transform="translate(-112,140)"/>
+<use xlink:href="#a" transform="translate(-112,154)"/>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98 112)"/>
+<use xlink:href="#a" transform="translate(-98 126)"/>
+<use xlink:href="#a" transform="translate(-98 140)"/>
+<use xlink:href="#a" transform="translate(-98 154)"/>
+<use xlink:href="#a" transform="translate(-98 168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84 112)"/>
+<use xlink:href="#a" transform="translate(-84 126)"/>
+<use xlink:href="#a" transform="translate(-84 140)"/>
+<use xlink:href="#a" transform="translate(-84 154)"/>
+<use xlink:href="#a" transform="translate(-84 168)"/>
+<use xlink:href="#a" transform="translate(-84 182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70 112)"/>
+<use xlink:href="#a" transform="translate(-70 126)"/>
+<use xlink:href="#a" transform="translate(-70 140)"/>
+<use xlink:href="#a" transform="translate(-70 154)"/>
+<use xlink:href="#a" transform="translate(-70 168)"/>
+<use xlink:href="#a" transform="translate(-70 182)"/>
+<use xlink:href="#a" transform="translate(-70 196)"/>
+<use xlink:href="#a" transform="translate(-56 56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56 112)"/>
+<use xlink:href="#a" transform="translate(-56 126)"/>
+<use xlink:href="#a" transform="translate(-56 140)"/>
+<use xlink:href="#a" transform="translate(-56 154)"/>
+<use xlink:href="#a" transform="translate(-56 168)"/>
+<use xlink:href="#a" transform="translate(-56 182)"/>
+<use xlink:href="#a" transform="translate(-56 196)"/>
+<use xlink:href="#a" transform="translate(-56 210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 98)"/>
+<use xlink:href="#a" transform="translate(28 168)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 98)"/>
+<use xlink:href="#a" transform="translate(42 112)"/>
+<use xlink:href="#a" transform="translate(42 154)"/>
+<use xlink:href="#a" transform="translate(42 168)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 112)"/>
+<use xlink:href="#a" transform="translate(56 126)"/>
+<use xlink:href="#a" transform="translate(56 140)"/>
+<use xlink:href="#a" transform="translate(56 154)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 126)"/>
+<use xlink:href="#a" transform="translate(70 140)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(112,98)"/>
+<use xlink:href="#a" transform="translate(112,112)"/>
+<use xlink:href="#a" transform="translate(112,126)"/>
+<use xlink:href="#a" transform="translate(112,140)"/>
+<use xlink:href="#a" transform="translate(112,154)"/>
+<use xlink:href="#a" transform="translate(112,168)"/>
+<use xlink:href="#a" transform="translate(126,112)"/>
+<use xlink:href="#a" transform="translate(126,126)"/>
+<use xlink:href="#a" transform="translate(126,140)"/>
+<use xlink:href="#a" transform="translate(126,154)"/>
+<use xlink:href="#a" transform="translate(140,126)"/>
+<use xlink:href="#a" transform="translate(140,140)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="280" width="280" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<g id="a" transform="translate(98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#767074"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="translate(-98 98)"/>
+<use xlink:href="#a" transform="translate(-98,112)"/>
+<use xlink:href="#a" transform="translate(-98,126)"/>
+<use xlink:href="#a" transform="translate(-98,140)"/>
+<use xlink:href="#a" transform="translate(-98,154)"/>
+<use xlink:href="#a" transform="translate(-98,168)"/>
+<use xlink:href="#a" transform="translate(-84 84)"/>
+<use xlink:href="#a" transform="translate(-84 98)"/>
+<use xlink:href="#a" transform="translate(-84,112)"/>
+<use xlink:href="#a" transform="translate(-84,126)"/>
+<use xlink:href="#a" transform="translate(-84,140)"/>
+<use xlink:href="#a" transform="translate(-84,154)"/>
+<use xlink:href="#a" transform="translate(-84,168)"/>
+<use xlink:href="#a" transform="translate(-84,182)"/>
+<use xlink:href="#a" transform="translate(-70 70)"/>
+<use xlink:href="#a" transform="translate(-70 84)"/>
+<use xlink:href="#a" transform="translate(-70 98)"/>
+<use xlink:href="#a" transform="translate(-70,112)"/>
+<use xlink:href="#a" transform="translate(-70,126)"/>
+<use xlink:href="#a" transform="translate(-70,140)"/>
+<use xlink:href="#a" transform="translate(-70,154)"/>
+<use xlink:href="#a" transform="translate(-70,168)"/>
+<use xlink:href="#a" transform="translate(-70,182)"/>
+<use xlink:href="#a" transform="translate(-70,196)"/>
+<use xlink:href="#a" transform="translate(-56,56)"/>
+<use xlink:href="#a" transform="translate(-56 70)"/>
+<use xlink:href="#a" transform="translate(-56 84)"/>
+<use xlink:href="#a" transform="translate(-56 98)"/>
+<use xlink:href="#a" transform="translate(-56,112)"/>
+<use xlink:href="#a" transform="translate(-56,126)"/>
+<use xlink:href="#a" transform="translate(-56,140)"/>
+<use xlink:href="#a" transform="translate(-56,154)"/>
+<use xlink:href="#a" transform="translate(-56,168)"/>
+<use xlink:href="#a" transform="translate(-56,182)"/>
+<use xlink:href="#a" transform="translate(-56,196)"/>
+<use xlink:href="#a" transform="translate(-56,210)"/>
+<use xlink:href="#a" transform="translate(-42 42)"/>
+<use xlink:href="#a" transform="translate(-42 56)"/>
+<use xlink:href="#a" transform="translate(-42 70)"/>
+<use xlink:href="#a" transform="translate(-42 84)"/>
+<use xlink:href="#a" transform="translate(-42 98)"/>
+<use xlink:href="#a" transform="translate(-42 112)"/>
+<use xlink:href="#a" transform="translate(-42 126)"/>
+<use xlink:href="#a" transform="translate(-42 140)"/>
+<use xlink:href="#a" transform="translate(-42 154)"/>
+<use xlink:href="#a" transform="translate(-42 168)"/>
+<use xlink:href="#a" transform="translate(-42 182)"/>
+<use xlink:href="#a" transform="translate(-42 196)"/>
+<use xlink:href="#a" transform="translate(-42 210)"/>
+<use xlink:href="#a" transform="translate(-42 224)"/>
+<use xlink:href="#a" transform="translate(-28 28)"/>
+<use xlink:href="#a" transform="translate(-28 42)"/>
+<use xlink:href="#a" transform="translate(-28 56)"/>
+<use xlink:href="#a" transform="translate(-28 70)"/>
+<use xlink:href="#a" transform="translate(-28 84)"/>
+<use xlink:href="#a" transform="translate(-28 98)"/>
+<use xlink:href="#a" transform="translate(-28 112)"/>
+<use xlink:href="#a" transform="translate(-28 126)"/>
+<use xlink:href="#a" transform="translate(-28 140)"/>
+<use xlink:href="#a" transform="translate(-28 154)"/>
+<use xlink:href="#a" transform="translate(-28 168)"/>
+<use xlink:href="#a" transform="translate(-28 182)"/>
+<use xlink:href="#a" transform="translate(-28 196)"/>
+<use xlink:href="#a" transform="translate(-28 210)"/>
+<use xlink:href="#a" transform="translate(-28 224)"/>
+<use xlink:href="#a" transform="translate(-28 238)"/>
+<use xlink:href="#a" transform="translate(-14 14)"/>
+<use xlink:href="#a" transform="translate(-14 28)"/>
+<use xlink:href="#a" transform="translate(-14 42)"/>
+<use xlink:href="#a" transform="translate(-14 56)"/>
+<use xlink:href="#a" transform="translate(-14 70)"/>
+<use xlink:href="#a" transform="translate(-14 84)"/>
+<use xlink:href="#a" transform="translate(-14 98)"/>
+<use xlink:href="#a" transform="translate(-14 112)"/>
+<use xlink:href="#a" transform="translate(-14 126)"/>
+<use xlink:href="#a" transform="translate(-14 140)"/>
+<use xlink:href="#a" transform="translate(-14 154)"/>
+<use xlink:href="#a" transform="translate(-14 168)"/>
+<use xlink:href="#a" transform="translate(-14 182)"/>
+<use xlink:href="#a" transform="translate(-14 196)"/>
+<use xlink:href="#a" transform="translate(-14 210)"/>
+<use xlink:href="#a" transform="translate(-14 224)"/>
+<use xlink:href="#a" transform="translate(-14 238)"/>
+<use xlink:href="#a" transform="translate(-14 252)"/>
+<use xlink:href="#a" transform="translate(0 14)"/>
+<use xlink:href="#a" transform="translate(0 28)"/>
+<use xlink:href="#a" transform="translate(0 42)"/>
+<use xlink:href="#a" transform="translate(0 56)"/>
+<use xlink:href="#a" transform="translate(0 70)"/>
+<use xlink:href="#a" transform="translate(0 84)"/>
+<use xlink:href="#a" transform="translate(0 98)"/>
+<use xlink:href="#a" transform="translate(0 112)"/>
+<use xlink:href="#a" transform="translate(0 154)"/>
+<use xlink:href="#a" transform="translate(0 168)"/>
+<use xlink:href="#a" transform="translate(0 182)"/>
+<use xlink:href="#a" transform="translate(0 196)"/>
+<use xlink:href="#a" transform="translate(0 210)"/>
+<use xlink:href="#a" transform="translate(0 224)"/>
+<use xlink:href="#a" transform="translate(0 238)"/>
+<use xlink:href="#a" transform="translate(0 252)"/>
+<use xlink:href="#a" transform="translate(0 266)"/>
+<use xlink:href="#a" transform="translate(14)"/>
+<use xlink:href="#a" transform="translate(14 14)"/>
+<use xlink:href="#a" transform="translate(14 28)"/>
+<use xlink:href="#a" transform="translate(14 42)"/>
+<use xlink:href="#a" transform="translate(14 56)"/>
+<use xlink:href="#a" transform="translate(14 70)"/>
+<use xlink:href="#a" transform="translate(14 84)"/>
+<use xlink:href="#a" transform="translate(14 98)"/>
+<use xlink:href="#a" transform="translate(14 168)"/>
+<use xlink:href="#a" transform="translate(14 182)"/>
+<use xlink:href="#a" transform="translate(14 196)"/>
+<use xlink:href="#a" transform="translate(14 210)"/>
+<use xlink:href="#a" transform="translate(14 224)"/>
+<use xlink:href="#a" transform="translate(14 238)"/>
+<use xlink:href="#a" transform="translate(14 252)"/>
+<use xlink:href="#a" transform="translate(14 266)"/>
+<use xlink:href="#a" transform="translate(28)"/>
+<use xlink:href="#a" transform="translate(28 14)"/>
+<use xlink:href="#a" transform="translate(28 28)"/>
+<use xlink:href="#a" transform="translate(28 42)"/>
+<use xlink:href="#a" transform="translate(28 56)"/>
+<use xlink:href="#a" transform="translate(28 70)"/>
+<use xlink:href="#a" transform="translate(28 84)"/>
+<use xlink:href="#a" transform="translate(28 182)"/>
+<use xlink:href="#a" transform="translate(28 196)"/>
+<use xlink:href="#a" transform="translate(28 210)"/>
+<use xlink:href="#a" transform="translate(28 224)"/>
+<use xlink:href="#a" transform="translate(28 238)"/>
+<use xlink:href="#a" transform="translate(28 252)"/>
+<use xlink:href="#a" transform="translate(28 266)"/>
+<use xlink:href="#a" transform="translate(42)"/>
+<use xlink:href="#a" transform="translate(42 14)"/>
+<use xlink:href="#a" transform="translate(42 28)"/>
+<use xlink:href="#a" transform="translate(42 42)"/>
+<use xlink:href="#a" transform="translate(42 56)"/>
+<use xlink:href="#a" transform="translate(42 70)"/>
+<use xlink:href="#a" transform="translate(42 84)"/>
+<use xlink:href="#a" transform="translate(42 182)"/>
+<use xlink:href="#a" transform="translate(42 196)"/>
+<use xlink:href="#a" transform="translate(42 210)"/>
+<use xlink:href="#a" transform="translate(42 224)"/>
+<use xlink:href="#a" transform="translate(42 238)"/>
+<use xlink:href="#a" transform="translate(42 252)"/>
+<use xlink:href="#a" transform="translate(42 266)"/>
+<use xlink:href="#a" transform="translate(56)"/>
+<use xlink:href="#a" transform="translate(56 14)"/>
+<use xlink:href="#a" transform="translate(56 28)"/>
+<use xlink:href="#a" transform="translate(56 42)"/>
+<use xlink:href="#a" transform="translate(56 56)"/>
+<use xlink:href="#a" transform="translate(56 70)"/>
+<use xlink:href="#a" transform="translate(56 84)"/>
+<use xlink:href="#a" transform="translate(56 98)"/>
+<use xlink:href="#a" transform="translate(56 168)"/>
+<use xlink:href="#a" transform="translate(56 182)"/>
+<use xlink:href="#a" transform="translate(56 196)"/>
+<use xlink:href="#a" transform="translate(56 210)"/>
+<use xlink:href="#a" transform="translate(56 224)"/>
+<use xlink:href="#a" transform="translate(56 238)"/>
+<use xlink:href="#a" transform="translate(56 252)"/>
+<use xlink:href="#a" transform="translate(56 266)"/>
+<use xlink:href="#a" transform="translate(70)"/>
+<use xlink:href="#a" transform="translate(70 14)"/>
+<use xlink:href="#a" transform="translate(70 28)"/>
+<use xlink:href="#a" transform="translate(70 42)"/>
+<use xlink:href="#a" transform="translate(70 56)"/>
+<use xlink:href="#a" transform="translate(70 70)"/>
+<use xlink:href="#a" transform="translate(70 84)"/>
+<use xlink:href="#a" transform="translate(70 98)"/>
+<use xlink:href="#a" transform="translate(70 112)"/>
+<use xlink:href="#a" transform="translate(70 154)"/>
+<use xlink:href="#a" transform="translate(70 168)"/>
+<use xlink:href="#a" transform="translate(70 182)"/>
+<use xlink:href="#a" transform="translate(70 196)"/>
+<use xlink:href="#a" transform="translate(70 210)"/>
+<use xlink:href="#a" transform="translate(70 224)"/>
+<use xlink:href="#a" transform="translate(70 238)"/>
+<use xlink:href="#a" transform="translate(70 252)"/>
+<use xlink:href="#a" transform="translate(70 266)"/>
+<use xlink:href="#a" transform="translate(84 14)"/>
+<use xlink:href="#a" transform="translate(84 28)"/>
+<use xlink:href="#a" transform="translate(84 42)"/>
+<use xlink:href="#a" transform="translate(84 56)"/>
+<use xlink:href="#a" transform="translate(84 70)"/>
+<use xlink:href="#a" transform="translate(84 84)"/>
+<use xlink:href="#a" transform="translate(84 98)"/>
+<use xlink:href="#a" transform="translate(84 112)"/>
+<use xlink:href="#a" transform="translate(84 126)"/>
+<use xlink:href="#a" transform="translate(84 140)"/>
+<use xlink:href="#a" transform="translate(84 154)"/>
+<use xlink:href="#a" transform="translate(84 168)"/>
+<use xlink:href="#a" transform="translate(84 182)"/>
+<use xlink:href="#a" transform="translate(84 196)"/>
+<use xlink:href="#a" transform="translate(84 210)"/>
+<use xlink:href="#a" transform="translate(84 224)"/>
+<use xlink:href="#a" transform="translate(84 238)"/>
+<use xlink:href="#a" transform="translate(84 252)"/>
+<use xlink:href="#a" transform="translate(98 28)"/>
+<use xlink:href="#a" transform="translate(98 42)"/>
+<use xlink:href="#a" transform="translate(98 56)"/>
+<use xlink:href="#a" transform="translate(98 70)"/>
+<use xlink:href="#a" transform="translate(98 84)"/>
+<use xlink:href="#a" transform="translate(98 98)"/>
+<use xlink:href="#a" transform="translate(98 112)"/>
+<use xlink:href="#a" transform="translate(98 126)"/>
+<use xlink:href="#a" transform="translate(98 140)"/>
+<use xlink:href="#a" transform="translate(98 154)"/>
+<use xlink:href="#a" transform="translate(98 168)"/>
+<use xlink:href="#a" transform="translate(98 182)"/>
+<use xlink:href="#a" transform="translate(98 196)"/>
+<use xlink:href="#a" transform="translate(98 210)"/>
+<use xlink:href="#a" transform="translate(98 224)"/>
+<use xlink:href="#a" transform="translate(98 238)"/>
+<use xlink:href="#a" transform="translate(112 42)"/>
+<use xlink:href="#a" transform="translate(112 56)"/>
+<use xlink:href="#a" transform="translate(112 70)"/>
+<use xlink:href="#a" transform="translate(112 84)"/>
+<use xlink:href="#a" transform="translate(112 98)"/>
+<use xlink:href="#a" transform="translate(112 112)"/>
+<use xlink:href="#a" transform="translate(112 126)"/>
+<use xlink:href="#a" transform="translate(112 140)"/>
+<use xlink:href="#a" transform="translate(112 154)"/>
+<use xlink:href="#a" transform="translate(112 168)"/>
+<use xlink:href="#a" transform="translate(112 182)"/>
+<use xlink:href="#a" transform="translate(112 196)"/>
+<use xlink:href="#a" transform="translate(112 210)"/>
+<use xlink:href="#a" transform="translate(112 224)"/>
+<use xlink:href="#a" transform="translate(126 56)"/>
+<use xlink:href="#a" transform="translate(126 70)"/>
+<use xlink:href="#a" transform="translate(126 84)"/>
+<use xlink:href="#a" transform="translate(126 98)"/>
+<use xlink:href="#a" transform="translate(126 112)"/>
+<use xlink:href="#a" transform="translate(126 126)"/>
+<use xlink:href="#a" transform="translate(126 140)"/>
+<use xlink:href="#a" transform="translate(126 154)"/>
+<use xlink:href="#a" transform="translate(126 168)"/>
+<use xlink:href="#a" transform="translate(126 182)"/>
+<use xlink:href="#a" transform="translate(126 196)"/>
+<use xlink:href="#a" transform="translate(126 210)"/>
+<use xlink:href="#a" transform="translate(140 70)"/>
+<use xlink:href="#a" transform="translate(140 84)"/>
+<use xlink:href="#a" transform="translate(140 98)"/>
+<use xlink:href="#a" transform="translate(140 112)"/>
+<use xlink:href="#a" transform="translate(140 126)"/>
+<use xlink:href="#a" transform="translate(140 140)"/>
+<use xlink:href="#a" transform="translate(140 154)"/>
+<use xlink:href="#a" transform="translate(140 168)"/>
+<use xlink:href="#a" transform="translate(140 182)"/>
+<use xlink:href="#a" transform="translate(140 196)"/>
+<use xlink:href="#a" transform="translate(154 84)"/>
+<use xlink:href="#a" transform="translate(154 98)"/>
+<use xlink:href="#a" transform="translate(154 112)"/>
+<use xlink:href="#a" transform="translate(154 126)"/>
+<use xlink:href="#a" transform="translate(154 140)"/>
+<use xlink:href="#a" transform="translate(154 154)"/>
+<use xlink:href="#a" transform="translate(154 168)"/>
+<use xlink:href="#a" transform="translate(154 182)"/>
+<use xlink:href="#a" transform="translate(168 98)"/>
+<use xlink:href="#a" transform="translate(168 112)"/>
+<use xlink:href="#a" transform="translate(168 126)"/>
+<use xlink:href="#a" transform="translate(168 140)"/>
+<use xlink:href="#a" transform="translate(168 154)"/>
+<use xlink:href="#a" transform="translate(168 168)"/>
+<g id="b" transform="translate(126 98)">
+<rect height="14" width="14" fill="#9a9298"/>
+<rect height="12" width="12" y="1" x="1" fill="#696267"/>
+<path d="m0.5 13.5v-13h13l-0.5 0.5h-12v12z" fill="#5a5458"/>
+<path d="m0.5 13.5h13v-13l-0.5 0.5v12h-12z" fill="#797276"/>
+</g>
+<use xlink:href="#b" transform="translate(-28,28)"/>
+<use xlink:href="#b" transform="translate(-28,42)"/>
+<use xlink:href="#b" transform="translate(-14,14)"/>
+<use xlink:href="#b" transform="translate(-14,28)"/>
+<use xlink:href="#b" transform="translate(-14,42)"/>
+<use xlink:href="#b" transform="translate(-14,56)"/>
+<use xlink:href="#b" transform="translate(0,14)"/>
+<use xlink:href="#b" transform="translate(0,28)"/>
+<use xlink:href="#b" transform="translate(0,42)"/>
+<use xlink:href="#b" transform="translate(0,56)"/>
+<use xlink:href="#b" transform="translate(0,70)"/>
+<use xlink:href="#b" transform="translate(14)"/>
+<use xlink:href="#b" transform="translate(14,14)"/>
+<use xlink:href="#b" transform="translate(14,28)"/>
+<use xlink:href="#b" transform="translate(14,42)"/>
+<use xlink:href="#b" transform="translate(14,56)"/>
+<use xlink:href="#b" transform="translate(14,70)"/>
+<use xlink:href="#b" transform="translate(28,14)"/>
+<use xlink:href="#b" transform="translate(28,28)"/>
+<use xlink:href="#b" transform="translate(28,42)"/>
+<use xlink:href="#b" transform="translate(28,56)"/>
+<use xlink:href="#b" transform="translate(42,28)"/>
+<use xlink:href="#b" transform="translate(42,42)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="14" width="14" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="14" height="14" fill="#9a9298"/>
+<path d="m14 0h-14v14l0.7-0.7v-12.6h12.6z" fill="#767074"/>
+<path d="m14 0-0.7 0.7v12.6h-12.6l-0.7 0.7h14z" fill="#b2acb0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="28" width="28" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="28" height="28" fill="#9a9298"/>
+<g id="a">
+<path d="m0 28v-21h7l-1 1h-5v19z" fill="#726c70"/>
+<path d="m0 28 1-1h5v-19l1-1v21z" fill="#b2acb0"/>
+</g>
+<use xlink:href="#a" transform="matrix(0,1,1,0,0,0)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="320" width="320" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m0 160 80-160h160l80 160-80 160h-160z" fill="#9a9298"/>
+<g id="c">
+<path d="m70 20 1.732-1.155l8.268-16.845v-2z" fill="#767074"/>
+<path d="m70 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#B2ACB0"/>
+</g>
+<g id="d">
+<path d="m100 0-1.732 1.155l-8.268 16.845v2z" fill="#B2ACB0"/>
+<path d="m100 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#767074"/>
+</g>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="140"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="360" width="360" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m90 0-90 180 90 180h180l90-180-90-180z" fill="#9a9298"/>
+<g id="c">
+<path d="m80 20 1.732-1.155l8.268-16.845v-2z" fill="#767074"/>
+<path d="m80 20h20l-10-20v2l8.27 16.845h-16.538z" fill="#B2ACB0"/>
+</g>
+<g id="d">
+<path d="m110 0-1.732 1.155l-8.268 16.845v2z" fill="#B2ACB0"/>
+<path d="m110 0h-20l10 20v-2l-8.27-16.845h16.538z" fill="#767074"/>
+</g>
+<use xlink:href="#d" x="-90" y="180"/>
+<use xlink:href="#d" x="-80" y="200"/>
+<use xlink:href="#d" x="-70" y="140"/>
+<use xlink:href="#d" x="-80" y="160"/>
+<use xlink:href="#d" x="-70" y="180"/>
+<use xlink:href="#d" x="-60" y="200"/>
+<use xlink:href="#d" x="-70" y="220"/>
+<use xlink:href="#d" x="-60" y="240"/>
+<use xlink:href="#d" x="-50" y="100"/>
+<use xlink:href="#d" x="-60" y="120"/>
+<use xlink:href="#d" x="-50" y="140"/>
+<use xlink:href="#d" x="-60" y="160"/>
+<use xlink:href="#d" x="-50" y="180"/>
+<use xlink:href="#d" x="-40" y="200"/>
+<use xlink:href="#d" x="-50" y="220"/>
+<use xlink:href="#d" x="-40" y="240"/>
+<use xlink:href="#d" x="-50" y="260"/>
+<use xlink:href="#d" x="-40" y="280"/>
+<use xlink:href="#d" x="-30" y="60"/>
+<use xlink:href="#d" x="-40" y="80"/>
+<use xlink:href="#d" x="-30" y="100"/>
+<use xlink:href="#d" x="-40" y="120"/>
+<use xlink:href="#d" x="-30" y="140"/>
+<use xlink:href="#d" x="-40" y="160"/>
+<use xlink:href="#d" x="-30" y="180"/>
+<use xlink:href="#d" x="-20" y="200"/>
+<use xlink:href="#d" x="-30" y="220"/>
+<use xlink:href="#d" x="-20" y="240"/>
+<use xlink:href="#d" x="-30" y="260"/>
+<use xlink:href="#d" x="-20" y="280"/>
+<use xlink:href="#d" x="-30" y="300"/>
+<use xlink:href="#d" x="-20" y="320"/>
+<use xlink:href="#d" x="-10" y="20"/>
+<use xlink:href="#d" x="-20" y="40"/>
+<use xlink:href="#d" x="-10" y="60"/>
+<use xlink:href="#d" x="-20" y="80"/>
+<use xlink:href="#d" x="-10" y="100"/>
+<use xlink:href="#d" x="-20" y="120"/>
+<use xlink:href="#d" x="-10" y="140"/>
+<use xlink:href="#d" x="-20" y="160"/>
+<use xlink:href="#d" x="-10" y="180"/>
+<use xlink:href="#d" y="200"/>
+<use xlink:href="#d" x="-10" y="220"/>
+<use xlink:href="#d" y="240"/>
+<use xlink:href="#d" x="-10" y="260"/>
+<use xlink:href="#d" y="280"/>
+<use xlink:href="#d" x="-10" y="300"/>
+<use xlink:href="#d" y="320"/>
+<use xlink:href="#d" x="-10" y="340"/>
+<use xlink:href="#d" x="10" y="20"/>
+<use xlink:href="#d" y="40"/>
+<use xlink:href="#d" x="10" y="60"/>
+<use xlink:href="#d" y="80"/>
+<use xlink:href="#d" x="10" y="100"/>
+<use xlink:href="#d" y="120"/>
+<use xlink:href="#d" x="10" y="140"/>
+<use xlink:href="#d" y="160"/>
+<use xlink:href="#d" x="10" y="180"/>
+<use xlink:href="#d" x="20" y="200"/>
+<use xlink:href="#d" x="10" y="220"/>
+<use xlink:href="#d" x="20" y="240"/>
+<use xlink:href="#d" x="10" y="260"/>
+<use xlink:href="#d" x="20" y="280"/>
+<use xlink:href="#d" x="10" y="300"/>
+<use xlink:href="#d" x="20" y="320"/>
+<use xlink:href="#d" x="10" y="340"/>
+<use xlink:href="#d" x="20"/>
+<use xlink:href="#d" x="30" y="20"/>
+<use xlink:href="#d" x="20" y="40"/>
+<use xlink:href="#d" x="30" y="60"/>
+<use xlink:href="#d" x="20" y="80"/>
+<use xlink:href="#d" x="30" y="100"/>
+<use xlink:href="#d" x="20" y="120"/>
+<use xlink:href="#d" x="30" y="140"/>
+<use xlink:href="#d" x="20" y="160"/>
+<use xlink:href="#d" x="30" y="180"/>
+<use xlink:href="#d" x="40" y="200"/>
+<use xlink:href="#d" x="30" y="220"/>
+<use xlink:href="#d" x="40" y="240"/>
+<use xlink:href="#d" x="30" y="260"/>
+<use xlink:href="#d" x="40" y="280"/>
+<use xlink:href="#d" x="30" y="300"/>
+<use xlink:href="#d" x="40" y="320"/>
+<use xlink:href="#d" x="30" y="340"/>
+<use xlink:href="#d" x="40"/>
+<use xlink:href="#d" x="50" y="20"/>
+<use xlink:href="#d" x="40" y="40"/>
+<use xlink:href="#d" x="50" y="60"/>
+<use xlink:href="#d" x="40" y="80"/>
+<use xlink:href="#d" x="50" y="100"/>
+<use xlink:href="#d" x="40" y="120"/>
+<use xlink:href="#d" x="50" y="140"/>
+<use xlink:href="#d" x="40" y="160"/>
+<use xlink:href="#d" x="50" y="180"/>
+<use xlink:href="#d" x="60" y="200"/>
+<use xlink:href="#d" x="50" y="220"/>
+<use xlink:href="#d" x="60" y="240"/>
+<use xlink:href="#d" x="50" y="260"/>
+<use xlink:href="#d" x="60" y="280"/>
+<use xlink:href="#d" x="50" y="300"/>
+<use xlink:href="#d" x="60" y="320"/>
+<use xlink:href="#d" x="50" y="340"/>
+<use xlink:href="#d" x="60"/>
+<use xlink:href="#d" x="70" y="20"/>
+<use xlink:href="#d" x="60" y="40"/>
+<use xlink:href="#d" x="70" y="60"/>
+<use xlink:href="#d" x="60" y="80"/>
+<use xlink:href="#d" x="70" y="100"/>
+<use xlink:href="#d" x="60" y="120"/>
+<use xlink:href="#d" x="70" y="140"/>
+<use xlink:href="#d" x="60" y="160"/>
+<use xlink:href="#d" x="70" y="180"/>
+<use xlink:href="#d" x="80" y="200"/>
+<use xlink:href="#d" x="70" y="220"/>
+<use xlink:href="#d" x="80" y="240"/>
+<use xlink:href="#d" x="70" y="260"/>
+<use xlink:href="#d" x="80" y="280"/>
+<use xlink:href="#d" x="70" y="300"/>
+<use xlink:href="#d" x="80" y="320"/>
+<use xlink:href="#d" x="70" y="340"/>
+<use xlink:href="#d" x="80"/>
+<use xlink:href="#d" x="90" y="20"/>
+<use xlink:href="#d" x="80" y="40"/>
+<use xlink:href="#d" x="90" y="60"/>
+<use xlink:href="#d" x="80" y="80"/>
+<use xlink:href="#d" x="90" y="100"/>
+<use xlink:href="#d" x="80" y="120"/>
+<use xlink:href="#d" x="90" y="140"/>
+<use xlink:href="#d" x="80" y="160"/>
+<use xlink:href="#d" x="90" y="180"/>
+<use xlink:href="#d" x="100" y="200"/>
+<use xlink:href="#d" x="90" y="220"/>
+<use xlink:href="#d" x="100" y="240"/>
+<use xlink:href="#d" x="90" y="260"/>
+<use xlink:href="#d" x="100" y="280"/>
+<use xlink:href="#d" x="90" y="300"/>
+<use xlink:href="#d" x="100" y="320"/>
+<use xlink:href="#d" x="90" y="340"/>
+<use xlink:href="#d" x="100"/>
+<use xlink:href="#d" x="110" y="20"/>
+<use xlink:href="#d" x="100" y="40"/>
+<use xlink:href="#d" x="110" y="60"/>
+<use xlink:href="#d" x="100" y="80"/>
+<use xlink:href="#d" x="110" y="100"/>
+<use xlink:href="#d" x="100" y="120"/>
+<use xlink:href="#d" x="110" y="140"/>
+<use xlink:href="#d" x="100" y="160"/>
+<use xlink:href="#d" x="110" y="180"/>
+<use xlink:href="#d" x="120" y="200"/>
+<use xlink:href="#d" x="110" y="220"/>
+<use xlink:href="#d" x="120" y="240"/>
+<use xlink:href="#d" x="110" y="260"/>
+<use xlink:href="#d" x="120" y="280"/>
+<use xlink:href="#d" x="110" y="300"/>
+<use xlink:href="#d" x="120" y="320"/>
+<use xlink:href="#d" x="110" y="340"/>
+<use xlink:href="#d" x="120"/>
+<use xlink:href="#d" x="130" y="20"/>
+<use xlink:href="#d" x="120" y="40"/>
+<use xlink:href="#d" x="130" y="60"/>
+<use xlink:href="#d" x="120" y="80"/>
+<use xlink:href="#d" x="130" y="100"/>
+<use xlink:href="#d" x="120" y="120"/>
+<use xlink:href="#d" x="130" y="140"/>
+<use xlink:href="#d" x="120" y="160"/>
+<use xlink:href="#d" x="130" y="180"/>
+<use xlink:href="#d" x="140" y="200"/>
+<use xlink:href="#d" x="130" y="220"/>
+<use xlink:href="#d" x="140" y="240"/>
+<use xlink:href="#d" x="130" y="260"/>
+<use xlink:href="#d" x="140" y="280"/>
+<use xlink:href="#d" x="130" y="300"/>
+<use xlink:href="#d" x="140" y="320"/>
+<use xlink:href="#d" x="130" y="340"/>
+<use xlink:href="#d" x="140"/>
+<use xlink:href="#d" x="150" y="20"/>
+<use xlink:href="#d" x="140" y="40"/>
+<use xlink:href="#d" x="150" y="60"/>
+<use xlink:href="#d" x="140" y="80"/>
+<use xlink:href="#d" x="150" y="100"/>
+<use xlink:href="#d" x="140" y="120"/>
+<use xlink:href="#d" x="150" y="140"/>
+<use xlink:href="#d" x="140" y="160"/>
+<use xlink:href="#d" x="150" y="180"/>
+<use xlink:href="#d" x="160" y="200"/>
+<use xlink:href="#d" x="150" y="220"/>
+<use xlink:href="#d" x="160" y="240"/>
+<use xlink:href="#d" x="150" y="260"/>
+<use xlink:href="#d" x="160" y="280"/>
+<use xlink:href="#d" x="150" y="300"/>
+<use xlink:href="#d" x="160" y="320"/>
+<use xlink:href="#d" x="150" y="340"/>
+<use xlink:href="#d" x="160"/>
+<use xlink:href="#d" x="170" y="20"/>
+<use xlink:href="#d" x="160" y="40"/>
+<use xlink:href="#d" x="170" y="60"/>
+<use xlink:href="#d" x="160" y="80"/>
+<use xlink:href="#d" x="170" y="100"/>
+<use xlink:href="#d" x="160" y="120"/>
+<use xlink:href="#d" x="170" y="140"/>
+<use xlink:href="#d" x="160" y="160"/>
+<use xlink:href="#d" x="170" y="180"/>
+<use xlink:href="#d" x="180" y="200"/>
+<use xlink:href="#d" x="170" y="220"/>
+<use xlink:href="#d" x="180" y="240"/>
+<use xlink:href="#d" x="170" y="260"/>
+<use xlink:href="#d" x="180" y="280"/>
+<use xlink:href="#d" x="170" y="300"/>
+<use xlink:href="#d" x="180" y="320"/>
+<use xlink:href="#d" x="170" y="340"/>
+<use xlink:href="#d" x="180" y="40"/>
+<use xlink:href="#d" x="190" y="60"/>
+<use xlink:href="#d" x="180" y="80"/>
+<use xlink:href="#d" x="190" y="100"/>
+<use xlink:href="#d" x="180" y="120"/>
+<use xlink:href="#d" x="190" y="140"/>
+<use xlink:href="#d" x="180" y="160"/>
+<use xlink:href="#d" x="190" y="180"/>
+<use xlink:href="#d" x="200" y="200"/>
+<use xlink:href="#d" x="190" y="220"/>
+<use xlink:href="#d" x="200" y="240"/>
+<use xlink:href="#d" x="190" y="260"/>
+<use xlink:href="#d" x="200" y="280"/>
+<use xlink:href="#d" x="190" y="300"/>
+<use xlink:href="#d" x="200" y="80"/>
+<use xlink:href="#d" x="210" y="100"/>
+<use xlink:href="#d" x="200" y="120"/>
+<use xlink:href="#d" x="210" y="140"/>
+<use xlink:href="#d" x="200" y="160"/>
+<use xlink:href="#d" x="210" y="180"/>
+<use xlink:href="#d" x="220" y="200"/>
+<use xlink:href="#d" x="210" y="220"/>
+<use xlink:href="#d" x="220" y="240"/>
+<use xlink:href="#d" x="210" y="260"/>
+<use xlink:href="#d" x="220" y="120"/>
+<use xlink:href="#d" x="230" y="140"/>
+<use xlink:href="#d" x="220" y="160"/>
+<use xlink:href="#d" x="230" y="180"/>
+<use xlink:href="#d" x="240" y="200"/>
+<use xlink:href="#d" x="230" y="220"/>
+<use xlink:href="#d" x="240" y="160"/>
+<use xlink:href="#d" x="250" y="180"/>
+<use xlink:href="#c" x="-70" y="140"/>
+<use xlink:href="#c" x="-80" y="160"/>
+<use xlink:href="#c" x="-70" y="180"/>
+<use xlink:href="#c" x="-60" y="200"/>
+<use xlink:href="#c" x="-50" y="100"/>
+<use xlink:href="#c" x="-60" y="120"/>
+<use xlink:href="#c" x="-50" y="140"/>
+<use xlink:href="#c" x="-60" y="160"/>
+<use xlink:href="#c" x="-50" y="180"/>
+<use xlink:href="#c" x="-40" y="200"/>
+<use xlink:href="#c" x="-50" y="220"/>
+<use xlink:href="#c" x="-40" y="240"/>
+<use xlink:href="#c" x="-30" y="60"/>
+<use xlink:href="#c" x="-40" y="80"/>
+<use xlink:href="#c" x="-30" y="100"/>
+<use xlink:href="#c" x="-40" y="120"/>
+<use xlink:href="#c" x="-30" y="140"/>
+<use xlink:href="#c" x="-40" y="160"/>
+<use xlink:href="#c" x="-30" y="180"/>
+<use xlink:href="#c" x="-20" y="200"/>
+<use xlink:href="#c" x="-30" y="220"/>
+<use xlink:href="#c" x="-20" y="240"/>
+<use xlink:href="#c" x="-30" y="260"/>
+<use xlink:href="#c" x="-20" y="280"/>
+<use xlink:href="#c" x="-10" y="20"/>
+<use xlink:href="#c" x="-20" y="40"/>
+<use xlink:href="#c" x="-10" y="60"/>
+<use xlink:href="#c" x="-20" y="80"/>
+<use xlink:href="#c" x="-10" y="100"/>
+<use xlink:href="#c" x="-20" y="120"/>
+<use xlink:href="#c" x="-10" y="140"/>
+<use xlink:href="#c" x="-20" y="160"/>
+<use xlink:href="#c" x="-10" y="180"/>
+<use xlink:href="#c" y="200"/>
+<use xlink:href="#c" x="-10" y="220"/>
+<use xlink:href="#c" y="240"/>
+<use xlink:href="#c" x="-10" y="260"/>
+<use xlink:href="#c" y="280"/>
+<use xlink:href="#c" x="-10" y="300"/>
+<use xlink:href="#c" y="320"/>
+<use xlink:href="#c" x="10" y="20"/>
+<use xlink:href="#c" y="40"/>
+<use xlink:href="#c" x="10" y="60"/>
+<use xlink:href="#c" y="80"/>
+<use xlink:href="#c" x="10" y="100"/>
+<use xlink:href="#c" y="120"/>
+<use xlink:href="#c" x="10" y="140"/>
+<use xlink:href="#c" y="160"/>
+<use xlink:href="#c" x="10" y="180"/>
+<use xlink:href="#c" x="20" y="200"/>
+<use xlink:href="#c" x="10" y="220"/>
+<use xlink:href="#c" x="20" y="240"/>
+<use xlink:href="#c" x="10" y="260"/>
+<use xlink:href="#c" x="20" y="280"/>
+<use xlink:href="#c" x="10" y="300"/>
+<use xlink:href="#c" x="20" y="320"/>
+<use xlink:href="#c" x="10" y="340"/>
+<use xlink:href="#c" x="20"/>
+<use xlink:href="#c" x="30" y="20"/>
+<use xlink:href="#c" x="20" y="40"/>
+<use xlink:href="#c" x="30" y="60"/>
+<use xlink:href="#c" x="20" y="80"/>
+<use xlink:href="#c" x="30" y="100"/>
+<use xlink:href="#c" x="20" y="120"/>
+<use xlink:href="#c" x="30" y="140"/>
+<use xlink:href="#c" x="20" y="160"/>
+<use xlink:href="#c" x="30" y="180"/>
+<use xlink:href="#c" x="40" y="200"/>
+<use xlink:href="#c" x="30" y="220"/>
+<use xlink:href="#c" x="40" y="240"/>
+<use xlink:href="#c" x="30" y="260"/>
+<use xlink:href="#c" x="40" y="280"/>
+<use xlink:href="#c" x="30" y="300"/>
+<use xlink:href="#c" x="40" y="320"/>
+<use xlink:href="#c" x="30" y="340"/>
+<use xlink:href="#c" x="40"/>
+<use xlink:href="#c" x="50" y="20"/>
+<use xlink:href="#c" x="40" y="40"/>
+<use xlink:href="#c" x="50" y="60"/>
+<use xlink:href="#c" x="40" y="80"/>
+<use xlink:href="#c" x="50" y="100"/>
+<use xlink:href="#c" x="40" y="120"/>
+<use xlink:href="#c" x="50" y="140"/>
+<use xlink:href="#c" x="40" y="160"/>
+<use xlink:href="#c" x="50" y="180"/>
+<use xlink:href="#c" x="60" y="200"/>
+<use xlink:href="#c" x="50" y="220"/>
+<use xlink:href="#c" x="60" y="240"/>
+<use xlink:href="#c" x="50" y="260"/>
+<use xlink:href="#c" x="60" y="280"/>
+<use xlink:href="#c" x="50" y="300"/>
+<use xlink:href="#c" x="60" y="320"/>
+<use xlink:href="#c" x="50" y="340"/>
+<use xlink:href="#c" x="60"/>
+<use xlink:href="#c" x="70" y="20"/>
+<use xlink:href="#c" x="60" y="40"/>
+<use xlink:href="#c" x="70" y="60"/>
+<use xlink:href="#c" x="60" y="80"/>
+<use xlink:href="#c" x="70" y="100"/>
+<use xlink:href="#c" x="60" y="120"/>
+<use xlink:href="#c" x="70" y="140"/>
+<use xlink:href="#c" x="60" y="160"/>
+<use xlink:href="#c" x="70" y="180"/>
+<use xlink:href="#c" x="80" y="200"/>
+<use xlink:href="#c" x="70" y="220"/>
+<use xlink:href="#c" x="80" y="240"/>
+<use xlink:href="#c" x="70" y="260"/>
+<use xlink:href="#c" x="80" y="280"/>
+<use xlink:href="#c" x="70" y="300"/>
+<use xlink:href="#c" x="80" y="320"/>
+<use xlink:href="#c" x="70" y="340"/>
+<use xlink:href="#c" x="80"/>
+<use xlink:href="#c" x="90" y="20"/>
+<use xlink:href="#c" x="80" y="40"/>
+<use xlink:href="#c" x="90" y="60"/>
+<use xlink:href="#c" x="80" y="80"/>
+<use xlink:href="#c" x="90" y="100"/>
+<use xlink:href="#c" x="80" y="120"/>
+<use xlink:href="#c" x="90" y="140"/>
+<use xlink:href="#c" x="80" y="160"/>
+<use xlink:href="#c" x="90" y="180"/>
+<use xlink:href="#c" x="100" y="200"/>
+<use xlink:href="#c" x="90" y="220"/>
+<use xlink:href="#c" x="100" y="240"/>
+<use xlink:href="#c" x="90" y="260"/>
+<use xlink:href="#c" x="100" y="280"/>
+<use xlink:href="#c" x="90" y="300"/>
+<use xlink:href="#c" x="100" y="320"/>
+<use xlink:href="#c" x="90" y="340"/>
+<use xlink:href="#c" x="100"/>
+<use xlink:href="#c" x="110" y="20"/>
+<use xlink:href="#c" x="100" y="40"/>
+<use xlink:href="#c" x="110" y="60"/>
+<use xlink:href="#c" x="100" y="80"/>
+<use xlink:href="#c" x="110" y="100"/>
+<use xlink:href="#c" x="100" y="120"/>
+<use xlink:href="#c" x="110" y="140"/>
+<use xlink:href="#c" x="100" y="160"/>
+<use xlink:href="#c" x="110" y="180"/>
+<use xlink:href="#c" x="120" y="200"/>
+<use xlink:href="#c" x="110" y="220"/>
+<use xlink:href="#c" x="120" y="240"/>
+<use xlink:href="#c" x="110" y="260"/>
+<use xlink:href="#c" x="120" y="280"/>
+<use xlink:href="#c" x="110" y="300"/>
+<use xlink:href="#c" x="120" y="320"/>
+<use xlink:href="#c" x="110" y="340"/>
+<use xlink:href="#c" x="120"/>
+<use xlink:href="#c" x="130" y="20"/>
+<use xlink:href="#c" x="120" y="40"/>
+<use xlink:href="#c" x="130" y="60"/>
+<use xlink:href="#c" x="120" y="80"/>
+<use xlink:href="#c" x="130" y="100"/>
+<use xlink:href="#c" x="120" y="120"/>
+<use xlink:href="#c" x="130" y="140"/>
+<use xlink:href="#c" x="120" y="160"/>
+<use xlink:href="#c" x="130" y="180"/>
+<use xlink:href="#c" x="140" y="200"/>
+<use xlink:href="#c" x="130" y="220"/>
+<use xlink:href="#c" x="140" y="240"/>
+<use xlink:href="#c" x="130" y="260"/>
+<use xlink:href="#c" x="140" y="280"/>
+<use xlink:href="#c" x="130" y="300"/>
+<use xlink:href="#c" x="140" y="320"/>
+<use xlink:href="#c" x="130" y="340"/>
+<use xlink:href="#c" x="140"/>
+<use xlink:href="#c" x="150" y="20"/>
+<use xlink:href="#c" x="140" y="40"/>
+<use xlink:href="#c" x="150" y="60"/>
+<use xlink:href="#c" x="140" y="80"/>
+<use xlink:href="#c" x="150" y="100"/>
+<use xlink:href="#c" x="140" y="120"/>
+<use xlink:href="#c" x="150" y="140"/>
+<use xlink:href="#c" x="140" y="160"/>
+<use xlink:href="#c" x="150" y="180"/>
+<use xlink:href="#c" x="160" y="200"/>
+<use xlink:href="#c" x="150" y="220"/>
+<use xlink:href="#c" x="160" y="240"/>
+<use xlink:href="#c" x="150" y="260"/>
+<use xlink:href="#c" x="160" y="280"/>
+<use xlink:href="#c" x="150" y="300"/>
+<use xlink:href="#c" x="160" y="320"/>
+<use xlink:href="#c" x="150" y="340"/>
+<use xlink:href="#c" x="160"/>
+<use xlink:href="#c" x="170" y="20"/>
+<use xlink:href="#c" x="160" y="40"/>
+<use xlink:href="#c" x="170" y="60"/>
+<use xlink:href="#c" x="160" y="80"/>
+<use xlink:href="#c" x="170" y="100"/>
+<use xlink:href="#c" x="160" y="120"/>
+<use xlink:href="#c" x="170" y="140"/>
+<use xlink:href="#c" x="160" y="160"/>
+<use xlink:href="#c" x="170" y="180"/>
+<use xlink:href="#c" x="180" y="200"/>
+<use xlink:href="#c" x="170" y="220"/>
+<use xlink:href="#c" x="180" y="240"/>
+<use xlink:href="#c" x="170" y="260"/>
+<use xlink:href="#c" x="180" y="280"/>
+<use xlink:href="#c" x="170" y="300"/>
+<use xlink:href="#c" x="180" y="320"/>
+<use xlink:href="#c" x="170" y="340"/>
+<use xlink:href="#c" x="180"/>
+<use xlink:href="#c" x="190" y="20"/>
+<use xlink:href="#c" x="180" y="40"/>
+<use xlink:href="#c" x="190" y="60"/>
+<use xlink:href="#c" x="180" y="80"/>
+<use xlink:href="#c" x="190" y="100"/>
+<use xlink:href="#c" x="180" y="120"/>
+<use xlink:href="#c" x="190" y="140"/>
+<use xlink:href="#c" x="180" y="160"/>
+<use xlink:href="#c" x="190" y="180"/>
+<use xlink:href="#c" x="200" y="200"/>
+<use xlink:href="#c" x="190" y="220"/>
+<use xlink:href="#c" x="200" y="240"/>
+<use xlink:href="#c" x="190" y="260"/>
+<use xlink:href="#c" x="200" y="280"/>
+<use xlink:href="#c" x="190" y="300"/>
+<use xlink:href="#c" x="200" y="40"/>
+<use xlink:href="#c" x="210" y="60"/>
+<use xlink:href="#c" x="200" y="80"/>
+<use xlink:href="#c" x="210" y="100"/>
+<use xlink:href="#c" x="200" y="120"/>
+<use xlink:href="#c" x="210" y="140"/>
+<use xlink:href="#c" x="200" y="160"/>
+<use xlink:href="#c" x="210" y="180"/>
+<use xlink:href="#c" x="220" y="200"/>
+<use xlink:href="#c" x="210" y="220"/>
+<use xlink:href="#c" x="220" y="240"/>
+<use xlink:href="#c" x="210" y="260"/>
+<use xlink:href="#c" x="220" y="80"/>
+<use xlink:href="#c" x="230" y="100"/>
+<use xlink:href="#c" x="220" y="120"/>
+<use xlink:href="#c" x="230" y="140"/>
+<use xlink:href="#c" x="220" y="160"/>
+<use xlink:href="#c" x="230" y="180"/>
+<use xlink:href="#c" x="240" y="200"/>
+<use xlink:href="#c" x="230" y="220"/>
+<use xlink:href="#c" x="240" y="120"/>
+<use xlink:href="#c" x="250" y="140"/>
+<use xlink:href="#c" x="240" y="160"/>
+<use xlink:href="#c" x="250" y="180"/>
+<use xlink:href="#c" x="260" y="160"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#0073cf" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#004881" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#0e94ff"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#00c000" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#007800" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#00fa00"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#e63e2c" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#90261b" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#fa6253"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path fill="#ebcd23" d="m0 0v20h20v-20l-20-6.2e-7zm3.5 3.5h13v13h-13v-13z"/>
+<path fill="#938015" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#ffe658"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#0073cf"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#00c000"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#e63e2c"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="7" fill="#ebcd23"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#0073cf"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#00c000"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#e63e2c"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m7 7v-5l-5 5z" fill="#ebcd23"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#0073cf"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#00c000"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#e63e2c"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="4" width="7" y="1.5" fill="#ebcd23"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#0073cf"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#00c000"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#e63e2c"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="7" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="5" width="7" y="2" fill="#ebcd23"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="21" fill="#0073cf"/>
+<path fill="#004881" d="m0 7h21v-7l-1 1v5h-19z"/>
+<path fill="#0e94ff" d="m0 7 1-1v-5h19l1-1h-21z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect width="21" height="7" fill="#00c000"/>
+<path d="m0 7h21v-7l-1 1v5h-19z" fill="#007800"/>
+<path d="m0 7 1-1v-5h19l1-1h-21z" fill="#00fa00"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="21" fill="#e63e2c"/>
+<path d="m0 7h21v-7l-1 1v5h-19z" fill="#90261b"/>
+<path d="m0 7 1-1v-5h19l1-1h-21z" fill="#fa6253"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="7" width="21" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="7" width="21" fill="#ebcd23"/>
+<path d="m0 7h21v-7l-1 1v5h-19z" fill="#938015"/>
+<path d="m0 7 1-1v-5h19l1-1h-21z" fill="#ffe658"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="200" width="200" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m200 100a100 100 0 0 1 -200 0 100 100 0 1 1 200 0z" fill="#70716d"/>
+<path d="m160 100a60 60 0 0 1 -120 0 60 60 0 1 1 120 0z" fill="#fafafa"/>
+<g fill="#555753">
+<g id="c">
+<path fill="#fafafa" d="m40 100a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z"/>
+<path d="m20 87.5c6.9035 0 12.5 5.5964 12.5 12.5h-5.3571c0-3.9449-3.198-7.1429-7.1428-7.1429-3.9449 0-7.1429 3.198-7.1429 7.1429 0 2.0384 0.86101 3.8449 2.2322 5.1339l3.3482-3.3482v10.714h-10.714l3.5714-3.5714c-2.389-2.3238-3.7946-5.5105-3.7946-8.9286 0-6.9036 5.5964-12.5 12.5-12.5z"/>
+</g>
+<g id="d">
+<path fill="#fafafa" d="m120 180a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z"/>
+<path d="m95 170-10 10 10 10v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z"/>
+</g>
+<use xlink:href="#d" transform="matrix(0 1 -1 0 360 0)"/>
+<use xlink:href="#c" transform="matrix(-1 0 0 1 120 -80)"/>
+</g>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="200" width="200" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m200 100a100 100 0 0 1 -200 0 100 100 0 1 1 200 0z" fill="#70716d"/>
+<path d="m160 100a60 60 0 1 1 -120 0 60 60 0 1 1 120 0z" fill="#9c9c90"/>
+<g id="c">
+<path d="m40 100a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z" fill="#fafafa"/>
+<path d="m20 87.5c6.9035 0 12.5 5.5964 12.5 12.5h-5.3571c0-3.9449-3.198-7.1428-7.1428-7.1428-3.9449 0-7.1429 3.198-7.1429 7.1428 0 2.0384 0.86101 3.8449 2.2322 5.1339l3.3482-3.3482v10.714h-10.714l3.5714-3.5714c-2.389-2.3238-3.7946-5.5105-3.7946-8.9286 0-6.9036 5.5964-12.5 12.5-12.5z" fill="#555753"/>
+</g>
+<g id="d">
+<path d="m120 180a20 20 0 0 1 -40 0 20 20 0 1 1 40 0z" fill="#fafafa"/>
+<path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#555753"/>
+</g>
+<use xlink:href="#c" transform="matrix(-1 0 0 1 120 -80)"/>
+<use xlink:href="#d" transform="matrix(0 1 -1 0 360 0)"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#0073cf"/>
+<path fill="#004881" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path fill="#0e94ff" d="m0 20 1-1v-18h18l1-1h-20z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#00c000"/>
+<path d="m0 20h20v-20l-1 1v18h-18z" fill="#007800"/>
+<path d="m0 20 1-1v-18h18l1-1h-20z" fill="#00fa00"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#e63e2c"/>
+<path fill="#90261b" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path fill="#fa6253" d="m0 20 1-1v-18h18l1-1h-20z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<rect height="20" width="20" fill="#ebcd23"/>
+<path fill="#938015" d="m0 20h20v-20l-1 1v18h-18z"/>
+<path fill="#ffe658" d="m0 20 1-1v-18h18l1-1h-20z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#0073cf"/>
+<path d="m0 20 1.6-1 8.4-16.8v-2.2z" fill="#0e94ff"/>
+<path d="m0 20 1.6-1h16.8l1.6 1z" fill="#004881"/>
+<path d="m20 20-10-20v2.2l8.4 16.8z" fill="#0059a0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#0073cf"/>
+<path fill="#0080e6" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#0e94ff" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#004881" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#00c000"/>
+<path fill="#00d200" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#00fa00" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#007800" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#e63e2c"/>
+<path fill="#f93e2b" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#fa6253" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#90261b" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 20 10-20h-20z" fill="#ebcd23"/>
+<path fill="#ffe133" d="m0 0 1.6 0.96 8.4 16.8v2.2z"/>
+<path fill="#ffe658" d="m0 0 1.6 0.96h16.8l1.6-0.96z"/>
+<path fill="#938015" d="m20 0-10 20v-2.2l8.4-16.8z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#00c000"/>
+<path fill="#00fa00" d="m0 20 1.6-1 8.4-16.8v-2.2z"/>
+<path fill="#007800" d="m0 20 1.6-1h16.8l1.6 1z"/>
+<path fill="#009700" d="m20 20-10-20v2.2l8.4 16.8z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#e63e2c"/>
+<path fill="#fa6253" d="m0 20 1.6-1 8.4-16.8v-2.2z"/>
+<path fill="#90261b" d="m0 20 1.6-1h16.8l1.6 1z"/>
+<path fill="#bb3223" d="m20 20-10-20v2.2l8.4 16.8z"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="20" width="20" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<path d="m10 0 10 20h-20z" fill="#ebcd23"/>
+<path fill="#ffe658" d="m0 20 1.6-1 8.4-16.8v-2.2z"/>
+<path fill="#938015" d="m0 20 1.6-1h16.8l1.6 1z"/>
+<path fill="#b09919" d="m20 20-10-20v2.2l8.4 16.8z"/>
+</svg>
--- /dev/null
+<RCC>
+<qresource prefix="/qml/themes">
+<file>dark/board-callisto.svg</file>
+<file>dark/board-callisto-2.svg</file>
+<file>dark/board-callisto-3.svg</file>
+<file>dark/board-tile-classic.svg</file>
+<file>dark/board-tile-nexos.svg</file>
+<file>dark/board-trigon.svg</file>
+<file>dark/board-trigon-3.svg</file>
+<file>dark/Theme.qml</file>
+</qresource>
+</RCC>
--- /dev/null
+<RCC>
+<qresource prefix="/qml/themes">
+<file>light/board-callisto.svg</file>
+<file>light/board-callisto-2.svg</file>
+<file>light/board-callisto-3.svg</file>
+<file>light/board-tile-classic.svg</file>
+<file>light/board-tile-nexos.svg</file>
+<file>light/board-trigon.svg</file>
+<file>light/board-trigon-3.svg</file>
+<file>light/Theme.qml</file>
+</qresource>
+</RCC>
--- /dev/null
+<RCC>
+<qresource prefix="/qml/themes">
+<file>light/frame-blue.svg</file>
+<file>light/frame-green.svg</file>
+<file>light/frame-red.svg</file>
+<file>light/frame-yellow.svg</file>
+<file>light/junction-all-blue.svg</file>
+<file>light/junction-all-green.svg</file>
+<file>light/junction-all-red.svg</file>
+<file>light/junction-all-yellow.svg</file>
+<file>light/junction-rect-blue.svg</file>
+<file>light/junction-rect-green.svg</file>
+<file>light/junction-rect-red.svg</file>
+<file>light/junction-rect-yellow.svg</file>
+<file>light/junction-straight-blue.svg</file>
+<file>light/junction-straight-green.svg</file>
+<file>light/junction-straight-red.svg</file>
+<file>light/junction-straight-yellow.svg</file>
+<file>light/junction-t-blue.svg</file>
+<file>light/junction-t-green.svg</file>
+<file>light/junction-t-red.svg</file>
+<file>light/junction-t-yellow.svg</file>
+<file>light/linesegment-blue.svg</file>
+<file>light/linesegment-green.svg</file>
+<file>light/linesegment-red.svg</file>
+<file>light/linesegment-yellow.svg</file>
+<file>light/piece-manipulator-legal.svg</file>
+<file>light/piece-manipulator.svg</file>
+<file>light/square-blue.svg</file>
+<file>light/square-green.svg</file>
+<file>light/square-red.svg</file>
+<file>light/square-yellow.svg</file>
+<file>light/triangle-blue.svg</file>
+<file>light/triangle-down-blue.svg</file>
+<file>light/triangle-down-green.svg</file>
+<file>light/triangle-down-red.svg</file>
+<file>light/triangle-down-yellow.svg</file>
+<file>light/triangle-green.svg</file>
+<file>light/triangle-red.svg</file>
+<file>light/triangle-yellow.svg</file>
+</qresource>
+</RCC>
--- /dev/null
+<RCC>
+ <qresource prefix="/">
+ <file>qml/AndroidToolBar.qml</file>
+ <file>qml/AndroidToolButton.qml</file>
+ <file>qml/Board.qml</file>
+ <file>qml/Button.qml</file>
+ <file>qml/ComputerColorDialog.qml</file>
+ <file>qml/GameDisplay.qml</file>
+ <file>qml/LineSegment.qml</file>
+ <file>qml/Main.qml</file>
+ <file>qml/MenuComputer.qml</file>
+ <file>qml/MenuEdit.qml</file>
+ <file>qml/MenuGame.qml</file>
+ <file>qml/MenuGo.qml</file>
+ <file>qml/MenuHelp.qml</file>
+ <file>qml/MenuItemGameVariant.qml</file>
+ <file>qml/MenuItemLevel.qml</file>
+ <file>qml/MenuView.qml</file>
+ <file>qml/NavigationPanel.qml</file>
+ <file>qml/OpenDialog.qml</file>
+ <file>qml/PieceCallisto.qml</file>
+ <file>qml/PieceClassic.qml</file>
+ <file>qml/PieceFlipAnimation.qml</file>
+ <file>qml/PieceList.qml</file>
+ <file>qml/PieceManipulator.qml</file>
+ <file>qml/PieceNexos.qml</file>
+ <file>qml/PieceRotationAnimation.qml</file>
+ <file>qml/PieceSelector.qml</file>
+ <file>qml/PieceTrigon.qml</file>
+ <file>qml/SaveDialog.qml</file>
+ <file>qml/ScoreDisplay.qml</file>
+ <file>qml/ScoreElement.qml</file>
+ <file>qml/ScoreElement2.qml</file>
+ <file>qml/Square.qml</file>
+ <file>qml/ToolBar.qml</file>
+ <file>qml/Triangle.qml</file>
+ <file>qml/Main.js</file>
+ <file>qml/GameDisplay.js</file>
+ </qresource>
+</RCC>
--- /dev/null
+<RCC>
+ <qresource prefix="/">
+ <file>qml/i18n/qml_de.qm</file>
+ <file>qml/i18n/replace_qtbase_de.qm</file>
+ </qresource>
+</RCC>
--- /dev/null
+set(pentobi_thumbnailer_SRCS Main.cpp)
+
+add_executable(pentobi-thumbnailer Main.cpp)
+
+target_link_libraries(pentobi-thumbnailer
+ pentobi_thumbnail
+ pentobi_gui
+ pentobi_base
+ boardgame_base
+ boardgame_sgf
+ boardgame_util
+ boardgame_sys
+ )
+
+target_link_libraries(pentobi-thumbnailer Qt5::Widgets)
+
+install(TARGETS pentobi-thumbnailer DESTINATION ${CMAKE_INSTALL_BINDIR})
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_thumbnailer/Main.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <iostream>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QImage>
+#include <QImageWriter>
+#include <QString>
+#include "libpentobi_thumbnail/CreateThumbnail.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+ QCoreApplication app(argc, argv);
+ try
+ {
+ QCommandLineParser parser;
+ QCommandLineOption optionSize(QStringList() << "s" << "size",
+ "Generate image with height and width <size>.",
+ "size", "128");
+ parser.addOption(optionSize);
+ parser.process(app);
+ auto args = parser.positionalArguments();
+ bool ok;
+ int size = parser.value(optionSize).toInt(&ok);
+ if (! ok || size <= 0)
+ throw runtime_error("Invalid image size");
+ if (args.size() > 2)
+ throw runtime_error("Too many file arguments");
+ if (args.size() < 2)
+ throw runtime_error("Need input and output file argument");
+ QImage image(size, size, QImage::Format_ARGB32);
+ image.fill(Qt::transparent);
+ if (! createThumbnail(args.at(0), size, size, image))
+ throw runtime_error("Thumbnail generation failed");
+ QImageWriter writer(args.at(1), "png");
+ if (! writer.write(image))
+ throw runtime_error(writer.errorString().toLocal8Bit().constData());
+ }
+ catch (const exception& e)
+ {
+ cerr << e.what() << '\n';
+ return 1;
+ }
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/Analyze.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Analyze.h"
+
+#include <fstream>
+#include <map>
+#include <regex>
+#include "libboardgame_util/FmtSaver.h"
+#include "libboardgame_util/Statistics.h"
+#include "libboardgame_util/StringUtil.h"
+
+using libboardgame_util::from_string;
+using libboardgame_util::split;
+using libboardgame_util::trim;
+using libboardgame_util::FmtSaver;
+using libboardgame_util::Statistics;
+using libboardgame_util::StatisticsExt;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void write_result(const Statistics<>& stat)
+{
+ FmtSaver saver(cout);
+ cout << fixed << setprecision(1) << stat.get_mean() * 100 << "+-"
+ << stat.get_error() * 100;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+void analyze(const string& file)
+{
+ ifstream in(file);
+ Statistics<> stat_result;
+ map<unsigned, Statistics<>> stat_result_player;
+ map<float, unsigned> result_count;
+ StatisticsExt<> stat_length;
+ StatisticsExt<> stat_cpu_b;
+ StatisticsExt<> stat_cpu_w;
+ StatisticsExt<> stat_fast_open;
+ string line;
+ while (getline(in, line))
+ {
+ line = trim(line);
+ if (! line.empty() && line[0] == '#')
+ continue;
+ auto columns = split(line, '\t');
+ if (columns.empty())
+ continue;
+ float result;
+ unsigned length;
+ unsigned player;
+ float cpu_b;
+ float cpu_w;
+ unsigned fast_open;
+ if (columns.size() != 7
+ || ! from_string(columns[1], result)
+ || ! from_string(columns[2], length)
+ || ! from_string(columns[3], player)
+ || ! from_string(columns[4], cpu_b)
+ || ! from_string(columns[5], cpu_w)
+ || ! from_string(columns[6], fast_open))
+ throw runtime_error("invalid format");
+ stat_result.add(result);
+ stat_result_player[player].add(result);
+ ++result_count[result];
+ stat_length.add(length);
+ stat_cpu_b.add(cpu_b);
+ stat_cpu_w.add(cpu_w);
+ stat_fast_open.add(fast_open);
+ }
+ auto count = stat_result.get_count();
+ cout << "Gam: " << count;
+ if (count == 0)
+ {
+ cout << '\n';
+ return;
+ }
+ cout << ", Res: ";
+ write_result(stat_result);
+ cout << " (";
+ bool is_first = true;
+ for (auto& i : stat_result_player)
+ {
+ if (! is_first)
+ cout << ", ";
+ else
+ is_first = false;
+ cout << i.first << ": ";
+ write_result(i.second);
+ }
+ cout << ")\nResFreq:";
+ for (auto& i : result_count)
+ {
+ cout << ' ' << i.first << "=";
+ {
+ FmtSaver saver(cout);
+ auto fraction = i.second / count;
+ cout << fixed << setprecision(1) << fraction * 100
+ << "+-" << sqrt(fraction * (1 - fraction) / count) * 100;
+ }
+ }
+ cout << "\nCpuB: ";
+ stat_cpu_b.write(cout, true, 3, false, true);
+ cout << "\nCpuW: ";
+ stat_cpu_w.write(cout, true, 3, false, true);
+ auto cpu_b = stat_cpu_b.get_mean();
+ auto cpu_w = stat_cpu_w.get_mean();
+ auto err_cpu_b = stat_cpu_b.get_error();
+ auto err_cpu_w = stat_cpu_w.get_error();
+ cout << "\nCpuB/CpuW: ";
+ if (cpu_b > 0 && cpu_w > 0)
+ cout << fixed << setprecision(3) << cpu_b / cpu_w << "+-"
+ << cpu_b / cpu_w * hypot(err_cpu_b / cpu_b, err_cpu_w / cpu_w);
+ else
+ cout << "-";
+ cout << ", Len: ";
+ stat_length.write(cout, true, 1, true, true);
+ if (stat_fast_open.get_mean() > 0)
+ {
+ cout << ", Fast: ";
+ stat_fast_open.write(cout, true, 1, true, true);
+ }
+ cout << '\n';
+}
+
+void splitsgf(const string& file)
+{
+ ifstream in(file);
+ string filename;
+ string buffer;
+ regex pattern("GN\\[(\\d+)\\]");
+ string line;
+ while (getline(in, line))
+ {
+ if (trim(line) == "(")
+ {
+ if (! filename.empty())
+ {
+ ofstream out(filename);
+ out << buffer;
+ }
+ buffer.clear();
+ }
+ else
+ {
+ smatch match;
+ regex_search(line, match, pattern);
+ if (! match.empty())
+ filename = string(match[1]) + ".blksgf";
+ }
+ buffer.append(line);
+ buffer.append("\n");
+ }
+ if (! filename.empty())
+ {
+ ofstream out(filename);
+ out << buffer;
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/Analyze.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef TWOGTP_ANALYZE_H
+#define TWOGTP_ANALYZE_H
+
+#include <string>
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+void analyze(const string& file);
+
+void splitsgf(const string& file);
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_ANALYZE_H
--- /dev/null
+add_executable(twogtp
+ Analyze.h
+ Analyze.cpp
+ FdStream.h
+ FdStream.cpp
+ GtpConnection.h
+ GtpConnection.cpp
+ Main.cpp
+ Output.h
+ Output.cpp
+ OutputTree.h
+ OutputTree.cpp
+ TwoGtp.h
+ TwoGtp.cpp
+)
+
+target_link_libraries(twogtp
+ pentobi_base
+ boardgame_sgf
+ boardgame_base
+ boardgame_util
+ boardgame_sys
+)
+
+if(CMAKE_THREAD_LIBS_INIT)
+ target_link_libraries(twogtp ${CMAKE_THREAD_LIBS_INIT})
+endif()
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/FdStream.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "FdStream.h"
+
+#include <cstring>
+#include <unistd.h>
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const size_t put_back = 1;
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+FdInBuf::FdInBuf(int fd, size_t buf_size)
+ : m_fd(fd),
+ m_buf(buf_size + put_back)
+{
+ auto end = &(*m_buf.end());
+ setg(end, end, end);
+}
+
+FdInBuf::~FdInBuf() = default;
+
+auto FdInBuf::underflow() -> int_type
+{
+ if (gptr() < egptr())
+ return traits_type::to_int_type(*gptr());
+ auto base = &m_buf.front();
+ auto start = base;
+ if (eback() == base)
+ {
+ memmove(base, egptr() - put_back, put_back);
+ start += put_back;
+ }
+ auto n = read(m_fd, start, m_buf.size() - (start - base));
+ if (n <= 0)
+ return traits_type::eof();
+ setg(base, start, start + n);
+ return traits_type::to_int_type(*gptr());
+}
+
+//-----------------------------------------------------------------------------
+
+FdInStream::FdInStream(int fd)
+ : istream(nullptr),
+ m_buf(fd)
+{
+ rdbuf(&m_buf);
+}
+
+//-----------------------------------------------------------------------------
+
+FdOutBuf::~FdOutBuf() = default;
+
+auto FdOutBuf::overflow(int_type c) -> int_type
+{
+ if (c != traits_type::eof())
+ {
+ char buffer[1];
+ buffer[0] = static_cast<char>(c);
+ if (write(m_fd, buffer, 1) != 1)
+ return traits_type::eof();
+ }
+ return c;
+}
+
+streamsize FdOutBuf::xsputn(const char_type* s, streamsize count)
+{
+ return write(m_fd, s, count);
+}
+
+//-----------------------------------------------------------------------------
+
+FdOutStream::FdOutStream(int fd)
+ : ostream(nullptr),
+ m_buf(fd)
+{
+ rdbuf(&m_buf);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/FdStream.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef TWOGTP_FDSTREAM_H
+#define TWOGTP_FDSTREAM_H
+
+#include <iostream>
+#include <vector>
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Input stream buffer from a file descriptor. */
+class FdInBuf
+ : public streambuf
+{
+public:
+ FdInBuf(int fd, size_t buf_size = 1024);
+
+ ~FdInBuf();
+
+protected:
+ int_type underflow() override;
+
+private:
+ int m_fd;
+
+ vector<char_type> m_buf;
+};
+
+//-----------------------------------------------------------------------------
+
+/** Input stream from a file descriptor. */
+class FdInStream
+ : public istream
+{
+public:
+ explicit FdInStream(int fd);
+
+private:
+ FdInBuf m_buf;
+};
+
+//-----------------------------------------------------------------------------
+
+/** Output stream buffer from a file descriptor. */
+class FdOutBuf
+ : public streambuf
+{
+public:
+ explicit FdOutBuf(int fd)
+ : m_fd(fd)
+ { }
+
+ ~FdOutBuf();
+
+protected:
+ int_type overflow(int_type c) override;
+
+ streamsize xsputn(const char_type* s, streamsize count) override;
+
+private:
+ int m_fd;
+};
+
+//-----------------------------------------------------------------------------
+
+/** Output stream from a file descriptor. */
+class FdOutStream
+ : public ostream
+{
+public:
+ explicit FdOutStream(int fd);
+
+private:
+ FdOutBuf m_buf;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_FDSTREAM_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/GtpConnection.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "GtpConnection.h"
+
+#include <cstdlib>
+#include <cstring>
+#include <vector>
+#include <unistd.h>
+#include "FdStream.h"
+#include "libboardgame_util/Log.h"
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void terminate_child(const string& message)
+{
+ LIBBOARDGAME_LOG(message);
+ exit(1);
+}
+
+vector<string> split_args(string s)
+{
+ vector<string> result;
+ bool escape = false;
+ bool is_in_string = false;
+ ostringstream token;
+ for (auto c : s)
+ {
+ if (c == '"' && ! escape)
+ {
+ if (is_in_string)
+ {
+ result.push_back(token.str());
+ token.str("");
+ }
+ is_in_string = ! is_in_string;
+ }
+ else if (isspace(c) && ! is_in_string)
+ {
+ if (! token.str().empty())
+ {
+ result.push_back(token.str());
+ token.str("");
+ }
+ }
+ else
+ token << c;
+ escape = (c == '\\' && ! escape);
+ }
+ if (! token.str().empty())
+ result.push_back(token.str());
+ return result;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+GtpConnection::GtpConnection(const string& command)
+{
+ vector<string> args = split_args(command);
+ if (args.size() == 0)
+ throw runtime_error("GtpConnection: empty command line");
+ int fd1[2];
+ if (pipe(fd1) < 0)
+ throw runtime_error("GtpConnection: pipe creation failed");
+ int fd2[2];
+ if (pipe(fd2) < 0)
+ {
+ close(fd1[0]);
+ close(fd1[1]);
+ throw runtime_error("GtpConnection: pipe creation failed");
+ }
+ pid_t pid;
+ if ((pid = fork()) < 0)
+ throw runtime_error("GtpConnection: fork failed");
+ else if (pid > 0) // Parent
+ {
+ close(fd1[0]);
+ close(fd2[1]);
+ m_in.reset(new FdInStream(fd2[0]));
+ m_out.reset(new FdOutStream(fd1[1]));
+ return;
+ }
+ else // Child
+ {
+ close(fd1[1]);
+ close(fd2[0]);
+ if (fd1[0] != STDIN_FILENO)
+ if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
+ {
+ close(fd1[0]);
+ terminate_child("GtpConnection: dup2 to stdin failed");
+ }
+ if (fd2[1] != STDOUT_FILENO)
+ if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
+ {
+ close(fd2[1]);
+ terminate_child("GtpConnection: dup2 to stdout failed");
+ }
+ auto const argv = new char*[args.size() + 1];
+ for (size_t i = 0; i < args.size(); ++i)
+ {
+ argv[i] = new char[args[i].size() + 1];
+ strcpy(argv[i], args[i].c_str());
+ }
+ argv[args.size()] = nullptr;
+ if (execvp(args[0].c_str(), argv) == -1)
+ terminate_child("Could not execute '" + command + "'");
+ }
+}
+
+GtpConnection::~GtpConnection() = default;
+
+void GtpConnection::enable_log(const string& prefix)
+{
+ m_quiet = false;
+ m_prefix = prefix;
+}
+
+string GtpConnection::send(const string& command)
+{
+ if (! m_quiet)
+ LIBBOARDGAME_LOG(m_prefix, ">> ", command);
+ *m_out << command << '\n';
+ m_out->flush();
+ if (! *m_out)
+ throw Failure("GtpConnection: write failure");
+ ostringstream response;
+ bool done = false;
+ bool is_first = true;
+ bool success = true;
+ while (! done)
+ {
+ string line;
+ getline(*m_in, line);
+ if (! *m_in)
+ throw Failure("GtpConnection: read failure");
+ if (! m_quiet && ! line.empty())
+ LIBBOARDGAME_LOG(m_prefix, "<< ", line);
+ if (is_first)
+ {
+ if (line.size() < 2 || (line[0] != '=' && line[0] != '?')
+ || line[1] != ' ')
+ throw Failure("GtpConnection: malformed response: '" + line
+ + "'");
+ if (line[0] == '?')
+ success = false;
+ line = line.substr(2);
+ response << line;
+ is_first = false;
+ }
+ else
+ {
+ if (line.empty())
+ done = true;
+ else
+ response << '\n' << line;
+ }
+ }
+ if (! success)
+ throw Failure(response.str());
+ return response.str();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/GtpConnection.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef TWOGTP_GTP_CONNECTION_H
+#define TWOGTP_GTP_CONNECTION_H
+
+#include <iosfwd>
+#include <memory>
+#include <string>
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Invokes a GTP engine in an external process. */
+class GtpConnection
+{
+public:
+ class Failure
+ : public runtime_error
+ {
+ using runtime_error::runtime_error;
+ };
+
+
+ explicit GtpConnection(const string& command);
+
+ ~GtpConnection();
+
+ void enable_log(const string& prefix = "");
+
+ /** Send a GTP command.
+ @param command The command.
+ @return The response if the command returns a success status.
+ @throws Failure If the command returns an error status. */
+ string send(const string& command);
+
+private:
+ bool m_quiet = true;
+
+ string m_prefix;
+
+ unique_ptr<istream> m_in;
+
+ unique_ptr<ostream> m_out;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_GTP_CONNECTION_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/Main.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <atomic>
+#include <thread>
+#include "Analyze.h"
+#include "TwoGtp.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/Options.h"
+#include "libpentobi_base/Variant.h"
+
+using namespace std;
+using libboardgame_util::to_string;
+using libboardgame_util::Options;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+ atomic<int> result(0);
+ try
+ {
+ vector<string> specs = {
+ "analyze:",
+ "black|b:",
+ "fastopen",
+ "file|f:",
+ "game|g:",
+ "nugames|n:",
+ "quiet",
+ "splitsgf:",
+ "threads:",
+ "tree",
+ "white|w:",
+ };
+ Options opt(argc, argv, specs);
+ if (opt.contains("analyze"))
+ {
+ analyze(opt.get("analyze"));
+ return 0;
+ }
+ if (opt.contains("splitsgf"))
+ {
+ splitsgf(opt.get("splitsgf"));
+ return 0;
+ }
+ auto black = opt.get("black");
+ auto white = opt.get("white");
+ auto prefix = opt.get("file", "output");
+ auto nu_games = opt.get<unsigned>("nugames", 1);
+ auto nu_threads = opt.get<unsigned>("threads", 1);
+ auto variant_string = opt.get("game", "classic");
+ bool quiet = opt.contains("quiet");
+ if (quiet)
+ libboardgame_util::disable_logging();
+ bool fast_open = opt.contains("fastopen");
+ bool create_tree = opt.contains("tree") || fast_open;
+ Variant variant;
+ if (! parse_variant_id(variant_string, variant))
+ throw runtime_error("invalid game variant " + variant_string);
+ Output output(variant, prefix, create_tree);
+ vector<shared_ptr<TwoGtp>> twogtp;
+ for (unsigned i = 0; i < nu_threads; ++i)
+ {
+ string log_prefix;
+ if (nu_threads > 1)
+ log_prefix = to_string(i + 1);
+ twogtp.push_back(make_shared<TwoGtp>(black, white, variant,
+ nu_games, output, quiet,
+ log_prefix, fast_open));
+ }
+ vector<thread> threads;
+ for (auto& i : twogtp)
+ threads.push_back(thread([&i, &result]()
+ {
+ try
+ {
+ i->run();
+ }
+ catch (const exception& e)
+ {
+ LIBBOARDGAME_LOG("Error: ", e.what());
+ result = 1;
+ }
+ }));
+ for (auto& t : threads)
+ t.join();
+ }
+ catch (const exception& e)
+ {
+ LIBBOARDGAME_LOG("Error: ", e.what());
+ result = 1;
+ }
+ return result;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/Output.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "Output.h"
+
+#include <cstdio>
+#include <fstream>
+#include <iomanip>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/file.h>
+#include "libboardgame_util/StringUtil.h"
+
+using libboardgame_util::from_string;
+using libboardgame_util::split;
+using libboardgame_util::trim;
+
+//-----------------------------------------------------------------------------
+
+Output::Output(Variant variant, const string& prefix, bool create_tree)
+ : m_create_tree(create_tree),
+ m_prefix(prefix),
+ m_output_tree(variant)
+{
+ m_lock_fd = creat((prefix + ".lock").c_str(), 0644);
+ if (m_lock_fd == -1)
+ throw runtime_error("Output: could not create lock file");
+ if (flock(m_lock_fd, LOCK_EX | LOCK_NB) == -1)
+ throw runtime_error("Output: twogtp already running");
+ ifstream in(prefix + ".dat");
+ if (! in)
+ return;
+ string line;
+ while (getline(in, line))
+ {
+ line = trim(line);
+ if (! line.empty() && line[0] == '#')
+ continue;
+ auto columns = split(line, '\t');
+ if (columns.empty())
+ continue;
+ unsigned game_number;
+ if (! from_string(columns[0], game_number))
+ throw runtime_error("Output: expected game number");
+ m_games.insert(make_pair(game_number, line));
+ }
+ while (m_games.count(m_next) != 0)
+ ++m_next;
+ if (check_sentinel())
+ remove((prefix + ".stop").c_str());
+ if (m_create_tree && m_next > 0)
+ m_output_tree.load(prefix + "-tree.blksgf");
+}
+
+Output::~Output()
+{
+ flock(m_lock_fd, LOCK_UN);
+ close(m_lock_fd);
+ remove((m_prefix + ".lock").c_str());
+}
+
+void Output::add_result(unsigned n, float result, const Board& bd,
+ unsigned player_black, double cpu_black,
+ double cpu_white, const string& sgf,
+ const array<bool, Board::max_game_moves>& is_real_move)
+{
+ lock_guard<mutex> lock(m_mutex);
+ unsigned nu_fast_open = 0;
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ if (! is_real_move[i])
+ ++nu_fast_open;
+ ostringstream line;
+ line << n << '\t'
+ << setprecision(4) << result << '\t'
+ << bd.get_nu_moves() << '\t'
+ << player_black << '\t'
+ << setprecision(5) << cpu_black << '\t'
+ << cpu_white << '\t'
+ << nu_fast_open;
+ m_games.insert(make_pair(n, line.str()));
+ {
+ ofstream out(m_prefix + ".dat");
+ out << "# Game\tResult\tLength\tPlayerB\tCpuB\tCpuW\tFast\n";
+ for (auto& i : m_games)
+ out << i.second << '\n';
+ }
+ {
+ ofstream out(m_prefix + ".blksgf", ios::app);
+ out << sgf;
+ }
+ if (m_create_tree)
+ {
+ m_output_tree.add_game(bd, player_black, result, is_real_move);
+ m_output_tree.save(m_prefix + "-tree.blksgf");
+ }
+}
+
+bool Output::check_sentinel()
+{
+ return ! ifstream(m_prefix + ".stop").fail();
+}
+
+bool Output::generate_fast_open_move(bool is_player_black, const Board& bd,
+ Color to_play, Move& mv)
+{
+ lock_guard<mutex> lock(m_mutex);
+ m_output_tree.generate_move(is_player_black, bd, to_play, mv);
+ return ! mv.is_null();
+}
+
+unsigned Output::get_next()
+{
+ lock_guard<mutex> lock(m_mutex);
+ unsigned n = m_next;
+ do
+ ++m_next;
+ while (m_games.count(m_next) != 0);
+ return n;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/Output.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef TWOGTP_OUTPUT_H
+#define TWOGTP_OUTPUT_H
+
+#include <string>
+#include <map>
+#include <mutex>
+#include "OutputTree.h"
+
+//-----------------------------------------------------------------------------
+
+/** Handles the output files of TwoGtp and their concurrent access. */
+class Output
+{
+public:
+ Output(Variant variant, const string& prefix, bool fastopen);
+
+ ~Output();
+
+ void add_result(unsigned n, float result, const Board& bd,
+ unsigned player_black, double cpu_black, double cpu_white,
+ const string& sgf,
+ const array<bool, Board::max_game_moves>& is_real_move);
+
+ unsigned get_next();
+
+ bool check_sentinel();
+
+ bool generate_fast_open_move(bool is_player_black, const Board& bd,
+ Color to_play, Move& mv);
+
+private:
+ bool m_create_tree;
+
+ unsigned m_next = 0;
+
+ int m_lock_fd;
+
+ string m_prefix;
+
+ mutex m_mutex;
+
+ map<unsigned, string> m_games;
+
+ OutputTree m_output_tree;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_OUTPUT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/OutputTree.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "OutputTree.h"
+
+#include <fstream>
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_sgf/TreeWriter.h"
+#include "libpentobi_base/BoardUtil.h"
+
+using libboardgame_sgf::SgfNode;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::TreeWriter;
+using libpentobi_base::get_transforms;
+using libpentobi_base::ColorMove;
+using libpentobi_base::MovePoints;
+using libpentobi_base::boardutil::get_transformed;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void add(PentobiTree& tree, const SgfNode& node, bool is_player_black,
+ bool is_real_move, float result)
+{
+ unsigned index = is_player_black ? 0 : 1;
+ array<unsigned, 2> count;
+ array<double, 2> avg_result;
+ array<unsigned, 2> real_count;
+ auto comment = tree.get_comment(node);
+ if (comment.empty())
+ {
+ count.fill(0);
+ avg_result.fill(0);
+ real_count.fill(0);
+ count[index] = 1;
+ real_count[index] = 1;
+ avg_result[index] = result;
+ }
+ else
+ {
+ istringstream in(comment);
+ in >> count[0] >> real_count[0] >> avg_result[0]
+ >> count[1] >> real_count[1] >> avg_result[1];
+ if (! in)
+ throw runtime_error("OutputTree: invalid comment: " + comment);
+ ++count[index];
+ avg_result[index] += (result - avg_result[index]) / count[index];
+ if (is_real_move)
+ ++real_count[index];
+ }
+ ostringstream out;
+ out.precision(numeric_limits<double>::digits10);
+ out << count[0] << ' ' << real_count[0] << ' ' << avg_result[0] << '\n'
+ << count[1] << ' ' << real_count[1] << ' ' << avg_result[1];
+ tree.set_comment(node, out.str());
+}
+
+bool compare_sequence(ArrayList<ColorMove, Board::max_game_moves>& s1,
+ ArrayList<ColorMove, Board::max_game_moves>& s2)
+{
+ LIBBOARDGAME_ASSERT(s1.size() == s2.size());
+ for (unsigned i = 0; i < s1.size(); ++i)
+ {
+ LIBBOARDGAME_ASSERT(s1[i].color == s2[i].color);
+ if (s1[i].move.to_int() < s2[i].move.to_int())
+ return true;
+ else if (s1[i].move.to_int() > s2[i].move.to_int())
+ return false;
+ }
+ return false;
+}
+
+unsigned get_real_count(PentobiTree& tree, const SgfNode& node,
+ bool is_player_black)
+{
+ unsigned index = is_player_black ? 0 : 1;
+ array<unsigned, 2> count;
+ array<double, 2> avg_result;
+ array<unsigned, 2> real_count;
+ auto comment = tree.get_comment(node);
+ istringstream in(comment);
+ in >> count[0] >> real_count[0] >> avg_result[0]
+ >> count[1] >> real_count[1] >> avg_result[1];
+ if (! in)
+ throw runtime_error("OutputTree: invalid comment: " + comment);
+ return real_count[index];
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+OutputTree::OutputTree(Variant variant)
+ : m_tree(variant)
+{
+ get_transforms(variant, m_transforms, m_inv_transforms);
+}
+
+OutputTree::~OutputTree()
+{
+}
+
+void OutputTree::add_game(const Board& bd, unsigned player_black, float result,
+ const array<bool, Board::max_game_moves>& is_real_move)
+{
+ if (bd.has_setup())
+ throw runtime_error("OutputTree: setup not supported");
+
+ // Find the canonical representation
+ ArrayList<ColorMove, Board::max_game_moves> sequence;
+ for (auto& transform : m_transforms)
+ {
+ ArrayList<ColorMove, Board::max_game_moves> s;
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ {
+ auto mv = bd.get_move(i);
+ s.push_back(ColorMove(mv.color,
+ get_transformed(bd, mv.move, *transform)));
+ }
+ if (sequence.empty() || compare_sequence(s, sequence))
+ sequence = s;
+ }
+
+ auto node = &m_tree.get_root();
+ add(m_tree, *node, player_black == 0, true, result);
+ unsigned nu_moves_3 = 0;
+ for (unsigned i = 0; i < sequence.size(); ++i)
+ {
+ unsigned player;
+ auto mv = sequence[i];
+ Color c = mv.color;
+ if (bd.get_variant() == Variant::classic_3 && c == Color(3))
+ {
+ player = nu_moves_3 % 3;
+ ++nu_moves_3;
+ }
+ else
+ player = c.to_int() % bd.get_nu_players();
+ auto child = m_tree.find_child_with_move(*node, mv);
+ if (! child)
+ {
+ child = &m_tree.create_new_child(*node);
+ m_tree.set_move(*child, mv);
+ add(m_tree, *child, player == player_black, true, result);
+ return;
+ }
+ add(m_tree, *child, player == player_black, is_real_move[i], result);
+ node = child;
+ }
+}
+
+void OutputTree::generate_move(bool is_player_black, const Board& bd,
+ Color to_play, Move& mv)
+{
+ bool play_real;
+ for (unsigned i = 0; i < m_transforms.size(); ++i)
+ {
+ generate_move(is_player_black, bd, to_play, *m_transforms[i],
+ *m_inv_transforms[i], mv, play_real);
+ if (play_real || ! mv.is_null())
+ break;
+ }
+}
+
+void OutputTree::generate_move(bool is_player_black, const Board& bd,
+ Color to_play, const PointTransform& transform,
+ const PointTransform& inv_transform, Move& mv,
+ bool& play_real)
+{
+ if (bd.has_setup())
+ throw runtime_error("OutputTree: setup not supported");
+ play_real = false;
+ mv = Move::null();
+ auto node = &m_tree.get_root();
+ for (unsigned i = 0; i < bd.get_nu_moves(); ++i)
+ {
+ auto mv = bd.get_move(i);
+ ColorMove transformed_mv(mv.color,
+ get_transformed(bd, mv.move, transform));
+ auto child = m_tree.find_child_with_move(*node, transformed_mv);
+ if (! child)
+ return;
+ node = child;
+ }
+ unsigned sum = 0;
+ for (auto& i : node->get_children())
+ sum += get_real_count(m_tree, i, is_player_black);
+ if (sum == 0)
+ return;
+ uniform_real_distribution<double> distribution(0, 1);
+ if (distribution(m_random) < 1.0 / sum)
+ {
+ play_real = true;
+ return;
+ }
+ unsigned random = static_cast<unsigned>(distribution(m_random) * sum);
+ sum = 0;
+ for (auto& i : node->get_children())
+ {
+ auto real_count = get_real_count(m_tree, i, is_player_black);
+ if (real_count == 0)
+ continue;
+ sum += real_count;
+ if (sum >= random)
+ {
+ auto color_mv = m_tree.get_move(i);
+ if (color_mv.is_null())
+ throw runtime_error("OutputTree: tree has node without move");
+ if (color_mv.color != to_play)
+ throw runtime_error("OutputTree: tree has node wrong move color");
+ mv = get_transformed(bd, color_mv.move, inv_transform);
+ return;
+ }
+ }
+ LIBBOARDGAME_ASSERT(false);
+}
+
+void OutputTree::load(const string& file)
+{
+ TreeReader reader;
+ reader.read(file);
+ auto tree = reader.get_tree_transfer_ownership();
+ m_tree.init(tree);
+}
+
+void OutputTree::save(const string& file)
+{
+ ofstream out(file);
+ TreeWriter writer(out, m_tree.get_root());
+ writer.write();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/OutputTree.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef TWOGTP_OUTPUT_TREE_H
+#define TWOGTP_OUTPUT_TREE_H
+
+#include <random>
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/PentobiTree.h"
+
+using namespace std;
+using libboardgame_base::PointTransform;
+using libboardgame_util::ArrayList;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::Move;
+using libpentobi_base::PentobiTree;
+using libpentobi_base::Point;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Merges opening moves played by the players into a tree.
+
+ Keeps statistics of the average game result for each move and player.
+ This class can also speed up playing test games by generating opening moves
+ according to the measured probability distributions. With some probabilty,
+ which decreases with the number of times a position was visited but stays
+ non-zero, the player generates a real move, which is used to update the
+ distributions, otherwise a move from the tree is played. In the limit, the
+ player plays an infinite number of real moves in each position, so the
+ measured distributions approach the real distributions and the result of
+ the test games approaches the result as if only real moves had been
+ played. */
+class OutputTree
+{
+public:
+ explicit OutputTree(Variant variant);
+
+ ~OutputTree();
+
+ void load(const string& file);
+
+ void save(const string& file);
+
+ /** Generate a move for a player from the tree.
+ @param is_player_black
+ @param bd The board with the current position.
+ @param to_play The color to generate the move for..
+ @param[out] mv The generated move, or Move::null() if no move is in the
+ tree for this position or if the player should generate a real move
+ now. */
+ void generate_move(bool is_player_black, const Board& bd, Color to_play,
+ Move& mv);
+
+ /** Add the moves of a game to the tree and update the move counters. */
+ void add_game(const Board& bd, unsigned player_black, float result,
+ const array<bool, Board::max_game_moves>& is_real_move);
+
+private:
+ typedef libboardgame_base::PointTransform<Point> PointTransform;
+
+ PentobiTree m_tree;
+
+ vector<unique_ptr<PointTransform>> m_transforms;
+
+ vector<unique_ptr<PointTransform>> m_inv_transforms;
+
+ mt19937 m_random;
+
+ void generate_move(bool is_player_black, const Board& bd, Color to_play,
+ const PointTransform& transform,
+ const PointTransform& inv_transform, Move& mv,
+ bool& play_real);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_OUTPUT_TREE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/TwoGtp.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "TwoGtp.h"
+
+#include "libboardgame_sgf/Writer.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/StringUtil.h"
+#include "libpentobi_base/ScoreUtil.h"
+
+using libboardgame_sgf::Writer;
+using libboardgame_util::trim;
+using libpentobi_base::get_multiplayer_result;
+using libpentobi_base::Move;
+using libpentobi_base::PieceSet;
+using libpentobi_base::ScoreType;
+
+//-----------------------------------------------------------------------------
+
+TwoGtp::TwoGtp(const string& black, const string& white, Variant variant,
+ unsigned nu_games, Output& output, bool quiet,
+ const string& log_prefix, bool fast_open)
+ : m_quiet(quiet),
+ m_fast_open(fast_open),
+ m_variant(variant),
+ m_nu_games(nu_games),
+ m_bd(variant),
+ m_output(output),
+ m_black(black),
+ m_white(white)
+{
+ if (! m_quiet)
+ {
+ m_black.enable_log(log_prefix + "B");
+ m_white.enable_log(log_prefix + "W");
+ }
+ if (get_nu_colors(m_variant) == 2)
+ {
+ m_colors[0] = "b";
+ m_colors[1] = "w";
+ }
+ else
+ {
+ m_colors[0] = "1";
+ m_colors[1] = "2";
+ m_colors[2] = "3";
+ m_colors[3] = "4";
+ }
+}
+
+float TwoGtp::get_result(unsigned player_black)
+{
+ float result;
+ auto nu_players = m_bd.get_nu_players();
+ if (nu_players == 2)
+ {
+ auto score = m_bd.get_score_twoplayer(Color(0));
+ if (score > 0)
+ result = 1;
+ else if (score < 0)
+ result = 0;
+ else
+ result = 0.5;
+ if (player_black != 0)
+ result = 1 - result;
+ }
+ else
+ {
+ array<ScoreType, Color::range> points;
+ for (Color::IntType i = 0; i < m_bd.get_nu_colors(); ++i)
+ points[i] = m_bd.get_points(Color(i));
+ array<float, Color::range> player_result;
+ bool break_ties = (m_bd.get_piece_set() == PieceSet::callisto);
+ get_multiplayer_result(nu_players, points, player_result, break_ties);
+ result = player_result[player_black];
+ }
+ return result;
+}
+
+void TwoGtp::play_game(unsigned game_number)
+{
+ if (! m_quiet)
+ LIBBOARDGAME_LOG("================================================\n"
+ "Game ", game_number, "\n"
+ "================================================");
+ m_bd.init();
+ send_both("clear_board");
+ auto cpu_black = send_cputime(m_black);
+ auto cpu_white = send_cputime(m_white);
+ unsigned nu_players = m_bd.get_nu_players();
+ unsigned player_black = game_number % nu_players;
+ bool resign = false;
+ ostringstream sgf_string;
+ Writer sgf(sgf_string);
+ sgf.set_indent(0);
+ sgf.begin_tree();
+ sgf.begin_node();
+ sgf.write_property("GM", to_string(m_variant));
+ sgf.write_property("GN", game_number);
+ sgf.end_node();
+ array<bool, Board::max_game_moves> is_real_move;
+ unsigned player;
+ while (! m_bd.is_game_over())
+ {
+ auto to_play = m_bd.get_effective_to_play();
+ if (m_variant == Variant::classic_3 && to_play == Color(3))
+ player = m_bd.get_alt_player();
+ else
+ player = to_play.to_int() % nu_players;
+ auto& player_connection = (player == player_black ? m_black : m_white);
+ auto& other_connection = (player == player_black ? m_white : m_black);
+ auto color = m_colors[to_play.to_int()];
+ Move mv;
+ if (m_fast_open
+ && m_output.generate_fast_open_move(player == player_black,
+ m_bd, to_play, mv))
+ {
+ is_real_move[m_bd.get_nu_moves()] = false;
+ LIBBOARDGAME_LOG("Playing fast opening move");
+ player_connection.send("play " + color + " " + m_bd.to_string(mv));
+ }
+ else
+ {
+ is_real_move[m_bd.get_nu_moves()] = true;
+ auto response = player_connection.send("genmove " + color);
+ if (response == "resign")
+ {
+ resign = true;
+ break;
+ }
+ mv = m_bd.from_string(response);
+ }
+ sgf.begin_node();
+ sgf.write_property(string(1, static_cast<char>(toupper(color[0]))),
+ m_bd.to_string(mv));
+ sgf.end_node();
+ if (mv.is_null() || ! m_bd.is_legal(to_play, mv))
+ throw runtime_error("invalid move: " + m_bd.to_string(mv));
+ m_bd.play(to_play, mv);
+ other_connection.send("play " + color + " " + m_bd.to_string(mv));
+ }
+ cpu_black = send_cputime(m_black) - cpu_black;
+ cpu_white = send_cputime(m_white) - cpu_white;
+ float result;
+ if (resign)
+ {
+ if (nu_players > 2)
+ throw runtime_error("resign only allowed in two-player variants");
+ result = (player == player_black ? 0 : 1);
+ }
+ else
+ result = get_result(player_black);
+ sgf.end_tree();
+ m_output.add_result(game_number, result, m_bd, player_black, cpu_black,
+ cpu_white, sgf_string.str(), is_real_move);
+}
+
+void TwoGtp::run()
+{
+ send_both(string("set_game ") + to_string(m_variant));
+ while (! m_output.check_sentinel())
+ {
+ unsigned n = m_output.get_next();
+ if (n >= m_nu_games)
+ break;
+ play_game(n);
+ }
+ send_both("quit");
+}
+
+void TwoGtp::send_both(const string& cmd)
+{
+ m_black.send(cmd);
+ m_white.send(cmd);
+}
+
+double TwoGtp::send_cputime(GtpConnection& gtp_connection)
+{
+ string response = gtp_connection.send("cputime");
+ istringstream in(response);
+ double cputime;
+ in >> cputime;
+ if (! in)
+ throw runtime_error("invalid response to cputime: " + response);
+ return cputime;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/TwoGtp.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef TWOGTP_TWOGTP_H
+#define TWOGTP_TWOGTP_H
+
+#include <array>
+#include "GtpConnection.h"
+#include "Output.h"
+#include "libpentobi_base/Board.h"
+
+using namespace std;
+using libpentobi_base::Board;
+using libpentobi_base::Color;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class TwoGtp
+{
+public:
+ TwoGtp(const string& black, const string& white, Variant variant,
+ unsigned nu_games, Output& output, bool quiet,
+ const string& log_prefix, bool fast_open);
+
+ void run();
+
+private:
+ bool m_quiet;
+
+ bool m_fast_open;
+
+ Variant m_variant;
+
+ unsigned m_nu_games;
+
+ Board m_bd;
+
+ Output& m_output;
+
+ GtpConnection m_black;
+
+ GtpConnection m_white;
+
+ array<string, Color::range> m_colors;
+
+ float get_result(unsigned player_black);
+
+ void play_game(unsigned game_number);
+
+ void send_both(const string& cmd);
+
+ double send_cputime(GtpConnection& gtp_connection);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_TWOGTP_H
--- /dev/null
+add_subdirectory(libboardgame_util)
+add_subdirectory(libboardgame_sgf)
+add_subdirectory(libboardgame_base)
+add_subdirectory(libboardgame_mcts)
+add_subdirectory(libpentobi_base)
+add_subdirectory(libpentobi_mcts)
+
+if (PENTOBI_BUILD_GTP)
+ add_subdirectory(libboardgame_gtp)
+endif()
--- /dev/null
+add_executable(unittest_libboardgame_base
+ MarkerTest.cpp
+ PointTransformTest.cpp
+ RatingTest.cpp
+ RectGeometryTest.cpp
+ StringRepTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_base
+ boardgame_test_main
+ boardgame_base
+ boardgame_test
+ boardgame_util
+ boardgame_sys
+ )
+
+add_test(libboardgame_base unittest_libboardgame_base)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/MarkerTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_base/Marker.h"
+#include "libboardgame_base/Point.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point;
+typedef libboardgame_base::Marker<Point> Marker;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(boardgame_marker_basic)
+{
+ Marker m;
+ Point p1(10);
+ Point p2(11);
+ LIBBOARDGAME_CHECK(! m.set(p1));
+ LIBBOARDGAME_CHECK(! m.set(p2));
+ LIBBOARDGAME_CHECK(m.set(p1));
+ LIBBOARDGAME_CHECK(m.set(p2));
+ m.clear();
+ LIBBOARDGAME_CHECK(! m.set(p1));
+ LIBBOARDGAME_CHECK(! m.set(p2));
+}
+
+/** Test clear after a number of clears around the maximum unsigned integer
+ value.
+ This is a critical point of the implementation, which assumes that
+ values not equal to a clear counter are unmarked and the overflow of the
+ clear counter must be handled correctly.
+ This test is only run, if integers are not larger than 32-bit, otherwise
+ it would take too long. */
+LIBBOARDGAME_TEST_CASE(boardgame_marker_overflow)
+{
+ if (numeric_limits<unsigned>::digits > 32)
+ return;
+ Marker m;
+ m.setup_for_overflow_test(numeric_limits<unsigned>::max() - 5);
+ Point p1(10);
+ Point p2(11);
+ for (int i = 0; i < 10; ++i)
+ {
+ LIBBOARDGAME_CHECK(! m.set(p1));
+ LIBBOARDGAME_CHECK(! m.set(p2));
+ m.clear();
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/PointTransformTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_base/Point.h"
+#include "libboardgame_base/PointTransform.h"
+#include "libboardgame_base/RectGeometry.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point;
+typedef libboardgame_base::RectGeometry<Point> RectGeometry;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(boardgame_point_transform_get_transformed)
+{
+ unsigned sz = 9;
+ auto& geo = RectGeometry::get(sz, sz);
+ Point p = geo.get_point(1, 2);
+ {
+ libboardgame_base::PointTransfIdent<Point> transform;
+ LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) == p);
+ }
+ {
+ libboardgame_base::PointTransfRot180<Point> transform;
+ LIBBOARDGAME_CHECK(transform.get_transformed(p, geo)
+ == geo.get_point(7, 6));
+ }
+ {
+ libboardgame_base::PointTransfRot270Refl<Point> transform;
+ LIBBOARDGAME_CHECK(transform.get_transformed(p, geo)
+ == geo.get_point(2, 1));
+ }
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/RatingTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_base/Rating.h"
+#include "libboardgame_test/Test.h"
+
+using namespace libboardgame_base;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(boardgame_rating_get_expected_result)
+{
+ Rating a(2806);
+ Rating b(2577);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(a.get_expected_result(b), 0.789, 0.001);
+}
+
+LIBBOARDGAME_TEST_CASE(boardgame_rating_get_expected_result_multiplayer)
+{
+ // Player and 3 opponents, all with rating 1000, should have 25%
+ // winning probability
+ Rating a(1000);
+ Rating b(1000);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(a.get_expected_result(b, 3), 0.25, 0.001);
+}
+
+LIBBOARDGAME_TEST_CASE(boardgame_rating_update_1)
+{
+ Rating a(2806);
+ Rating b(2577);
+ Rating new_a = a;
+ Rating new_b = b;
+ new_a.update(0, b, 10);
+ new_b.update(1, a, 10);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2798.f, 1);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2585.f, 1);
+}
+
+LIBBOARDGAME_TEST_CASE(boardgame_rating_update_2)
+{
+ Rating a(2806);
+ Rating b(2577);
+ Rating new_a = a;
+ Rating new_b = b;
+ new_a.update(1, b, 10);
+ new_b.update(0, a, 10);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2808.f, 1);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2575.f, 1);
+}
+
+LIBBOARDGAME_TEST_CASE(boardgame_rating_update_3)
+{
+ Rating a(2806);
+ Rating b(2577);
+ Rating new_a = a;
+ Rating new_b = b;
+ new_a.update(0.5, b, 10);
+ new_b.update(0.5, a, 10);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_a.get(), 2803.f, 1);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2580.f, 1);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/RectGeometryTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_base/Point.h"
+#include "libboardgame_base/RectGeometry.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+typedef libboardgame_base::Point<19 * 19, 19, 19, unsigned short> Point;
+typedef libboardgame_base::Geometry<Point> Geometry;
+typedef libboardgame_base::RectGeometry<Point> RectGeometry;
+typedef libboardgame_base::ArrayList<Point, Point::max_onboard> PointList;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_iterate)
+{
+ auto& geo = RectGeometry::get(3, 3);
+ auto i = geo.begin();
+ auto end = geo.end();
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(0, 0) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(1, 0) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(2, 0) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(0, 1) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(1, 1) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(2, 1) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(0, 2) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(1, 2) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i != end);
+ LIBBOARDGAME_CHECK(geo.get_point(2, 2) == *i);
+ ++i;
+ LIBBOARDGAME_CHECK(i == end);
+}
+
+LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_from_string)
+{
+ auto& geo = RectGeometry::get(19, 19);
+ Point p;
+
+ LIBBOARDGAME_CHECK(geo.from_string("a1", p));
+ LIBBOARDGAME_CHECK(p == geo.get_point(0, 18));
+
+ LIBBOARDGAME_CHECK(geo.from_string("a19", p));
+ LIBBOARDGAME_CHECK(p == geo.get_point(0, 0));
+
+ LIBBOARDGAME_CHECK(geo.from_string("A1", p));
+ LIBBOARDGAME_CHECK(p == geo.get_point(0, 18));
+
+ LIBBOARDGAME_CHECK(! geo.from_string("foobar", p));
+ LIBBOARDGAME_CHECK(! geo.from_string("a123", p));
+ LIBBOARDGAME_CHECK(! geo.from_string("a56", p));
+ LIBBOARDGAME_CHECK(! geo.from_string("aa1", p));
+ LIBBOARDGAME_CHECK(! geo.from_string("c3#", p));
+}
+
+LIBBOARDGAME_TEST_CASE(boardgame_rect_geometry_to_string)
+{
+ auto& geo = RectGeometry::get(19, 19);
+ LIBBOARDGAME_CHECK_EQUAL(string("a1"), geo.to_string(geo.get_point(0, 18)));
+ LIBBOARDGAME_CHECK_EQUAL(string("a19"), geo.to_string(geo.get_point(0, 0)));
+ LIBBOARDGAME_CHECK_EQUAL(string("j10"), geo.to_string(geo.get_point(9, 9)));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/StringRepTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <sstream>
+#include "libboardgame_base/StringRep.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using libboardgame_base::StdStringRep;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+StdStringRep string_rep;
+
+bool read(const string& s, unsigned& x, unsigned& y, unsigned width,
+ unsigned height)
+{
+ istringstream in(s);
+ return string_rep.read(in, width, height, x, y);
+}
+
+string write(unsigned x, unsigned y, unsigned width, unsigned height)
+{
+ ostringstream out;
+ string_rep.write(out, x, y, width, height);
+ return out.str();
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(boardgame_base_spreadsheet_string_rep_read)
+{
+ unsigned x;
+ unsigned y;
+
+ LIBBOARDGAME_CHECK(read("a1", x, y, 20, 20));
+ LIBBOARDGAME_CHECK_EQUAL(x, 0u);
+ LIBBOARDGAME_CHECK_EQUAL(y, 19u);
+
+ LIBBOARDGAME_CHECK(read("a23", x, y, 25, 25));
+ LIBBOARDGAME_CHECK_EQUAL(x, 0u);
+ LIBBOARDGAME_CHECK_EQUAL(y, 2u);
+
+ LIBBOARDGAME_CHECK(read("A1", x, y, 20, 20));
+ LIBBOARDGAME_CHECK_EQUAL(x, 0u);
+ LIBBOARDGAME_CHECK_EQUAL(y, 19u);
+
+ LIBBOARDGAME_CHECK(read("j1", x, y, 20, 20));
+ LIBBOARDGAME_CHECK_EQUAL(x, 9u);
+ LIBBOARDGAME_CHECK_EQUAL(y, 19u);
+
+ LIBBOARDGAME_CHECK(read("ab1", x, y, 30, 30));
+ LIBBOARDGAME_CHECK_EQUAL(x, 27u);
+ LIBBOARDGAME_CHECK_EQUAL(y, 29u);
+
+ LIBBOARDGAME_CHECK(read(" a1", x, y, 20, 20));
+ LIBBOARDGAME_CHECK_EQUAL(x, 0u);
+ LIBBOARDGAME_CHECK_EQUAL(y, 19u);
+
+ LIBBOARDGAME_CHECK(! read("a 1", x, y, 20, 20));
+
+ LIBBOARDGAME_CHECK(! read("foobar", x, y, 20, 20));
+
+ LIBBOARDGAME_CHECK(! read("c3#", x, y, 20, 20));
+}
+
+LIBBOARDGAME_TEST_CASE(boardgame_base_spreadsheet_string_rep_write)
+{
+ LIBBOARDGAME_CHECK_EQUAL(string("a1"), write(0, 18, 19, 19));
+ LIBBOARDGAME_CHECK_EQUAL(string("a19"), write(0, 0, 19, 19));
+ LIBBOARDGAME_CHECK_EQUAL(string("ab1"), write(27, 59, 60, 60));
+ LIBBOARDGAME_CHECK_EQUAL(string("ba1"), write(52, 59, 60, 60));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/ArgumentsTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_gtp/Arguments.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_gtp;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_arg)
+{
+ CmdLine line("command arg1 \"arg2 \" arg3 ");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL("arg1", string(args.get(0)));
+ LIBBOARDGAME_CHECK_EQUAL("arg2 ", string(args.get(1)));
+ LIBBOARDGAME_CHECK_EQUAL("arg3", string(args.get(2)));
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_to_lower)
+{
+ CmdLine line("command cAsE");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL(string("case"), args.get_tolower(0));
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_bool)
+{
+ {
+ CmdLine line("command 0");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK(! args.parse<bool>(0));
+ }
+ {
+ CmdLine line("command 1");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK(args.parse<bool>(0));
+ }
+ {
+ CmdLine line("command 2");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_THROW(args.parse<bool>(0), Failure);
+ }
+ {
+ CmdLine line("command arg1");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_THROW(args.parse<bool>(0), Failure);
+ }
+ {
+ CmdLine line("command");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_THROW(args.parse<bool>(0), Failure);
+ }
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_float)
+{
+ CmdLine line("command abc 5.5");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_THROW(args.parse<float>(0), Failure);
+ LIBBOARDGAME_CHECK_CLOSE(5.5f, args.parse<float>(1), 1e-4);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_int)
+{
+ CmdLine line("command 5 arg");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL(5, args.parse<int>(0));
+ LIBBOARDGAME_CHECK_THROW(args.parse<int>(1), Failure);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_min_int)
+{
+ CmdLine line("command 5");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min<int>(0, 3));
+ LIBBOARDGAME_CHECK_THROW(args.parse_min<int>(0, 7), Failure);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_min_max_int)
+{
+ CmdLine line("command 5");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min_max<int>(0, 3, 10));
+ LIBBOARDGAME_CHECK_THROW(args.parse_min_max<int>(0, 0, 4), Failure);
+ LIBBOARDGAME_CHECK_THROW(args.parse_min_max<int>(0, 10, 20), Failure);
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_single_int)
+{
+ {
+ CmdLine line("command 5");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL(5, args.parse<int>());
+ }
+ {
+ CmdLine line("command 5 10");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_THROW(args.parse<int>(), Failure);
+ }
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_nu_arg_0)
+{
+ CmdLine line("1 command");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_NO_THROW(args.check_empty());
+ LIBBOARDGAME_CHECK_THROW(args.check_size(1), Failure);
+ LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(2));
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_nu_arg_3)
+{
+ CmdLine line("command arg1 arg2 arg3");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_THROW(args.check_empty(), Failure);
+ LIBBOARDGAME_CHECK_THROW(args.check_size(2), Failure);
+ LIBBOARDGAME_CHECK_NO_THROW(args.check_size(3));
+ LIBBOARDGAME_CHECK_THROW(args.check_size(4), Failure);
+ LIBBOARDGAME_CHECK_THROW(args.check_size_less_equal(2), Failure);
+ LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(3));
+ LIBBOARDGAME_CHECK_NO_THROW(args.check_size_less_equal(4));
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_arg)
+{
+ CmdLine line("command arg1 arg2");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL("arg2", string(args.get_remaining_line(0)));
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_arg_empty)
+{
+ CmdLine line("command arg1");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL("", string(args.get_remaining_line(0)));
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_arguments_remaining_line)
+{
+ CmdLine line("command arg1 \"arg2 \" arg3 ");
+ Arguments args(line);
+ LIBBOARDGAME_CHECK_EQUAL("\"arg2 \" arg3",
+ string(args.get_remaining_line(0)));
+ LIBBOARDGAME_CHECK_EQUAL("arg3", string(args.get_remaining_line(1)));
+ LIBBOARDGAME_CHECK_EQUAL("", string(args.get_remaining_line(2)));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libboardgame_gtp
+ ArgumentsTest.cpp
+ CmdLineTest.cpp
+ EngineTest.cpp
+ ResponseTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_gtp
+ boardgame_test_main
+ boardgame_test
+ boardgame_util
+ boardgame_gtp
+ )
+
+add_test(libboardgame_gtp unittest_libboardgame_gtp)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/CmdLineTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_gtp/CmdLine.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_gtp;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+string get_id(const CmdLine& c)
+{
+ ostringstream s;
+ c.write_id(s);
+ return s.str();
+}
+
+string get_element(const CmdLine& c, unsigned i)
+{
+ return string(c.get_element(i));
+}
+
+}
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(gtp_cmd_line_init)
+{
+ CmdLine c("100 command1 arg1 arg2");
+ LIBBOARDGAME_CHECK_EQUAL("100", get_id(c));
+ LIBBOARDGAME_CHECK_EQUAL("command1", string(c.get_name()));
+ LIBBOARDGAME_CHECK_EQUAL(4u, c.get_elements().size());
+ LIBBOARDGAME_CHECK_EQUAL("arg1", get_element(c, 2));
+ LIBBOARDGAME_CHECK_EQUAL("arg2", get_element(c, 3));
+ c.init("2 command2 arg3");
+ LIBBOARDGAME_CHECK_EQUAL("2", get_id(c));
+ LIBBOARDGAME_CHECK_EQUAL("command2", string(c.get_name()));
+ LIBBOARDGAME_CHECK_EQUAL(3u, c.get_elements().size());
+ LIBBOARDGAME_CHECK_EQUAL("arg3", get_element(c, 2));
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_cmd_line_parse)
+{
+ CmdLine c("10 boardsize 11");
+ LIBBOARDGAME_CHECK_EQUAL("10 boardsize 11", c.get_line());
+ LIBBOARDGAME_CHECK_EQUAL("11", string(c.get_trimmed_line_after_elem(1)));
+ LIBBOARDGAME_CHECK_EQUAL("10", get_id(c));
+ LIBBOARDGAME_CHECK_EQUAL("boardsize", string(c.get_name()));
+ LIBBOARDGAME_CHECK_EQUAL(3u, c.get_elements().size());
+ LIBBOARDGAME_CHECK_EQUAL("11", get_element(c, 2));
+
+ c.init(" 20 clear_board ");
+ LIBBOARDGAME_CHECK_EQUAL(" 20 clear_board ", c.get_line());
+ LIBBOARDGAME_CHECK_EQUAL("", string(c.get_trimmed_line_after_elem(1)));
+ LIBBOARDGAME_CHECK_EQUAL("20", get_id(c));
+ LIBBOARDGAME_CHECK_EQUAL("clear_board", string(c.get_name()));
+ LIBBOARDGAME_CHECK_EQUAL(2u, c.get_elements().size());
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/EngineTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_gtp/Engine.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_gtp;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+//-----------------------------------------------------------------------------
+
+/** GTP engine returning invalid responses for testing class Engine.
+ For testing that the base class Engine sanitizes responses of
+ subclasses that contain empty lines (see @ref Engine::exec_main_loop). */
+class InvalidResponseEngine
+ : public Engine
+{
+public:
+ InvalidResponseEngine();
+
+ void invalid_response(const Arguments&, Response&);
+
+ void invalid_response_2(const Arguments&, Response&);
+};
+
+InvalidResponseEngine::InvalidResponseEngine()
+{
+ add("invalid_response", &InvalidResponseEngine::invalid_response);
+ add("invalid_response_2", &InvalidResponseEngine::invalid_response_2);
+}
+
+void InvalidResponseEngine::invalid_response(const Arguments&, Response& r)
+{
+ r << "This response is invalid\n"
+ << "\n"
+ << "because it contains an empty line";
+}
+
+void InvalidResponseEngine::invalid_response_2(const Arguments&, Response& r)
+{
+ r << "This response is invalid\n"
+ << "\n"
+ << "\n"
+ << "because it contains two empty lines";
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(gtp_engine_command)
+{
+ istringstream in("version\n");
+ ostringstream out;
+ Engine engine;
+ engine.exec_main_loop(in, out);
+ LIBBOARDGAME_CHECK_EQUAL(string("= \n\n"), out.str());
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_engine_command_with_id)
+{
+ istringstream in("10 version\n");
+ ostringstream out;
+ Engine engine;
+ engine.exec_main_loop(in, out);
+ LIBBOARDGAME_CHECK_EQUAL(string("=10 \n\n"), out.str());
+}
+
+/** Check that invalid responses with one empty line are sanitized. */
+LIBBOARDGAME_TEST_CASE(gtp_engine_empty_lines)
+{
+ istringstream in("invalid_response\n");
+ ostringstream out;
+ InvalidResponseEngine engine;
+ engine.exec_main_loop(in, out);
+ LIBBOARDGAME_CHECK_EQUAL(string("= This response is invalid\n"
+ " \n"
+ "because it contains an empty line\n"
+ "\n"),
+ out.str());
+}
+
+/** Check that invalid responses with two empty lines are sanitized. */
+LIBBOARDGAME_TEST_CASE(gtp_engine_empty_lines_2)
+{
+ istringstream in("invalid_response_2\n");
+ ostringstream out;
+ InvalidResponseEngine engine;
+ engine.exec_main_loop(in, out);
+ LIBBOARDGAME_CHECK_EQUAL(string("= This response is invalid\n"
+ " \n"
+ " \n"
+ "because it contains two empty lines\n"
+ "\n"),
+ out.str());
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_engine_unknown_command)
+{
+ istringstream in("unknowncommand\n");
+ ostringstream out;
+ Engine engine;
+ engine.exec_main_loop(in, out);
+ LIBBOARDGAME_CHECK(out.str().size() >= 2);
+ LIBBOARDGAME_CHECK_EQUAL(string("? "), out.str().substr(0, 2));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_gtp/ResponseTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_gtp/Response.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_gtp;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(gtp_response_basic)
+{
+ Response r;
+ r << "Name";
+ LIBBOARDGAME_CHECK_EQUAL(string("Name"), r.to_string());
+ r.set("Name2");
+ LIBBOARDGAME_CHECK_EQUAL(string("Name2"), r.to_string());
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libboardgame_mcts
+ NodeTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_mcts
+ boardgame_test_main
+ boardgame_test
+ boardgame_sgf
+ boardgame_util
+ boardgame_sys
+ )
+
+add_test(libboardgame_mcts unittest_libboardgame_mcts)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_mcts/NodeTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_mcts/Node.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value)
+{
+ libboardgame_mcts::Node<int, float, true> node;
+ node.init(0, 0.5, 0);
+ node.add_value(5);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5., 1e-4);
+ node.add_value(2);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5, 1e-4);
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value_remove_loss)
+{
+ libboardgame_mcts::Node<int, float, true> node;
+ node.init(0, 0.5, 0);
+ node.add_value(5);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5., 1e-4);
+ node.add_value(0);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 2.5, 1e-4);
+ node.add_value_remove_loss(2);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5, 1e-4);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libboardgame_sgf
+ SgfNodeTest.cpp
+ SgfUtilTest.cpp
+ TreeReaderTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_sgf
+ boardgame_test_main
+ boardgame_test
+ boardgame_sgf
+ boardgame_util
+ )
+
+add_test(libboardgame_sgf unittest_libboardgame_sgf)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/SgfNodeTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <memory>
+#include "libboardgame_sgf/SgfNode.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_sgf;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(sgf_node_create_new_child)
+{
+ unique_ptr<SgfNode> parent(new SgfNode);
+ auto& child = parent->create_new_child();
+ LIBBOARDGAME_CHECK_EQUAL(&parent->get_child(), &child);
+ LIBBOARDGAME_CHECK_EQUAL(&child.get_parent(), parent.get());
+}
+
+LIBBOARDGAME_TEST_CASE(sgf_node_remove_property)
+{
+ string id = "B";
+ unique_ptr<SgfNode> node(new SgfNode);
+ LIBBOARDGAME_CHECK(! node->has_property(id));
+ node->set_property(id, "foo");
+ LIBBOARDGAME_CHECK(node->has_property(id));
+ LIBBOARDGAME_CHECK_EQUAL(node->get_property(id), "foo");
+ bool result = node->remove_property(id);
+ LIBBOARDGAME_CHECK(result);
+ LIBBOARDGAME_CHECK(! node->has_property(id));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/SgfUtilTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_sgf/SgfUtil.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_sgf;
+using namespace libboardgame_sgf::util;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(sgf_util_get_path_from_root)
+{
+ unique_ptr<SgfNode> root(new SgfNode);
+ auto& child = root->create_new_child();
+ vector<const SgfNode*> path;
+ get_path_from_root(child, path);
+ LIBBOARDGAME_CHECK_EQUAL(path.size(), 2u);
+ LIBBOARDGAME_CHECK_EQUAL(path[0], root.get());
+ LIBBOARDGAME_CHECK_EQUAL(path[1], &child);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/TreeReaderTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_sgf/TreeReader.h"
+
+#include <sstream>
+#include "libboardgame_sgf/TreeWriter.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_sgf;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_basic)
+{
+ istringstream in("(;B[aa];W[bb])");
+ TreeReader reader;
+ reader.read(in);
+ auto& root = reader.get_tree();
+ LIBBOARDGAME_CHECK(root.has_property("B"));
+ LIBBOARDGAME_CHECK(root.has_single_child());
+ auto& child = root.get_child();
+ LIBBOARDGAME_CHECK(child.has_property("W"));
+ LIBBOARDGAME_CHECK(! child.has_children());
+}
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_basic_2)
+{
+ istringstream in("(;C[1](;C[2.1])(;C[2.2]))");
+ TreeReader reader;
+ reader.read(in);
+ auto& root = reader.get_tree();
+ LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1");
+ LIBBOARDGAME_CHECK_EQUAL(root.get_nu_children(), 2u);
+ LIBBOARDGAME_CHECK_EQUAL(root.get_child(0).get_property("C"), "2.1");
+ LIBBOARDGAME_CHECK_EQUAL(root.get_child(1).get_property("C"), "2.2");
+}
+
+/** Test that a property value with a unicode character is preserved after
+ reading and writing.
+ In previous versions this was broken because of a bug in the replacement
+ of non-newline whitespaces (as required by SGF) by the writer. (The bug
+ occurred only on some platforms depending on the std::isspace()
+ implementation.) */
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_unicode)
+{
+ SgfNode root;
+ const char* id = "C";
+ const char* value = "\xc3\xbc"; // German u-umlaut as UTF-8
+ root.set_property(id, value);
+ ostringstream out;
+ TreeWriter writer(out, root);
+ writer.write();
+ istringstream in(out.str());
+ TreeReader reader;
+ reader.read(in);
+ LIBBOARDGAME_CHECK_EQUAL(reader.get_tree().get_property(id), value);
+}
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_property_after_newline)
+{
+ istringstream in("(;FF[4]\n"
+ "CA[UTF-8])");
+ TreeReader reader;
+ reader.read(in);
+ auto& root = reader.get_tree();
+ LIBBOARDGAME_CHECK(root.has_property("FF"));
+ LIBBOARDGAME_CHECK(root.has_property("CA"));
+}
+
+/** Test cross-platform handling of property values containing newlines.
+ The reader should convert all platform-dependent newline sequences (LF,
+ CR+LF, CR) into LF, such that property values containing newlines are
+ independent on the platform that was used to write the file. */
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_newline)
+{
+ {
+ istringstream in("(;C[1\n2])");
+ TreeReader reader;
+ reader.read(in);
+ auto& root = reader.get_tree();
+ LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2");
+ }
+ {
+ istringstream in("(;C[1\r\n2])");
+ TreeReader reader;
+ reader.read(in);
+ auto& root = reader.get_tree();
+ LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2");
+ }
+ {
+ istringstream in("(;C[1\r2])");
+ TreeReader reader;
+ reader.read(in);
+ auto& root = reader.get_tree();
+ LIBBOARDGAME_CHECK_EQUAL(root.get_property("C"), "1\n2");
+ }
+}
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_property_without_value)
+{
+ istringstream in("(;B)");
+ TreeReader reader;
+ LIBBOARDGAME_CHECK_THROW(reader.read(in), TreeReader::ReadError);
+}
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_text_before_node)
+{
+ istringstream in("(B;)");
+ TreeReader reader;
+ LIBBOARDGAME_CHECK_THROW(reader.read(in), TreeReader::ReadError);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/ArrayListTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_util/ArrayList.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_util;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(util_array_list_basic)
+{
+ ArrayList<int, 10> l;
+ LIBBOARDGAME_CHECK_EQUAL(0u, l.size());
+ LIBBOARDGAME_CHECK(l.empty());
+ l.push_back(5);
+ LIBBOARDGAME_CHECK_EQUAL(1u, l.size());
+ LIBBOARDGAME_CHECK(! l.empty());
+ LIBBOARDGAME_CHECK_EQUAL(5, l[0]);
+ l.push_back(7);
+ LIBBOARDGAME_CHECK_EQUAL(2u, l.size());
+ LIBBOARDGAME_CHECK(! l.empty());
+ LIBBOARDGAME_CHECK_EQUAL(5, l[0]);
+ LIBBOARDGAME_CHECK_EQUAL(7, l[1]);
+ l.clear();
+ LIBBOARDGAME_CHECK_EQUAL(0u, l.size());
+ LIBBOARDGAME_CHECK(l.empty());
+}
+
+LIBBOARDGAME_TEST_CASE(util_array_list_construct_single_element)
+{
+ ArrayList<int, 10> l(5);
+ LIBBOARDGAME_CHECK_EQUAL(1u, l.size());
+ LIBBOARDGAME_CHECK_EQUAL(5, l[0]);
+}
+
+LIBBOARDGAME_TEST_CASE(util_array_list_equals)
+{
+ ArrayList<int, 10> l1{ 1, 2, 3 };
+ ArrayList<int, 10> l2{ 1, 2, 3 };
+ LIBBOARDGAME_CHECK(l1 == l2);
+ l2.push_back(4);
+ LIBBOARDGAME_CHECK(! (l1 == l2));
+ l2 = ArrayList<int, 10>({ 2, 1, 3 });
+ LIBBOARDGAME_CHECK(! (l1 == l2));
+}
+
+LIBBOARDGAME_TEST_CASE(util_array_list_pop_back)
+{
+ ArrayList<int, 10> l(5);
+ int i = l.pop_back();
+ LIBBOARDGAME_CHECK_EQUAL(5, i);
+ LIBBOARDGAME_CHECK(l.empty());
+}
+
+LIBBOARDGAME_TEST_CASE(util_array_list_remove)
+{
+ ArrayList<int, 10> l{ 1, 2, 3, 4 };
+ l.remove(2);
+ LIBBOARDGAME_CHECK_EQUAL(3u, l.size());
+ LIBBOARDGAME_CHECK_EQUAL(1, l[0]);
+ LIBBOARDGAME_CHECK_EQUAL(3, l[1]);
+ LIBBOARDGAME_CHECK_EQUAL(4, l[2]);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libboardgame_util
+ ArrayListTest.cpp
+ OptionsTest.cpp
+ StatisticsTest.cpp
+ StringUtilTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_util
+ boardgame_test_main
+ boardgame_test
+ boardgame_util
+ boardgame_sys
+ )
+
+add_test(libboardgame_util unittest_libboardgame_util)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/OptionsTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_util/Options.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_util;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_options_basic)
+{
+ vector<string> specs =
+ { "first|a:", "second|b:", "third|c", "fourth", "fifth" };
+ const char* argv[] =
+ { nullptr, "--second", "secondval", "--first", "firstval",
+ "--fourth", "-c", "arg1", "arg2" };
+ int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+ Options opt(argc, argv, specs);
+ LIBBOARDGAME_CHECK(opt.contains("first"));
+ LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "firstval");
+ LIBBOARDGAME_CHECK(opt.contains("second"));
+ LIBBOARDGAME_CHECK_EQUAL(opt.get("second"), "secondval");
+ LIBBOARDGAME_CHECK(opt.contains("third"));
+ LIBBOARDGAME_CHECK(opt.contains("fourth"));
+ LIBBOARDGAME_CHECK(! opt.contains("fifth"));
+ auto& args = opt.get_args();
+ LIBBOARDGAME_CHECK_EQUAL(args.size(), 2u);
+ LIBBOARDGAME_CHECK_EQUAL(args[0], "arg1");
+ LIBBOARDGAME_CHECK_EQUAL(args[1], "arg2");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_options_end_options)
+{
+ vector<string> specs = { "first:" };
+ const char* argv[] =
+ { nullptr, "--first", "firstval", "--", "--arg1" };
+ int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+ Options opt(argc, argv, specs);
+ LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "firstval");
+ auto& args = opt.get_args();
+ LIBBOARDGAME_CHECK_EQUAL(args.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(args[0], "--arg1");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_options_missing_val)
+{
+ vector<string> specs = { "first:" };
+ const char* argv[] = { nullptr, "--first" };
+ int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+ LIBBOARDGAME_CHECK_THROW(Options opt(argc, argv, specs), runtime_error);
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_options_nospace)
+{
+ vector<string> specs = { "first|a:", "second|b:" };
+ const char* argv[] = { nullptr, "-abc" };
+ int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+ Options opt(argc, argv, specs);
+ LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), "bc");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_options_multi_short_with_val)
+{
+ vector<string> specs = { "first|a", "second|b:" };
+ const char* argv[] = { nullptr, "-ab", "c" };
+ int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+ Options opt(argc, argv, specs);
+ LIBBOARDGAME_CHECK(opt.contains("first"));
+ LIBBOARDGAME_CHECK_EQUAL(opt.get("second"), "c");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_options_type)
+{
+ vector<string> specs = { "first:", "second:" };
+ const char* argv[] = { nullptr, "--first", "10", "--second", "foo" };
+ int argc = static_cast<int>(sizeof(argv) / sizeof(argv[0]));
+ Options opt(argc, argv, specs);
+ LIBBOARDGAME_CHECK_EQUAL(opt.get<int>("first"), 10);
+ LIBBOARDGAME_CHECK_THROW(opt.get<int>("second"), runtime_error);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/StatisticsTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_util/Statistics.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_util;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_statistics_basic)
+{
+ Statistics<double> s;
+ s.add(12);
+ s.add(11);
+ s.add(14);
+ s.add(16);
+ s.add(15);
+ LIBBOARDGAME_CHECK_EQUAL(s.get_count(), 5.);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_mean(), 13.6, 1e-6);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_variance(), 3.44, 1e-6);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(s.get_deviation(), 1.854723, 1e-6);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_util/StringUtilTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_util/StringUtil.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_util;
+
+//----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_get_letter_coord)
+{
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(0), "a");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(1), "b");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(25), "z");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26), "aa");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 + 1), "ab");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 + 25), "az");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26), "ba");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26 + 1), "bb");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(2 * 26 + 25), "bz");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26), "za");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26 + 1), "zb");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(26 * 26 + 25), "zz");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26), "aaa");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26 + 1), "aab");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(27 * 26 + 25), "aaz");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26), "aba");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26 + 1), "abb");
+ LIBBOARDGAME_CHECK_EQUAL(get_letter_coord(28 * 26 + 25), "abz");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_split)
+{
+ {
+ vector<string> v = split("a,b,cc,d", ',');
+ LIBBOARDGAME_CHECK_EQUAL(v.size(), 4u);
+ LIBBOARDGAME_CHECK_EQUAL(v[0], "a");
+ LIBBOARDGAME_CHECK_EQUAL(v[1], "b");
+ LIBBOARDGAME_CHECK_EQUAL(v[2], "cc");
+ LIBBOARDGAME_CHECK_EQUAL(v[3], "d");
+ }
+ {
+ vector<string> v = split("", ',');
+ LIBBOARDGAME_CHECK_EQUAL(v.size(), 0u);
+ }
+ {
+ vector<string> v = split("a,", ',');
+ LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u);
+ LIBBOARDGAME_CHECK_EQUAL(v[0], "a");
+ LIBBOARDGAME_CHECK_EQUAL(v[1], "");
+ }
+ {
+ vector<string> v = split(",a", ',');
+ LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u);
+ LIBBOARDGAME_CHECK_EQUAL(v[0], "");
+ LIBBOARDGAME_CHECK_EQUAL(v[1], "a");
+ }
+ {
+ vector<string> v = split("a,,b", ',');
+ LIBBOARDGAME_CHECK_EQUAL(v.size(), 3u);
+ LIBBOARDGAME_CHECK_EQUAL(v[0], "a");
+ LIBBOARDGAME_CHECK_EQUAL(v[1], "");
+ LIBBOARDGAME_CHECK_EQUAL(v[2], "b");
+ }
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_to_lower)
+{
+ LIBBOARDGAME_CHECK_EQUAL(to_lower("AabC "), "aabc ");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_trim)
+{
+ LIBBOARDGAME_CHECK_EQUAL(trim("aa bb"), "aa bb");
+ LIBBOARDGAME_CHECK_EQUAL(trim(" \t\r\naa bb"), "aa bb");
+ LIBBOARDGAME_CHECK_EQUAL(trim("aa bb \t\r\n"), "aa bb");
+ LIBBOARDGAME_CHECK_EQUAL(trim(""), "");
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_util_trim_right)
+{
+ LIBBOARDGAME_CHECK_EQUAL(trim_right("aa bb"), "aa bb");
+ LIBBOARDGAME_CHECK_EQUAL(trim_right(" \t\r\naa bb"), " \t\r\naa bb");
+ LIBBOARDGAME_CHECK_EQUAL(trim_right("aa bb \t\r\n"), "aa bb");
+ LIBBOARDGAME_CHECK_EQUAL(trim_right(""), "");
+}
+
+//----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/BoardConstTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libpentobi_base/BoardConst.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+
+//-----------------------------------------------------------------------------
+
+/** Test that points in move strings are ordered.
+ As specified in doc/blksgf/Pentobi-SGF.html, the order should be
+ (a1, b1, ..., a2, b2, ...). There is no restriction on the order when
+ parsing move strings in from_string(). */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_move_string)
+{
+ auto& bc = BoardConst::get(Variant::duo);
+ Move mv = bc.from_string("h7,i7,i6,j6,j5");
+ LIBBOARDGAME_CHECK_EQUAL(bc.to_string(mv), "j5,i6,j6,h7,i7");
+}
+
+/** Check symmetry information in MoveInfoExt for some moves. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_symmetry_info)
+{
+ auto& bc = BoardConst::get(Variant::trigon_2);
+ auto& info_ext_2 =
+ bc.get_move_info_ext_2(bc.from_string("q9,q10,r10,q11,r11,s11"));
+ LIBBOARDGAME_CHECK(! info_ext_2.breaks_symmetry);
+ LIBBOARDGAME_CHECK_EQUAL(info_ext_2.symmetric_move.to_int(),
+ bc.from_string("q8,r8,s8,r9,s9,s10").to_int());
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/BoardTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libpentobi_base/Board.h"
+
+#include "libboardgame_test/Test.h"
+#include "libpentobi_base/MoveMarker.h"
+
+using namespace std;
+using namespace libpentobi_base;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void play(Board& bd, Color c, const char* s)
+{
+ bd.play(c, bd.from_string(s));
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+/** Check some basic functions in a Classic Two-Player game. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_classic_2)
+{
+ /*
+ (
+ ;GM[Blokus Two-Player]
+ ;1[a20,b20,c20,d20,e20]
+ ;2[q20,r20,s20,t20]
+ ;3[p1,q1,r1,s1,t1]
+ ;4[a1,b1,c1,d1]
+ ;1[f19,g19,h19,i19]
+ ;2[o19,p19]
+ ;3[m1,l2,m2,n2,o2]
+ ;4[e2,f2]
+ ;1[j18,k18,l18,l19,m19]
+ ;2[n20]
+ ;3[h2,i2,i3,j3,k3]
+ ;4[g1]
+ ;1[o17,n18,o18,p18,q18]
+ ;3[d2,d3,e3,f3,g3]
+ ;1[n13,o13,n14,n15,n16]
+ ;3[p3,p4,p5,p6]
+ ;1[n10,n11,o11,p11,p12]
+ ;3[l4,m4,m5,n5]
+ ;1[o7,p7,q7,o8,o9]
+ ;3[j5,k5]
+ ;1[l6,m6,n6,m7,m8]
+ ;3[a3,a4,b4,c4]
+ ;1[i6,j6,j7,k7,j8]
+ ;3[d5,e5,f5]
+ ;1[g6,f7,g7,h7]
+ ;3[j1]
+ ;1[c6,d6,e6,c7]
+ ;1[a8,b8,b9,c9]
+ ;1[d10,e10,d11,e11]
+ ;1[f9,g9,h9]
+ ;1[r4,s4,r5,r6,s6]
+ ;1[t7,s8,t8,r9,s9]
+ ;1[q13,r13,p14,q14,r14]
+ ;1[s16,r17,s17,t17,s18]
+ ;1[l9,k10,l10]
+ ;1[j11,j12]
+ ;1[i10]
+ )
+ */
+ unique_ptr<Board> bd(new Board(Variant::classic_2));
+ play(*bd, Color(0), "a20,b20,c20,d20,e20");
+ play(*bd, Color(1), "q20,r20,s20,t20");
+ play(*bd, Color(2), "p1,q1,r1,s1,t1");
+ play(*bd, Color(3), "a1,b1,c1,d1");
+ play(*bd, Color(0), "f19,g19,h19,i19");
+ play(*bd, Color(1), "o19,p19");
+ play(*bd, Color(2), "m1,l2,m2,n2,o2");
+ play(*bd, Color(3), "e2,f2");
+ play(*bd, Color(0), "j18,k18,l18,l19,m19");
+ play(*bd, Color(1), "n20");
+ play(*bd, Color(2), "h2,i2,i3,j3,k3");
+ play(*bd, Color(3), "g1");
+ play(*bd, Color(0), "o17,n18,o18,p18,q18");
+ play(*bd, Color(2), "d2,d3,e3,f3,g3");
+ play(*bd, Color(0), "n13,o13,n14,n15,n16");
+ play(*bd, Color(2), "p3,p4,p5,p6");
+ play(*bd, Color(0), "n10,n11,o11,p11,p12");
+ play(*bd, Color(2), "l4,m4,m5,n5");
+ play(*bd, Color(0), "o7,p7,q7,o8,o9");
+ play(*bd, Color(2), "j5,k5");
+ play(*bd, Color(0), "l6,m6,n6,m7,m8");
+ play(*bd, Color(2), "a3,a4,b4,c4");
+ play(*bd, Color(0), "i6,j6,j7,k7,j8");
+ play(*bd, Color(2), "d5,e5,f5");
+ play(*bd, Color(0), "g6,f7,g7,h7");
+ play(*bd, Color(2), "j1");
+ play(*bd, Color(0), "c6,d6,e6,c7");
+ play(*bd, Color(0), "a8,b8,b9,c9");
+ play(*bd, Color(0), "d10,e10,d11,e11");
+ play(*bd, Color(0), "f9,g9,h9");
+ play(*bd, Color(0), "r4,s4,r5,r6,s6");
+ play(*bd, Color(0), "t7,s8,t8,r9,s9");
+ play(*bd, Color(0), "q13,r13,p14,q14,r14");
+ play(*bd, Color(0), "s16,r17,s17,t17,s18");
+ play(*bd, Color(0), "l9,k10,l10");
+ play(*bd, Color(0), "j11,j12");
+ play(*bd, Color(0), "i10");
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 37u);
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(109));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(7));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(2)), ScoreType(38));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(3)), ScoreType(7));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(0)), ScoreType(133));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(1)), ScoreType(-133));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(2)), ScoreType(133));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_score(Color(3)), ScoreType(-133));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(0)), 21u);
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(1)), 3u);
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(2)), 10u);
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_onboard_pieces(Color(3)), 3u);
+}
+
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_gen_moves_classic_initial)
+{
+ unique_ptr<Board> bd(new Board(Variant::classic));
+ unique_ptr<MoveList> moves(new MoveList);
+ unique_ptr<MoveMarker> marker(new MoveMarker);
+ bd->gen_moves(Color(0), *marker, *moves);
+ LIBBOARDGAME_CHECK_EQUAL(moves->size(), 58u);
+}
+
+/** Test get_place() in a 4-color, 2-player game when the player 1 has
+ a higher score but color 1 has less points than color 2. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_get_place)
+{
+ unique_ptr<Board> bd(new Board(Variant::classic_2));
+ play(*bd, Color(0), "a20,b20");
+ play(*bd, Color(1), "r20,s20,t20");
+ play(*bd, Color(2), "q1,r1,s1,t1");
+ play(*bd, Color(3), "a1,b1");
+ // Not a final position but Board::get_place() should not care about that
+ unsigned place;
+ bool isPlaceShared;
+ bd->get_place(Color(0), place, isPlaceShared);
+ LIBBOARDGAME_CHECK_EQUAL(place, 0u);
+ LIBBOARDGAME_CHECK(! isPlaceShared);
+ bd->get_place(Color(1), place, isPlaceShared);
+ LIBBOARDGAME_CHECK_EQUAL(place, 1u);
+ LIBBOARDGAME_CHECK(! isPlaceShared);
+ bd->get_place(Color(2), place, isPlaceShared);
+ LIBBOARDGAME_CHECK_EQUAL(place, 0u);
+ LIBBOARDGAME_CHECK(! isPlaceShared);
+ bd->get_place(Color(3), place, isPlaceShared);
+ LIBBOARDGAME_CHECK_EQUAL(place, 1u);
+ LIBBOARDGAME_CHECK(! isPlaceShared);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/BoardUpdaterTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libpentobi_base/BoardUpdater.h"
+
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::util::get_last_node;
+
+//-----------------------------------------------------------------------------
+
+/** Test that BoardUpdater throws an exception if a piece is played twice.
+ A tree from a file written by another application could contain move
+ sequences where a piece is played twice. This could break assumptions
+ about the maximum number of moves in a game at some places in Pentobi's
+ code, so BoardUpdater should detect this and throw an exception. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_piece_played_twice)
+{
+ istringstream in("(;GM[Blokus];1[a1];1[a3])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ unique_ptr<Board> bd(new Board(tree.get_variant()));
+ BoardUpdater updater;
+ auto& node = get_last_node(tree.get_root());
+ LIBBOARDGAME_CHECK_THROW(updater.update(*bd, tree, node), runtime_error);
+}
+
+/** Test BoardUpdater with setup properties in root node. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup)
+{
+ istringstream in("(;GM[Blokus Duo]"
+ "AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]"
+ "AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ unique_ptr<Board> bd(new Board(tree.get_variant()));
+ BoardUpdater updater;
+ updater.update(*bd, tree, tree.get_root());
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 0u);
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(10));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(10));
+}
+
+/** Test BoardUpdater with setup properties in an inner node. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_inner_node)
+{
+ istringstream in("(;GM[Blokus Duo]"
+ " ;B[e8,e9,f9,d10,e10]"
+ " ;AB[g6,f7,g7,h7,g8]AW[i4,h5,i5,j5,i6]"
+ " ;W[j7,j8,j9,k9,j10])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ unique_ptr<Board> bd(new Board(tree.get_variant()));
+ BoardUpdater updater;
+ auto& node = get_last_node(tree.get_root());
+ updater.update(*bd, tree, node);
+ // BoardUpdater merges setup properties with existing position, so
+ // get_nu_moves() should return the number of moves played after the setup
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(10));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(10));
+}
+
+/** Test removing a piece with the AE property. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_empty)
+{
+ istringstream in("(;GM[Blokus Duo]"
+ " ;B[e8,e9,f9,d10,e10]"
+ " ;W[j7,j8,j9,k9,j10]"
+ " ;AE[e8,e9,f9,d10,e10])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ unique_ptr<Board> bd(new Board(tree.get_variant()));
+ BoardUpdater updater;
+ auto& node = get_last_node(tree.get_root());
+ updater.update(*bd, tree, node);
+ // BoardUpdater merges setup properties with existing position, so
+ // get_nu_moves() should return the number of moves played after the setup
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_nu_moves(), 0u);
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(0)), ScoreType(0));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(5));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libpentobi_base
+ BoardConstTest.cpp
+ BoardTest.cpp
+ BoardUpdaterTest.cpp
+ GameTest.cpp
+ TreeTest.cpp
+)
+
+target_link_libraries(unittest_libpentobi_base
+ boardgame_test_main
+ pentobi_base
+ boardgame_base
+ boardgame_sgf
+ boardgame_test
+ boardgame_util
+ boardgame_sys
+ )
+
+add_test(libpentobi_base unittest_libpentobi_base)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/GameTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libpentobi_base/Game.h"
+
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::util::get_last_node;
+
+//-----------------------------------------------------------------------------
+
+/** Test that the current node is in a defined state if the root node contains
+ invalid properties. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_game_current_defined_invalid_root)
+{
+ istringstream in("(;GM[Blokus]1[a99999])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ Game game(Variant::classic);
+ try
+ {
+ game.init(root);
+ }
+ catch (const runtime_error&)
+ {
+ // ignore
+ }
+ LIBBOARDGAME_CHECK_EQUAL(&game.get_current(), &game.get_root());
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/TreeTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libboardgame_sgf/MissingProperty.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_test/Test.h"
+#include "libpentobi_base/PentobiTree.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_sgf::InvalidPropertyValue;
+using libboardgame_sgf::MissingProperty;
+using libboardgame_sgf::TreeReader;
+
+//-----------------------------------------------------------------------------
+
+/** Check backwards compatibility to move properties used in Pentobi 0.1.
+ Pentobi 0.1 used the property id's BLUE,YELLOW,RED,GREEN in four-player
+ game variants instead of 1,2,3,4. (It also used point lists instead of
+ single-value move properties. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_tree_backward_compatibility_0_1)
+{
+ istringstream in("(;GM[Blokus Two-Player];BLUE[a16][a17][a18][a19][a20]"
+ ";YELLOW[s17][t17][t18][t19][t20];RED[t1][t2][t3][t4][t5]"
+ ";GREEN[a1][b1][c1][d1][d2])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ auto& bc = tree.get_board_const();
+ auto& geo = bc.get_geometry();
+ auto node = &tree.get_root();
+ node = &node->get_child();
+ {
+ auto mv = tree.get_move(*node);
+ LIBBOARDGAME_CHECK(! mv.is_null());
+ LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(0));
+ auto points = bc.get_move_points(mv.move);
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 4)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 3)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 2)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 1)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 0)));
+ }
+ node = &node->get_child();
+ {
+ auto mv = tree.get_move(*node);
+ LIBBOARDGAME_CHECK(! mv.is_null());
+ LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(1));
+ auto points = bc.get_move_points(mv.move);
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(18, 3)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 3)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 2)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 1)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 0)));
+ }
+ node = &node->get_child();
+ {
+ auto mv = tree.get_move(*node);
+ LIBBOARDGAME_CHECK(! mv.is_null());
+ LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(2));
+ auto points = bc.get_move_points(mv.move);
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 19)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 18)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 17)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 16)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(19, 15)));
+ }
+ node = &node->get_child();
+ {
+ auto mv = tree.get_move(*node);
+ LIBBOARDGAME_CHECK(! mv.is_null());
+ LIBBOARDGAME_CHECK_EQUAL(mv.color, Color(3));
+ auto points = bc.get_move_points(mv.move);
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(0, 19)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(1, 19)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(2, 19)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(3, 19)));
+ LIBBOARDGAME_CHECK(points.contains(geo.get_point(3, 18)));
+ }
+}
+
+/** Check that Tree constructor throws InvalidPropertyValue on unknown GM
+ property value. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_tree_invalid_game)
+{
+ istringstream in("(;GM[1])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ LIBBOARDGAME_CHECK_THROW(PentobiTree tree(root), InvalidPropertyValue);
+}
+
+/** Check that Tree constructor throws MissingProperty on missing GM
+ property. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_tree_missing_game_property)
+{
+ istringstream in("(;)");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ LIBBOARDGAME_CHECK_THROW(PentobiTree tree(root), MissingProperty);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libpentobi_mcts
+ SearchTest.cpp
+)
+
+target_link_libraries(unittest_libpentobi_mcts
+ boardgame_test_main
+ pentobi_mcts
+ pentobi_base
+ boardgame_base
+ boardgame_sgf
+ boardgame_test
+ boardgame_util
+ boardgame_sys
+ )
+
+if(CMAKE_THREAD_LIBS_INIT)
+ target_link_libraries(unittest_libpentobi_mcts ${CMAKE_THREAD_LIBS_INIT})
+endif()
+
+add_test(libpentobi_mcts unittest_libpentobi_mcts)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_mcts/SearchTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "libpentobi_mcts/Search.h"
+
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_test/Test.h"
+#include "libboardgame_util/CpuTimeSource.h"
+#include "libpentobi_base/BoardUpdater.h"
+#include "libpentobi_base/PentobiTree.h"
+
+using namespace std;
+using namespace libpentobi_mcts;
+using libboardgame_sgf::SgfNode;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::util::get_last_node;
+using libboardgame_util::CpuTimeSource;
+using libpentobi_base::BoardUpdater;
+using libpentobi_base::PentobiTree;
+
+//-----------------------------------------------------------------------------
+
+/** Test that state generates a playout move even if no large pieces are
+ playable early in the game.
+ This tests for a bug that occurred in Pentobi 1.1 with game variant Trigon:
+ Because moves that are below a certain piece size are not generated early
+ in the game, it could happen in rare cases that no moves were generated
+ at all. */
+LIBBOARDGAME_TEST_CASE(pentobi_mcts_search_no_large_pieces)
+{
+ istringstream
+ in(R"delim(
+ (;GM[Blokus Trigon Two-Player];1[r4,r5,s5,r6,s6,r7]
+ ;2[r12,q13,r13,q14,r14,r15];3[k11,l11,m11,n11,j12,k12]
+ ;4[w7,x7,y7,z7,v8,w8];1[s8,t8,r9,s9,t9,u9]
+ ;2[n12,o12,m13,n13,o13,o14];3[k13,k14,l14,l15,m15,n15]
+ ;4[w9,t10,u10,v10,w10,x10];1[n10,o10,p10,q10,r10,r11]
+ ;2[o15,k16,l16,m16,n16,o16];3[i15,j15,h16,i16,j16,j17]
+ ;4[u11,s12,t12,u12,v12,v13];1[p4,m5,n5,o5,p5,m6]
+ ;2[k17,i18,j18,k18,l18,m18];3[l17,m17,n17,o17,p17,o18]
+ ;4[t14,u14,s15,t15,r16,s16];1[l8,m8,j9,k9,l9,m9])
+ )delim");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ unique_ptr<Board> bd(new Board(tree.get_variant()));
+ BoardUpdater updater;
+ updater.update(*bd, tree, get_last_node(tree.get_root()));
+ unsigned nu_threads = 1;
+ size_t memory = 10000;
+ unique_ptr<Search> search(new Search(bd->get_variant(), nu_threads,
+ memory));
+ Float max_count = 1;
+ size_t min_simulations = 1;
+ double max_time = 0;
+ CpuTimeSource time_source;
+ Move mv;
+ bool res = search->search(mv, *bd, Color(1), max_count, min_simulations,
+ max_time, time_source);
+ LIBBOARDGAME_CHECK(res);
+ LIBBOARDGAME_CHECK(! mv.is_null());
+}
+
+/** Test that useless one-piece moves are generated if no other moves exist.
+ Useless one-piece moves (all neighbors occupied) are not needed during
+ the search, but the search should still return one if no other legal
+ moves exist. */
+LIBBOARDGAME_TEST_CASE(pentobi_mcts_search_callisto_useless_one_piece)
+{
+ istringstream
+ in(R"delim(
+ (;GM[Callisto Two-Player];1[k10];2[k7];1[g6];2[g11]
+ ;1[f7,g7,h7,f8,h8];2[d9,e9,e10,f10,f11];1[c8,d8,e8,c9]
+ ;2[k8,l8,m8,l9,l10];1[j11,k11,i12,j12];2[h11,i11,h12,h13,i13]
+ ;1[n9,m10,n10,l11,m11];2[j4,j5,j6,k6];1[j13,h14,i14,j14,j15]
+ ;2[h3,g4,h4,i4,h5];1[n6,m7,n7,o7,n8];2[f13,g13,f14,g14]
+ ;1[c10,d10,c11,d11];2[e5,f5,g5,f6];1[l5,m5,l6,m6];2[e6,c7,d7,e7]
+ ;1[j3,k3,k4,k5];2[h1,i1,h2,i2];1[e11,e12,f12,e13];2[i8,h9,i9,h10]
+ ;1[b7,a8,b8,a9];2[k12];1[g15,h15,i15,h16];2[l12,m12,k13,l13]
+ ;1[j8,j9,j10];2[i5,h6,i6,i7];1[g8,g9,g10];2[g2,f3,g3];1[o9,p9,o10]
+ ;2[d5,c6,d6];1[b9,b10];2[e4,f4];1[o8,p8])
+ )delim");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ unique_ptr<Board> bd(new Board(tree.get_variant()));
+ BoardUpdater updater;
+ updater.update(*bd, tree, get_last_node(tree.get_root()));
+ unsigned nu_threads = 1;
+ size_t memory = 10000;
+ unique_ptr<Search> search(new Search(bd->get_variant(), nu_threads,
+ memory));
+ Float max_count = 1;
+ size_t min_simulations = 1;
+ double max_time = 0;
+ CpuTimeSource time_source;
+ Move mv;
+ bool res = search->search(mv, *bd, Color(0), max_count, min_simulations,
+ max_time, time_source);
+ LIBBOARDGAME_CHECK(res);
+ LIBBOARDGAME_CHECK(! mv.is_null());
+ LIBBOARDGAME_CHECK(bd->get_move_piece(mv) == bd->get_one_piece());
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+# Build the NSIS installer
+# We assume dynamic linking and add a custom target that runs makensis and
+# uses windeployqt to include the Qt libraries.
+
+get_target_property(QMAKE Qt5::qmake LOCATION)
+find_program(WINDEPLOYQT windeployqt.exe HINTS "${QMAKE}")
+
+set(X86 "(x86)")
+find_program(MAKENSIS makensis
+ PATHS "$ENV{ProgramFiles}\\NSIS" "$ENV{ProgramFiles${X86}}\\NSIS")
+
+add_custom_target(nsis
+ COMMAND ${CMAKE_COMMAND} -E remove_directory deploy
+ COMMAND ${CMAKE_COMMAND} -E make_directory deploy
+ COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:pentobi>" deploy
+ COMMAND ${WINDEPLOYQT} --dir deploy --release --no-svg "deploy/pentobi.exe"
+ COMMAND ${MAKENSIS} install.nsis
+)
+
+configure_file(install.nsis.in install.nsis @ONLY)
--- /dev/null
+; German translations
+; NSIS version 2.46 does not support Unicode yet, so this file needs to be
+; encoded in ISO 8859
+
+LangString ADD_START_MENU_ENTRY ${LANG_GERMAN} \
+ "Eintrag im Startmenü hinzufügen"
+LangString CREATE_DESKTOP_SHORTCUT ${LANG_GERMAN} \
+ "Desktopverknüpfung erstellen"
+LangString INSTALLER_TITLE ${LANG_GERMAN} \
+ "Pentobi ${PENTOBI_VERSION} installieren"
--- /dev/null
+; Script for creating a Windows installer with NSIS (http://nsis.sf.net)\r
+\r
+!define PENTOBI_VERSION "@PENTOBI_VERSION@"\r
+!define PENTOBI_SRC_DIR "@CMAKE_SOURCE_DIR@"\r
+!define PENTOBI_BUILD_DIR "@CMAKE_BINARY_DIR@"\r
+\r
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Pentobi"\r
+\r
+SetCompressor /SOLID lzma\r
+\r
+!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico"\r
+!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\orange-uninstall.ico"\r
+!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\orange.bmp"\r
+!define MUI_COMPONENTSPAGE_NODESC\r
+!include "MUI.nsh"\r
+!insertmacro MUI_PAGE_WELCOME\r
+!insertmacro MUI_PAGE_LICENSE "${PENTOBI_SRC_DIR}\COPYING"\r
+!insertmacro MUI_PAGE_COMPONENTS\r
+!insertmacro MUI_PAGE_DIRECTORY\r
+!insertmacro MUI_PAGE_INSTFILES\r
+!define MUI_FINISHPAGE_RUN "$INSTDIR\Pentobi.exe"\r
+!insertmacro MUI_PAGE_FINISH\r
+!insertmacro MUI_UNPAGE_CONFIRM\r
+!insertmacro MUI_UNPAGE_INSTFILES\r
+\r
+!insertmacro MUI_LANGUAGE "English"\r
+!insertmacro MUI_LANGUAGE "German"\r
+!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS\r
+\r
+!define ADD_START_MENU_ENTRY_DEFAULT "Add start menu entry"\r
+!define CREATE_DESKTOP_SHORTCUT_DEFAULT "Create desktop shortcut"\r
+!define INSTALLER_TITLE_DEFAULT "Pentobi ${PENTOBI_VERSION} Installer"\r
+LangString ADD_START_MENU_ENTRY ${LANG_ENGLISH} \\r
+ "${ADD_START_MENU_ENTRY_DEFAULT}"\r
+LangString CREATE_DESKTOP_SHORTCUT ${LANG_ENGLISH} \\r
+ "${CREATE_DESKTOP_SHORTCUT_DEFAULT}"\r
+LangString INSTALLER_TITLE ${LANG_ENGLISH} \\r
+ "${INSTALLER_TITLE_DEFAULT}"\r
+!include "${PENTOBI_SRC_DIR}\windows\German.nsh"\r
+\r
+Name "Pentobi"\r
+Caption "$(INSTALLER_TITLE)"\r
+OutFile "pentobi-${PENTOBI_VERSION}-install.exe"\r
+InstallDir "$PROGRAMFILES\Pentobi"\r
+InstallDirRegKey HKLM "Software\Pentobi" ""\r
+; Set admin level, needed for shortcut removal on Vista\r
+; (http://nsis.sf.net/Shortcuts_removal_fails_on_Windows_Vista)\r
+RequestExecutionLevel admin\r
+\r
+Section\r
+\r
+IfFileExists "$INSTDIR\Uninstall.exe" 0 +2\r
+ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR'\r
+\r
+SetOutPath "$INSTDIR\translations"\r
+File "${PENTOBI_BUILD_DIR}\src\libpentobi_gui\*.qm"\r
+File "${PENTOBI_BUILD_DIR}\src\pentobi\*.qm"\r
+SetOutPath "$INSTDIR\books"\r
+File "${PENTOBI_SRC_DIR}\src\books\book_*.blksgf"\r
+SetOutPath "$INSTDIR"\r
+File /r "${PENTOBI_SRC_DIR}\src\pentobi\help"\r
+File /oname=COPYING.txt "${PENTOBI_SRC_DIR}\COPYING"\r
+File /oname=Pentobi.exe "${PENTOBI_BUILD_DIR}\windows\deploy\pentobi.exe"\r
+File "${PENTOBI_SRC_DIR}\src\pentobi\pentobi.ico"\r
+SetOutPath "$INSTDIR"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\*.dll"\r
+SetOutPath "$INSTDIR\imageformats"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\imageformats\*.dll"\r
+SetOutPath "$INSTDIR\platforms"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\platforms\*.dll"\r
+SetOutPath "$INSTDIR\translations"\r
+File "${PENTOBI_BUILD_DIR}\windows\deploy\translations\qt_de.qm"\r
+\r
+WriteRegStr HKLM "Software\Pentobi" "" $INSTDIR\r
+\r
+WriteUninstaller $INSTDIR\Uninstall.exe\r
+WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "Pentobi"\r
+WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${PENTOBI_VERSION}"\r
+WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\pentobi.ico"\r
+WriteRegStr HKLM "${UNINST_KEY}" "URLInfoAbout" "http://pentobi.sf.net/"\r
+WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe"\r
+\r
+SetOutPath "$INSTDIR"\r
+File "${PENTOBI_SRC_DIR}\windows\blksgf.ico"\r
+\r
+WriteRegStr HKCR ".blksgf" "" "Pentobi"\r
+WriteRegStr HKCR ".blksgf" "Content Type" "application/x-blokus-sgf"\r
+WriteRegStr HKCR "Pentobi" "" "Blokus Game"\r
+WriteRegStr HKCR "Pentobi\DefaultIcon" "" "$INSTDIR\blksgf.ico"\r
+WriteRegStr HKCR "Pentobi\shell\open\command" "" \\r
+ "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\""\r
+\r
+WriteRegStr HKCR "MIME\Database\Content Type\application/x-blokus-sgf" \\r
+ "Extension" ".blksgf"\r
+\r
+WriteRegStr HKCR "Applications\Pentobi.exe" "SupportedTypes" ".blksgf"\r
+WriteRegStr HKCR "Applications\Pentobi\shell\open\command" "" \\r
+ "$\"$INSTDIR\Pentobi.exe$\" $\"%1$\""\r
+\r
+SectionEnd\r
+\r
+Section "$(ADD_START_MENU_ENTRY)"\r
+\r
+SetShellVarContext all\r
+CreateDirectory "$SMPROGRAMS\Games"\r
+CreateShortCut "$SMPROGRAMS\Games\Pentobi.lnk" "$INSTDIR\Pentobi.exe"\r
+\r
+SectionEnd\r
+\r
+Section "$(CREATE_DESKTOP_SHORTCUT)"\r
+\r
+SetShellVarContext all\r
+CreateShortCut "$DESKTOP\Pentobi.lnk" "$INSTDIR\Pentobi.exe"\r
+\r
+SectionEnd\r
+\r
+Section "Uninstall"\r
+\r
+Delete "$INSTDIR\Uninstall.exe"\r
+Delete "$INSTDIR\Pentobi.exe"\r
+Delete "$INSTDIR\COPYING.txt"\r
+Delete "$INSTDIR\pentobi.ico"\r
+Delete "$INSTDIR\blksgf.ico"\r
+Delete "$INSTDIR\*.dll"\r
+RmDir /r "$INSTDIR\books"\r
+RmDir /r "$INSTDIR\translations"\r
+RmDir /r "$INSTDIR\help"\r
+RmDir /r "$INSTDIR\platforms"\r
+RmDir /r "$INSTDIR\imageformats"\r
+RmDir /r "$INSTDIR\plugins"\r
+RmDir "$INSTDIR"\r
+\r
+SetShellVarContext all\r
+Delete "$SMPROGRAMS\Games\Pentobi.lnk"\r
+Delete "$DESKTOP\Pentobi.lnk"\r
+\r
+DeleteRegKey HKLM "Software\Pentobi"\r
+DeleteRegKey HKLM "${UNINST_KEY}"\r
+DeleteRegKey HKCR "Pentobi"\r
+DeleteRegKey HKCR "Applications\Pentobi.exe"\r
+DeleteRegKey HKCR "Applications\Pentobi"\r
+\r
+SectionEnd\r