--- /dev/null
+Main developer:
+
+Markus Enzenberger <enz@users.sourceforge.net>
+
+Translators:
+
+Allan Nordhøy <epost@anotheragency.no> (Norsk bokmål)
+Markus Enzenberger <enz@users.sourceforge.net> (German, French)
--- /dev/null
+cmake_minimum_required(VERSION 3.1.0)
+
+project(Pentobi)
+set(PENTOBI_VERSION 16.2)
+set(PENTOBI_RELEASE_DATE 2019-01-16)
+
+cmake_policy(SET CMP0043 NEW)
+cmake_policy(SET CMP0071 NEW)
+
+include(GNUInstallDirs)
+
+option(PENTOBI_BUILD_GTP "Build GTP interface" OFF)
+option(PENTOBI_BUILD_GUI "Build GUI" ON)
+option(PENTOBI_BUILD_TESTS "Build unit tests" OFF)
+option(PENTOBI_BUILD_THUMBNAILER "Build Gnome thumbnailer" ON)
+option(PENTOBI_BUILD_KDE_THUMBNAILER "Build KDE thumbnailer" OFF)
+option(PENTOBI_OPEN_HELP_EXTERNALLY "Force using web browser for displaying help" OFF)
+
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ message(STATUS "No build type selected, default to Release")
+ set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
+endif()
+
+set(CMAKE_CXX_STANDARD 14)
+if(CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang"))
+ add_compile_options(-ffast-math -Wall -Wextra)
+endif()
+
+if(PENTOBI_BUILD_TESTS)
+ if(PENTOBI_BUILD_KDE_THUMBNAILER)
+ configure_file(CTestCustom.cmake ${CMAKE_BINARY_DIR} COPYONLY)
+ endif()
+ enable_testing()
+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)
+
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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
+<https://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
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
--- /dev/null
+# We don't want to run appstreamtest added by KDECMakeSettings if
+# PENTOBI_BUILD_KDE_THUMBNAILER because it requires an Internet connection
+# to check the screenshot images.
+set(CTEST_CUSTOM_TESTS_IGNORE appstreamtest)
--- /dev/null
+This file explains how to compile and install Pentobi from the sources.
+
+
+== Requirements ==
+
+Pentobi requires the Qt libraries (>=5.11). The C++ compiler needs C++14
+support (GCC >=4.9). The build system uses CMake (>=3.1.0).
+
+In Debian-based distributions that support Qt >=5.11, the necessary tools
+and libraries can be installed with the command:
+
+ sudo apt-get install cmake g++ libqt5svg5-dev libqt5webview5-dev make \
+ qml-module-qt-labs-folderlistmodel qml-module-qt-labs-settings \
+ qml-module-qtquick2 qml-module-qtquick-controls2 \
+ qml-module-qtquick-layouts qml-module-qtquick-window2 \
+ qml-module-qtwebview qtquickcontrols2-5-dev qttools5-dev
+
+
+== Building ==
+
+Pentobi can be compiled from the source directory with the command:
+
+ 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 libkf5kio-dev and extra-cmake-modules on
+Debian-based distributions). Note that the plugin might 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 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 Debian-based distributions 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 ==
+
+Because building, deploying and debugging for Android is not yet functional
+for CMake projects in QtCreator, there exists a project file in
+src/pentobi/Pentobi.pro for building the Android app.
--- /dev/null
+Version 16.2 (16 Jan 2019)
+==========================
+
+* As a workaround for platforms without support for Qt5WebView, Pentobi can
+ now be built such that the help is displayed in an external web browser.
+ This option will automatically be used if Qt5WebView is not found or if the
+ cmake option -DPENTOBI_OPEN_HELP_EXTERNALLY=ON is used. Note that this
+ requires that a web browser is installed.
+* The help files are no longer compiled into the resources but installed
+ again in DATAROOTDIR/help.
+* Fixed keyboard navigation in file dialog.
+* Status message was not shown after successful Export/ASCII Art and
+ Android Media Scanner was not informed to make the saved file
+ immediately visible to MTP-connected devices.
+* Changed new-game icon, which looked too much like a ratings/bookmarks
+ icon.
+* Fixed compilation on systems without sys/sysctl.h header.
+* Enabled QML compiler again, now that QTBUG-70976 has been fixed, which
+ broke translations in Qt 5.12 beta releases.
+* Changed android.app_extract_android_style in Android manifest from none
+ to minimal, which is recommended for Quick Controls 2 apps (see
+ QTBUG-69810 and comments in QTBUG-71902)
+
+
+Version 16.1 (11 Oct 2018)
+==========================
+
+* Fixed alignment issues of pieces on board if high-DPI scaling is used.
+
+
+Version 16.0 (10 Oct 2018)
+==========================
+
+* The desktop version of Pentobi now uses the same QtQuick-based GUI as
+ the Android version, which makes the desktop version support all
+ features of the Android version like piece animations, dark and light
+ themes and more of the state saved between sessions (e.g. position in
+ game tree, modifications to loaded file, current analysis)
+* The minimum required Qt version is now 5.11 also for the desktop version
+ See INSTALL for the new run-time and compile-time dependencies.
+* The installation directory /usr/share/pentobi does not exist anymore.
+ The translations, opening books and user manual are compiled as resources
+ into the binary executable.
+* New themes optimized for colorblindness.
+* New appearance option in desktop mode that handles the comment visibility
+ after a position change.
+* New context menu to go to any played move on board or edit its annotation.
+* Additional warning dialogs to reduce the likelihood that an autosaved
+ game is lost, for example because it was changed by another instance
+ of Pentobi.
+* The computer level is now set in the computer colors dialog. It is no
+ longer stored in the settings separately for each game variant.
+* The visibility of the move number and variation information in the status
+ bar can now be configured in the appearance dialog and is off by default.
+* New toolbar button to stop computer play or game analysis.
+* Play and undo buttons now support autorepeat.
+* Reintroduced forward10/backward10 toolbuttons on desktop.
+* New shortcut keys for moving the selected piece in larger steps on the
+ board.
+* New menu item Recent Files/Clear List.
+* The locations and file format for the rated game history is not compatible
+ with Pentobi 15.0, the rating history will be lost.
+* New shortcut Ctrl+Shift+H, which behaves like Find Move (Ctrl+H) but
+ iterates backwards through the list of legal moves.
+* The game analysis now always contains a value for the position after
+ the last move, which is useful for analyzing unfinished games.
+* The Android version now shows an error before open/save if permission
+ to access storage have not been granted.
+* Android: color dot of the color to play is no longer surrounded by a border
+ because the color to play is already indicated by having its unplayed pieces
+ at the top.
+* The translation source strings for menu items and actions no longer use
+ an ampersand to mark a mnemonic but a separate translation string for
+ the mnemonic.
+
+Bug fixes (both desktop and Android version of Pentobi 15.0):
+
+* Fixed bugs in handling AE (add empty) SGF property.
+
+Bug fixes (Android version of Pentobi 15.0):
+
+* Picking up a piece from board in setup mode sometimes switched piece
+ instances in game variants with multiple instances per piece.
+* Workaround for a bug that made the analysis graph only partially visible
+ on Android low-density devices (QTBUG-69102)
+* Program could hang or crash if quit during running game analysis.
+* Board was not updated if it became empty after opening a file failed.
+* Running computer move was not aborted after opening a file failed.
+* Disable menu item Analyze Game if game has no moves.
+* Show a meaningful error message if startup fails due to low memory.
+* Show a warning if current game has unsaved changes before opening
+ a file from clipboard.
+* Changed text color on purple pieces to white to make it more readable.
+* Game info was not updated after loading a file.
+* Rating dialog did not show game variant in Callisto (2 players, 4 colors)
+* Don't crash if game analysis stored in settings was not valid.
+* Game was not marked as modified after changing move annotation.
+
+
+Version 15.0 (28 Jun 2018)
+==========================
+
+General:
+
+* New UI translations: French, Norsk bokmål (thanks to Allan Nordhøy)
+* Added a workaround for a compiler issue with GCC 7/8, which slowed
+ down the startup time of Pentobi.
+* Disable menu item "Keep Only Position" if board is empty.
+
+Android version:
+
+* The minimum required Qt version is now 5.11.
+* Games table in rating dialog did not show the correct level used.
+* Saved files should now immediately be visible from computers connecting
+ with the Android device via MTP (might not work on all devices).
+* An error message is now shown when an invalid loaded SGF file causes
+ a problem later (e.g. invalid move property value in a side variation).
+
+
+Version 14.1 (03 Jan 2018)
+==========================
+
+General:
+
+* Fixed a potential race condition during move generation.
+* Reduced maximum memory usage to a quarter instead of a third of the
+ total system memory.
+* Made unit tests work again.
+
+Android version:
+
+* Migrated QML files from Qt 5.6 to Qt >=5.7.
+* The binary translation files are now automatically created by the
+ qmake project file.
+
+
+Version 14.0 (26 Oct 2017)
+==========================
+
+General:
+
+* Increased playing strength in almost all game variants (except for
+ Nexos), especially in Trigon, GembloQ and Callisto.
+* Duo now uses the colors purple/orange.
+* Junior now uses the colors green/orange.
+* File format: accept whitespaces before and after property identifiers.
+
+Desktop version:
+
+* Minimum required Qt version is now 5.6.
+* Bugfix: dot indicating color to play in orientation selector was not
+ always updated correctly after loading a file of a different game
+ variant.
+* Bugfix: added missing include that broke compilation on FreeBSD 11.
+
+
+Version 13.1 (06 Jun 2017)
+==========================
+
+General:
+
+* Fixed some crashes that could be triggered by invalid SGF files.
+
+Desktop version:
+
+* Callisto: selected piece was wrongly rendered as one-piece in some
+ situations if partially outside board.
+* Fixed Leave Fullscreen button positioning if multiple screens exist.
+* Window close button did not work in message dialogs with detailed text.
+* Ctrl-W now closes application.
+* Use reverse-domain file names for appstream and desktop file.
+* Removed no longer needed workaround for disabling appstreamtest
+ added by KDECMakeSettings.
+
+Android version:
+
+* Displayed game variant was not changed when loading a file of a
+ different game variant with SGF errors.
+
+
+Version 13.0 (17 Mar 2017)
+==========================
+
+General:
+
+* New game variant GembloQ.
+* New game subvariant Callisto Two-Player Four-Color.
+* Slightly increased playing strength in Callisto, Trigon and Nexos.
+* New menu item Game/Open From Clipboard.
+* The engine now uses up to 8 threads (instead of 4) by default if the CPU
+ has enough hardware threads.
+* Support for SGF file encodings other than ISO-8859-1 and UTF-8.
+
+Desktop version:
+
+* Install AppData file to /usr/share/metainfo instead of /usr/share/appdata.
+* Added AppStream file for the KDE thumbnailer.
+* Disabled AppStream tests added by KDECMakeSettings that are broken in some
+ versions of KDE and made the project tests fail.
+* The compilation now requires CMake >=3.1.0.
+
+Android version:
+
+* The Android version now supports most features of the desktop version,
+ including comments, move annotations, setup positions, game analysis (only
+ a very fast mode) and rated games. The playing levels are still restricted
+ to 1-7 because the top levels would be too slow on mobile devices.
+* Current game position, associated file name and file modification status
+ are now remembered between sessions.
+* Opened games now show the initial instead of the last position if the
+ initial position contains either a setup or a comment.
+* Game/Find Move now behaves like in the desktop version and will cycle
+ through all legal moves if called repeatedly.
+* Added a light theme in addition to the default dark theme.
+* New menu item View/Fullscreen to make better use of small-screen displays.
+* Bugfix: game variant Junior erroneously used level set for Classic.
+* The minimum required Android version is now 4.1.
+
+
+Version 12.2 (05 Jan 2017)
+==========================
+
+Desktop version:
+
+* Added patterns for Nexos and Callisto SGF files to MIME type
+ specification for detecting them independently of the file ending.
+* 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 in updating the analysis window that could cause
+ a crash 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.
+
+It has a strong Blokus engine with 9 different playing levels.
+The supported game variants are: Classic, Duo, Trigon, Junior, Nexos,
+GembloQ and Callisto.
+
+See INSTALL for instructions how to build the program from the sources.
+See NEWS for release notes.
+See AUTHORS for a full list of copyright holders.
+The homepage of Pentobi is at https://pentobi.sourceforge.io
+
+Copyright (C) 2011-2019 Markus Enzenberger <enz@users.sourceforge.net>
+
+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 <https://www.gnu.org/licenses/>.
+
+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.
--- /dev/null
+if(PENTOBI_BUILD_GUI)
+
+foreach(icon application-x-blokus-sgf application-x-blokus-sgf-16
+ application-x-blokus-sgf-32 application-x-blokus-sgf-64)
+ set(icon_svg "${CMAKE_CURRENT_SOURCE_DIR}/${icon}.svg")
+ set(icon_png "${CMAKE_CURRENT_BINARY_DIR}/${icon}.png")
+ add_custom_command(OUTPUT "${icon_png}"
+ COMMAND convert "${icon_svg}" "${icon_png}" DEPENDS "${icon_svg}")
+ list(APPEND png_icons "${icon_png}")
+endforeach()
+foreach(icon pentobi pentobi-16 pentobi-32 pentobi-64)
+ set(icon_svg "${CMAKE_SOURCE_DIR}/src/icon/${icon}.svg")
+ set(icon_png "${CMAKE_CURRENT_BINARY_DIR}/${icon}.png")
+ add_custom_command(OUTPUT "${icon_png}"
+ COMMAND convert "${icon_svg}" "${icon_png}" DEPENDS "${icon_svg}")
+ list(APPEND png_icons "${icon_png}")
+endforeach()
+add_custom_target(data_icons ALL DEPENDS ${png_icons})
+
+configure_file(io.sourceforge.pentobi.desktop.in io.sourceforge.pentobi.desktop @ONLY)
+configure_file(io.sourceforge.pentobi.appdata.xml.in io.sourceforge.pentobi.appdata.xml @ONLY)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/48x48/apps)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi-16.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/16x16/apps
+ RENAME pentobi.png)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/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/icon/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 ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf-32.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/32x32/mimetypes
+ RENAME application-x-blokus-sgf.png)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/application-x-blokus-sgf-64.png
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/64x64/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}/io.sourceforge.pentobi.desktop
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications)
+install(FILES pentobi-mime.xml
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/mime/packages)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.appdata.xml
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo)
+
+endif(PENTOBI_BUILD_GUI)
+
+if(PENTOBI_BUILD_THUMBNAILER)
+ configure_file(pentobi.thumbnailer.in pentobi.thumbnailer @ONLY)
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pentobi.thumbnailer
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/thumbnailers)
+endif()
+
+if(PENTOBI_BUILD_KDE_THUMBNAILER)
+ configure_file(io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in
+ io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml @ONLY)
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo)
+endif()
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="15" width="13" stroke="#7c7f79" y=".5" x="1.5" fill="#eeeeec"/>
+ <path d="m4.0004 10-0.0004 1.321c-0.001 0.375 0.3007 0.677 0.675 0.679h1.325v-1.999l-1.3324-0.001z" stroke-width=".2" fill="#edd400"/>
+ <g id="f" transform="matrix(.2 0 0 .2 3.2 3.2)">
+ <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-8,-8)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-8,-16)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.2 0 0 .2 3.2 3.2)">
+ <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-8)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-8,-8)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.2 0 0 .2 3.2 3.2)">
+ <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0,-8)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-8,-16)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.2 0 0 .2 5.2 3.2)">
+ <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.2 0 0 .2 8.641 2.306)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m5.9992 9.9986-0.19922 0.19922v1.6h-1.6l-0.00196 0.002c0.12197 0.12174 0.29018 0.19698 0.47656 0.19805h1.3254v-1.9992h-0.00078z" stroke-width=".2" fill="#c4a000"/>
+ <path d="m4.0004 9.998-0.0004 1.321c-0.0005 0.189 0.0752 0.358 0.198 0.481l0.002-0.002v-1.6h1.6l0.19922-0.19922-1.3316-0.00078h-0.66718z" stroke-width=".2" fill="#fce94f"/>
+ <g transform="matrix(.2 0 0 .2 -3.005 4.6281)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.2 0 0 .2 -.9998 -1.3)">
+ <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0,2)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(2)" height="100%" width="100%" y="0" x="0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="27" width="23" stroke="#555753" y="2.5" x="4.5" fill="#eeeeec"/>
+ <path d="m10.001 19-0.001 1.982c-0.0015 0.563 0.451 1.015 1.012 1.018h1.988v-2.999l-1.999-0.001z" stroke-width="0.3" fill="#edd400"/>
+ <g id="f" transform="matrix(.3 0 0 .3 8.8 8.8)">
+ <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-7,-7)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-7,-14)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.3 0 0 .3 8.8 8.8)">
+ <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-7)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-7,-7)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.3 0 0 .3 8.8 8.8)">
+ <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0,-7)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-7,-14)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.3 0 0 .3 11.8 8.8)">
+ <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.3 0 0 .3 16.962 7.459)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m12.999 18.998-0.29883 0.29883v2.4h-2.4l-0.0029 0.0029c0.18296 0.1826 0.43527 0.29547 0.71484 0.29707h1.9881v-2.9988h-0.0012z" stroke-width="0.3" fill="#c4a000"/>
+ <path d="m10.001 18.997-0.001 1.982c-0.00078 0.2823 0.11277 0.5367 0.29706 0.7209l0.003-0.003v-2.4h2.4l0.29882-0.29883-1.9974-0.0012h-1.0008z" stroke-width="0.3" fill="#fce94f"/>
+ <g transform="matrix(.3 0 0 .3 -.5075 10.942)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.3 0 0 .3 2.5003 2.05)">
+ <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0,3)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(3)" height="100%" width="100%" y="0" x="0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="55" width="47" stroke="#555753" y="4.5" x="8.5" fill="#eeeeec"/>
+ <path d="m18.001 39-0.001 4.624c-0.0036 1.3125 1.0523 2.3674 2.3625 2.3751h4.638v-6.9971l-4.6635-0.0028z" stroke-width=".69999" fill="#edd400"/>
+ <g id="f" transform="matrix(.7 0 0 .69999 15.2 15.2)">
+ <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-3 -2.9999)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-3 -5.9999)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.7 0 0 .69999 15.2 15.2)">
+ <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-3,2.4e-4)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-3 -2.9997)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.7 0 0 .69999 15.2 15.2)">
+ <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0 -2.9998)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-3 -5.9998)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.7 0 0 .69999 22.2 15.2)">
+ <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 34.244 12.071)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m24.997 38.995-0.69726 0.69725v5.5999h-5.6l-0.0069 0.0069c0.4269 0.42607 1.0156 0.68941 1.668 0.69315h4.6389v-6.9972h-0.0027z" stroke-width=".69999" fill="#c4a000"/>
+ <path d="m18.001 38.993-0.001 4.624c-0.0018 0.65869 0.26313 1.2523 0.69314 1.6821l0.007-0.006v-5.5999h5.5999l0.69726-0.69725-4.6607-0.0027h-2.3351z" stroke-width=".69999" fill="#fce94f"/>
+ <g transform="matrix(.7 0 0 .69999 -6.5175 20.198)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 .5007 -.54978)">
+ <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0 7)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(7,10e-5)" height="100%" width="100%" y="0" x="0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect stroke-linejoin="round" ry="1.15" height="41" width="35" stroke="#555753" y="3.5" x="6.5" fill="#eeeeec"/>
+ <path d="m14.001 29-0.001 3.303c-0.0025 0.93748 0.75165 1.691 1.6875 1.6965h3.312v-4.9979l-3.331-0.002z" stroke-width=".49999" fill="#edd400"/>
+ <g id="f" transform="matrix(.5 0 0 .49999 12 12)">
+ <rect height="10" width="10" y="4" x="24" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use xlink:href="#f" transform="translate(-5 -4.9999)" height="100%" width="100%" y="10" x="10"/>
+ <use xlink:href="#f" transform="translate(-5 -9.9999)" height="100%" width="100%" y="20" x="10"/>
+ <g id="e" transform="matrix(.5 0 0 .49999 12 12)">
+ <rect height="10" width="10" y="24" x="14" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use xlink:href="#e" transform="translate(-5,3.4e-4)" height="100%" width="100%" y="0" x="10"/>
+ <use xlink:href="#e" transform="translate(-5 -4.9997)" height="100%" width="100%" y="10" x="10"/>
+ <g id="d" transform="matrix(.5 0 0 .49999 12 12)">
+ <rect height="10" width="10" y="14" x="4" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use xlink:href="#d" transform="translate(0 -4.9998)" height="100%" width="100%" y="10" x="0"/>
+ <use xlink:href="#d" transform="translate(-5 -9.9998)" height="100%" width="100%" y="20" x="10"/>
+ <g id="b" transform="matrix(.5 0 0 .49999 17 12)">
+ <rect height="10" width="10" y="4" x="4" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.5 0 0 .49999 25.602 9.765)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 0.002 0.002z" fill="#ef2929"/>
+ </g>
+ <path d="m18.998 28.996-0.49804 0.49804v3.9999h-4l-0.0049 0.0049c0.30493 0.30433 0.72545 0.49244 1.1914 0.4951h3.3135v-4.998h-0.002z" stroke-width=".49999" fill="#c4a000"/>
+ <path d="m14.001 28.995-0.001 3.303c-0.0013 0.47049 0.18795 0.89448 0.4951 1.2015l0.005-0.005v-3.9999h4l0.49804-0.49804-3.329-0.002h-1.668z" stroke-width=".49999" fill="#fce94f"/>
+ <g transform="matrix(.5 0 0 .49999 -3.5125 15.57)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.5 0 0 .49999 1.5005 .75022)">
+ <path d="m54.999 26.5v9.9961l6.664 0.004h3.336v-6.627c-0.006-1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-0.000355-0.000355-0.0016 0.000355-0.002 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" xlink:href="#b" transform="translate(0,4.9999)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(5,2.4e-4)" height="100%" width="100%" y="0" x="0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<component type="desktop-application">
+ <id>io.sourceforge.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, GembloQ.</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, GembloQ.</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.5 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,5 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">
+ https://pentobi.sourceforge.io/pentobi-classic.png</image>
+ <caption>Game variant Classic</caption>
+ <caption xml:lang="de">Spielvariante Klassisch</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ https://pentobi.sourceforge.io/pentobi-duo.png</image>
+ <caption>Game variant Duo</caption>
+ <caption xml:lang="de">Spielvariante Duo</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ https://pentobi.sourceforge.io/pentobi-trigon.png</image>
+ <caption>Game variant Trigon</caption>
+ <caption xml:lang="de">Spielvariante Trigon</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ https://pentobi.sourceforge.io/pentobi-nexos.png</image>
+ <caption>Game variant Nexos</caption>
+ <caption xml:lang="de">Spielvariante Nexos</caption>
+ </screenshot>
+ <screenshot>
+ <image width="1248" height="702">
+ https://pentobi.sourceforge.io/pentobi-gembloq.png</image>
+ <caption>Game variant GembloQ</caption>
+ <caption xml:lang="de">Spielvariante GembloQ</caption>
+ </screenshot>
+ </screenshots>
+
+ <url type="homepage">https://pentobi.sourceforge.io/</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>
+ <translation type="qt">pentobi</translation>
+
+ <releases>
+ <release version="@PENTOBI_VERSION@" date="@PENTOBI_RELEASE_DATE@"/>
+ </releases>
+</component>
--- /dev/null
+[Desktop Entry]
+Name=Pentobi
+Comment=Computer opponent for the board game Blokus
+Comment[de]=Computer-Gegner für das Brettspiel Blokus
+Comment[fr]=Un adversaire d'ordinateur pour le jeu Blokus.
+Comment[nb_NO]=Datamaskinmotstander for brettspillet Blokus.
+Keywords=Blokus;Blokus Duo;Blokus Trigon;Blokus Junior;Nexos;Callisto;Gemblo Q;GembloQ
+Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi %f
+Icon=pentobi
+Type=Application
+Categories=Game;BoardGame;
+MimeType=application/x-blokus-sgf;
+StartupWMClass=Pentobi
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<component type="addon">
+ <id>io.sourceforge.pentobi.kde_thumnailer</id>
+ <extends>org.kde.dolphin.desktop</extends>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-3.0+</project_license>
+ <name>Pentobi KDE Thumbnailer</name>
+ <name xml:lang="de">Pentobi-Vorschaubilder unter KDE</name>
+ <summary>Enables previews of game files written by Pentobi on KDE</summary>
+ <summary xml:lang="de">Ermöglicht Vorschaubilder von Spieldateien, die von Pentobi geschrieben wurden, unter KDE</summary>
+
+ <description>
+ <p>Plugin that enables previews of Blokus game files as written by the
+ program Pentobi in the Dolphin file manager of the KDE desktop
+ environment.</p>
+ <p xml:lang="de">Plug-in, das Vorschaubilder von Blokus-Spieldateien,
+ wie vom Programm Pentobi erzeugt, im Dateimanager Dolphin der
+ KDE-Desktop-Umgebung ermöglicht.</p>
+ </description>
+
+ <screenshots>
+ <screenshot type="default">
+ <image width="1248" height="702">
+ https://pentobi.sourceforge.io/pentobi-kde-thumbnailer.png</image>
+ <caption>Game file previews in Dolphin</caption>
+ <caption xml:lang="de">Vorschaubilder von Spieldateien in Dolphin</caption>
+ </screenshot>
+ </screenshots>
+
+ <url type="homepage">https://pentobi.sourceforge.io/</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>
+
+ <releases>
+ <release version="@PENTOBI_VERSION@" date="@PENTOBI_RELEASE_DATE@"/>
+ </releases>
+</component>
--- /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>
+<comment xml:lang="fr">Partie de Blokus</comment>
+<comment xml:lang="nb_NO">Blokus-spill</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[Callisto Two-Player Four-Color]"/>
+<match type="string" offset="0:256" value="GM[GembloQ]"/>
+<match type="string" offset="0:256" value="GM[GembloQ Three-Player]"/>
+<match type="string" offset="0:256" value="GM[GembloQ Two-Player]"/>
+<match type="string" offset="0:256" value="GM[GembloQ Two-Player Four-Color]"/>
+<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
+[Thumbnailer Entry]
+Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi-thumbnailer --size %s %i %o
+MimeType=application/x-blokus-sgf;
--- /dev/null
+if(PENTOBI_BUILD_GUI)
+ install(DIRECTORY help DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}
+ FILES_MATCHING PATTERN "*.css" PATTERN "*.html" PATTERN "*.png"
+ PATTERN "*.jpg")
+endif()
+
+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">
+<style type="text/css">
+html { background-color: lightgray; }
+body {
+ background-color:white;
+ color:black;
+ font-size:17px;
+ line-height:23px;
+ max-width:60em;
+ margin:auto;
+ padding:15px;
+ min-height: 100vh;
+}
+a:link { text-decoration:none; color:blue; }
+a:visited { text-decoration:none; color:purple; }
+</style>
+</head>
+<body>
+<h1>Pentobi SGF Files</h1>
+<div style="font-size:small">Author: Markus Enzenberger<br>
+Last modified: 2017-09-16</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="https://pentobi.sourceforge.io">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>
+<p style="font-size:small"><b>Note</b><br>
+Older versions of Pentobi (up to version 13.1) did not accept whitespaces
+before and after property identifiers, so it is recommended to avoid them for
+compatibility.</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><a href="http://en.wikipedia.org/wiki/UTF-8">UTF-8</a> should be used as the
+character set. Pentobi always writes files in UTF-8 and indicates that with the
+<tt>CA</tt> property. Pentobi versions before 13.0 can only read SGF files
+encoded in UTF-8 or ISO-8859-1 (Latin1). 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 Two-Player Four-Color, Callisto Three-Player, GembloQ,
+GembloQ Two-Player, GembloQ Three-Player, GembloQ Two-Player Four-Color.</p>
+The strings are case-sensitive, words must be separated by exactly one space
+and must not contain whitespaces at the beginning or end of the string.
+<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>PB</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 must not 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
+GembloQ.</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>
+<p>In GembloQ, each square field is divided into four triangles with their own
+coordinates, like in this example:</p>
+<pre>
+ 4 | / | \ | / | \ | /
+ 3 | \ | / | \ | / | \
+ 2 | / | \ | / | \ | /
+ 1 | \ | / | \ | / | \
+ a b c d e f g h i
+</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
+<!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">
+<style type="text/css">
+html { background-color: lightgray; }
+body {
+ background-color:white;
+ color:black;
+ font-size:17px;
+ line-height:23px;
+ max-width:60em;
+ margin:auto;
+ padding:15px;
+ min-height: 100vh;
+}
+a:link { text-decoration:none; color:blue; }
+a:visited { text-decoration:none; color:purple; }
+</style>
+</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="https://pentobi.sourceforge.io">Pentobi</a>. The interface is
+an adaption of the <a href="https://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=
+"https://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, nexos, nexos_2, gembloq,
+gembloq_2, gembloq_3, gembloq_2_4, callisto, callisto_2, callisto_3,
+callisto_2_4 or the abbreviations c, c2, d, t, t2, t3, j, n, n2, g, g2, g3,
+g24, ca, ca2, ca3, ca24. 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 disabled, 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 (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 8) is untested
+and might reduce the playing strength 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 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=
+"https://pentobi.sourceforge.io/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>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>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=
+"https://pentobi.sourceforge.io/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
+<!-- Help files for use in pentobi_qml -->
+<RCC>
+ <qresource prefix="/qml">
+ <file>help/C/pentobi/analysis.jpg</file>
+ <file>help/C/pentobi/become_stronger.html</file>
+ <file>help/C/pentobi/board_callisto.png</file>
+ <file>help/C/pentobi/board_classic.png</file>
+ <file>help/C/pentobi/board_duo.png</file>
+ <file>help/C/pentobi/board_gembloq.png</file>
+ <file>help/C/pentobi/board_nexos.png</file>
+ <file>help/C/pentobi/board_trigon.jpg</file>
+ <file>help/C/pentobi/callisto_rules.html</file>
+ <file>help/C/pentobi/classic_rules.html</file>
+ <file>help/C/pentobi/duo_rules.html</file>
+ <file>help/C/pentobi/gembloq_rules.html</file>
+ <file>help/C/pentobi/index.html</file>
+ <file>help/C/pentobi/junior_rules.html</file>
+ <file>help/C/pentobi/license.html</file>
+ <file>help/C/pentobi/nexos_rules.html</file>
+ <file>help/C/pentobi/pieces_callisto.png</file>
+ <file>help/C/pentobi/pieces_gembloq.jpg</file>
+ <file>help/C/pentobi/pieces_junior.png</file>
+ <file>help/C/pentobi/pieces_nexos.png</file>
+ <file>help/C/pentobi/pieces.png</file>
+ <file>help/C/pentobi/pieces_trigon.jpg</file>
+ <file>help/C/pentobi/position_callisto.png</file>
+ <file>help/C/pentobi/position_classic.png</file>
+ <file>help/C/pentobi/position_duo.png</file>
+ <file>help/C/pentobi/position_gembloq.png</file>
+ <file>help/C/pentobi/position_nexos.png</file>
+ <file>help/C/pentobi/position_trigon.jpg</file>
+ <file>help/C/pentobi/rating.jpg</file>
+ <file>help/C/pentobi/shortcuts.html</file>
+ <file>help/C/pentobi/stylesheet.css</file>
+ <file>help/C/pentobi/system.html</file>
+ <file>help/C/pentobi/trigon_rules.html</file>
+ <file>help/C/pentobi/user_interface.html</file>
+ <file>help/C/pentobi/window_menu.html</file>
+ <file>help/de/pentobi/become_stronger.html</file>
+ <file>help/de/pentobi/callisto_rules.html</file>
+ <file>help/de/pentobi/classic_rules.html</file>
+ <file>help/de/pentobi/duo_rules.html</file>
+ <file>help/de/pentobi/gembloq_rules.html</file>
+ <file>help/de/pentobi/index.html</file>
+ <file>help/de/pentobi/junior_rules.html</file>
+ <file>help/de/pentobi/license.html</file>
+ <file>help/de/pentobi/nexos_rules.html</file>
+ <file>help/de/pentobi/shortcuts.html</file>
+ <file>help/de/pentobi/system.html</file>
+ <file>help/de/pentobi/trigon_rules.html</file>
+ <file>help/de/pentobi/user_interface.html</file>
+ <file>help/de/pentobi/window_menu.html</file>
+ </qresource>
+</RCC>
--- /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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="user_interface.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="window_menu.html">Next</a></div>
+<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>
+<div class="fig"><img src="analysis.jpg" alt=""></div>
+<div 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 800.</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 whether the
+game was won, lost or a tie. The exact number of score points does not
+matter.</p>
+<div class="fig"><img src="rating.jpg" alt=""></div>
+<div 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 50 games as a graph. The last 50 games are automatically
+saved and can be loaded by opening a context menu in the game table below the
+graph.</p>
+<div class="nav"><a href="user_interface.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="window_menu.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="gembloq_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="user_interface.html">Next</a></div>
+<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>
+<div class="fig"><img src="pieces_callisto.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="board_callisto.png" alt=""></div>
+<div class="caption">The board with the center having a darker color</div>
+<p>All larger pieces may be placed anywhere on the board but must touch an
+existing piece of the same color edge-to-edge.</p>
+<div class="fig"><img src="position_callisto.png" alt=""></div>
+<div 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. In addition to the standard two-player
+variant, Pentobi also supports a game variant for two players like in Blokus,
+in which each player plays two colors.</p>
+<div class="nav"><a href="gembloq_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="user_interface.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="index.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="duo_rules.html">Next</a></div>
+<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 from a number of squares
+connected along the edges.</p>
+<div class="fig"><img src="pieces.png" alt=""></div>
+<div class="caption">The 21 pieces</div>
+<p>The players alternate in placing one of their pieces on the board. The first
+piece of a player must cover its starting square. The starting squares are
+located in the corners of the board.</p>
+<div class="fig"><img src="board_classic.png" alt=""></div>
+<div class="caption">The 20×20 board with the starting 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>
+<div class="fig"><img src="position_classic.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="index.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="duo_rules.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="trigon_rules.html">Next</a></div>
+<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 (Purple and Orange) and the starting squares are not in the corners, but
+on the square with the coordinates (5,10) for Purple, and on (10,5) for
+Orange.</p>
+<div class="fig"><img src="board_duo.png" alt=""></div>
+<div class="caption">The 14×14 board used in game variant Duo with the starting
+squares marked with colored dots</div>
+<div class="fig"><img src="position_duo.png" alt=""></div>
+<div class="caption">An example position in game variant Duo</div>
+<div class="nav"><a href="classic_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="trigon_rules.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="nexos_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="callisto_rules.html">Next</a></div>
+<h2>GembloQ Rules</h2>
+<p>GembloQ is a board game similar to Blokus. The squares of the board are
+rotated by 45 degrees. The board has a diagonal size of 27 squares. In addition
+to the full squares, the edges also contain half squares such that the edges
+are straight lines.</p>
+<div class="fig"><img src="pieces_gembloq.jpg" alt=""></div>
+<div class="caption">The 21 pieces</div>
+<p>Each player has a set of 21 pieces, which include a subset of the pieces
+used in Blokus, but also some pieces that contain a half square.</p>
+<div class="fig"><img src="board_gembloq.png" alt=""></div>
+<div class="caption">The board for GembloQ with the starting points marked with
+colored dots</div>
+<p>As in Blokus, the starting squares are in the corners, the first move must
+fully cover the starting square of the color and subsequent moves must touch a
+piece of the player color at a vertex, but not edge-to-edge. Moves are also
+legal, if a vertex of a half square touches an edge of a piece of the same
+color.</p>
+<div class="fig"><img src="position_gembloq.png" alt=""></div>
+<div class="caption">An example position after a few moves</div>
+<p>Scoring is done like in Blokus, half squares count 0.5 points. Bonus points
+are not used. Tie breaking by assigning additional values to the pieces, as
+described in the official GembloQ rules, is currently not supported by
+Pentobi.</p>
+<h3>Rules for Two and Three Players</h3>
+<p>The game variants for two and three players use smaller boards and different
+starting point locations. In addition to the standard two-player variant,
+Pentobi also supports a game variant for two players like in Blokus, in which
+each player plays two colors.</p>
+<div class="nav"><a href="nexos_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="callisto_rules.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Next</a></div>
+<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, GembloQ 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="gembloq_rules.html">GembloQ 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">Window Menu and Toolbar</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>
+<div class="nav"><a href="classic_rules.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="trigon_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="nexos_rules.html">Next</a></div>
+<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 polyominoes
+up to size five and the players get two of each of those polyominoes.</p>
+<div class="fig"><img src="pieces_junior.png" alt=""></div>
+<div class="caption">The 24 pieces used in Junior</div>
+<p>Bonus points are not used in Junior.</p>
+<div class="nav"><a href="trigon_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="nexos_rules.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="system.html">Previous</a> | <a href=
+"index.html">Contents</a></div>
+<h2>License</h2>
+<p>Copyright © 2011–2018 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>
+<div class="nav"><a href="system.html">Previous</a> | <a href=
+"index.html">Contents</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="junior_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="gembloq_rules.html">Next</a></div>
+<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>
+<div class="fig"><img src="pieces_nexos.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="board_nexos.png" alt=""></div>
+<div class="caption">The board for Nexos with the 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>
+<div class="fig"><img src="position_nexos.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="junior_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="gembloq_rules.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="window_menu.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="system.html">Next</a></div>
+<h2>Keyboard Shortcuts</h2>
+<p>In addition to the 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 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>Escape</dt>
+<dd>
+<p>Clear selected piece</p>
+</dd>
+<dt>Left, Right, Up, Down, Shift+Left, Shift+Right, Shift+Up, Shift+Down</dt>
+<dd>
+<p>Move the selected piece. The Shift key makes the piece move faster.</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>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>
+<dt>Ctrl+Home, Ctrl+Shift+Left, Ctrl+Left, Ctrl+Right, Ctrl+Shift+Right,
+Ctrl+End, Ctrl+Up, Ctrl+Down</dt>
+<dd>
+<p>Navigate in the game: beginning, ten moves backward, backward, forward, ten
+moves forward, end, previous variation, next variation.</p>
+</dd>
+<dt>Ctrl+Shift+H</dt>
+<dd>
+<p>Like <i>Find Move</i> (Ctrl+H) but iterates backwards through the list of
+legal moves.</p>
+</dd>
+<dt>Ctrl+T</dt>
+<dd>
+<p>Switch view between comment and game analysis.</p>
+</dd>
+<dt>Alt+M</dt>
+<dd>
+<p>Open menu.</p>
+</dd>
+</dl>
+<div class="nav"><a href="window_menu.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="system.html">Next</a></div>
+</body>
+</html>
--- /dev/null
+html {
+background-color: #eee;
+}
+
+body {
+color: black;
+background-color: white;
+max-width: 60em;
+margin: auto;
+padding: 0.7em;
+min-height: 100vh;
+}
+
+:link {
+text-decoration: none;
+color: blue;
+}
+
+:visited {
+text-decoration: none;
+color: purple;
+}
+
+:focus {
+outline-color: darkorange;
+}
+
+.fig {
+text-align: center;
+}
+
+.fig img {
+width: auto;
+height: auto;
+max-width: 90%;
+max-height: 90%;
+margin: 0.5em;
+}
+
+.caption {
+font-size: 90%;
+text-align: center;
+margin-left: 15%;
+margin-right: 15%;
+}
+
+.nav {
+text-align: right;
+margin-top: 0.5em;
+margin-bottom: 0.5em;
+}
--- /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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="shortcuts.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="license.html">Next</a></div>
+<h2>System Requirements</h2>
+<p>Minimum: 1 GB RAM, 1 GHz CPU<br>
+Recommended for playing level 9: 4 GB RAM, 2.5 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>
+<div class="nav"><a href="shortcuts.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="license.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="duo_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="junior_rules.html">Next</a></div>
+<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 from a number of equilateral triangles connected
+along the edges.</p>
+<div class="fig"><img src="pieces_trigon.jpg" alt=""></div>
+<div 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>
+<div class="fig"><img src="board_trigon.jpg" alt=""></div>
+<div class="caption">The board with the starting 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>
+<div class="fig"><img src="position_trigon.jpg" alt=""></div>
+<div 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>
+<div class="nav"><a href="duo_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="junior_rules.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="callisto_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="become_stronger.html">Next</a></div>
+<h2>How to Use Pentobi</h2>
+<h3>Board</h3>
+<p>Pieces can be selected by clicking on one of the unplayed pieces or by using
+<a href="shortcuts.html">shortcut keys</a>. Pieces can be played by dragging
+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>
+<p>Played pieces on the board can have numbers on them that indicate the move
+number in which the piece was played. A letter after the move number indicates
+that there exists a variation to this move (see below).</p>
+<p>The score display shows 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>Settings</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. 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 and the navigation buttons.</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
+pressing the <i>Forward</i> button). 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>
+<div class="nav"><a href="callisto_rules.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="become_stronger.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="become_stronger.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="shortcuts.html">Next</a></div>
+<h2>Window Menu and Toolbar</h2>
+<h3>Navigation Buttons in Toolbar</h3>
+<dl>
+<dt>Beginning</dt>
+<dd>Go to the beginning of the game.</dd>
+<dt>Backward 10</dt>
+<dd>Go ten moves backward in the current variation. The button supports
+autorepeat if pressed and held.</dd>
+<dt>Backward</dt>
+<dd>Go one move backward in the current variation. The button 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 button supports
+autorepeat if pressed and held.</dd>
+<dt>Forward 10</dt>
+<dd>Go ten moves forward in the current variation. If a 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 button 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>
+</dl>
+<h3>Game Menu</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>Open Clipboard</dt>
+<dd>Open a game from a text copied to the clipboard. The text must be a valid
+game in Pentobi SGF file format.</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). For a crisp image, the image width should be an integer
+multiple of the number of board columns in the current game variant.</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 Menu</h3>
+<dl>
+<dt>Move Number</dt>
+<dd>Go to the move with a given move number in the current variation.</dd>
+<dt>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>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 Menu</h3>
+<dl>
+<dt>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>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>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 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 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 Subtree</dt>
+<dd>Like <i>Keep 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 Menu</h3>
+<dl>
+<dt>Appearance</dt>
+<dd>
+<dl>
+<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>Move number</dt>
+<dd>This option exists only in desktop mode and shows the move number, move
+annotation and variation information at the right side of the status bar.</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>Show comment</dt>
+<dd>This option exists only in desktop mode and configures the visibility of
+the comment area when the position changes. By default, the comment area is
+only shown if a comment exists for the current position.</dd>
+</dl>
+</dd>
+<dt>Comment</dt>
+<dd>Toggle the visibility of the comment area in the current position.</dd>
+</dl>
+<dl>
+<dt>Fullscreen</dt>
+<dd>Make the main window full screen or leave full screen mode. To leave full
+screen mode without using the window menu, press the F11 key.</dd>
+</dl>
+<h3>Computer Menu</h3>
+<dl>
+<dt>Settings</dt>
+<dd>Select which colors are played by the computer and the playing strength for
+the computer. Higher levels are stronger but can make the computer take a long
+time for playing moves on slow computers.</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 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>
+</dl>
+<h3>Tools Menu</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 Menu</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>
+<div class="nav"><a href="become_stronger.html">Previous</a> | <a href=
+"index.html">Contents</a> | <a href="shortcuts.html">Next</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="user_interface.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="window_menu.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/analysis.jpg" alt=""></div>
+<div 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 800.</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 nicht 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>
+<div class="fig"><img src="../../C/pentobi/rating.jpg" alt=""></div>
+<div 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 50 Spiele als Graph gezeigt wird. Die letzten 50
+Spiele werden automatisch gespeichert und können durch Öffnen eines
+Kontextmenüs in der Spieltabelle unter dem Graph geladen werden.</p>
+<div class="nav"><a href="user_interface.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="window_menu.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="gembloq_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="user_interface.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces_callisto.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/board_callisto.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/position_callisto.png" alt=""></div>
+<div 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. Zusätzlich zur Standardvariante für zwei Spieler unterstützt
+Pentobi auch eine Spielvariante für zwei Spieler wie in Blokus, in der jeder
+Spieler zwei Farben spielt.</p>
+<div class="nav"><a href="gembloq_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="user_interface.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="index.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="duo_rules.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces.png" alt=""></div>
+<div class="caption">Die 21 Spielsteine</div>
+<p>Die Spieler setzen abwechselnd einen ihrer Spielsteine aufs Brett. Der erste
+Spielstein eines Spielers muss sein Startfeld abdecken. Die Startfelder
+befinden sich in den Ecken des Bretts.</p>
+<div class="fig"><img src="../../C/pentobi/board_classic.png" alt=""></div>
+<div class="caption">Das 20×20-Brett mit den durch farbige Punkte markierten
+Startfeldern</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>
+<div class="fig"><img src="../../C/pentobi/position_classic.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="index.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="duo_rules.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="trigon_rules.html">Weiter</a></div>
+<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 (Lila und Orange) und die Startfelder befinden sich nicht in
+den Ecken, sondern auf dem Feld mit den Koordinaten (5,10) für Lila und auf
+(10,5) für Orange.</p>
+<div class="fig"><img src="../../C/pentobi/board_duo.png" alt=""></div>
+<div class="caption">Das 14×14-Brett, das in der Spielvariante Duo benutzt
+wird, mit den durch farbige Punkte markierten Startfeldern</div>
+<div class="fig"><img src="../../C/pentobi/position_duo.png" alt=""></div>
+<div class="caption">Eine Beispielstellung in der Spielvariante Duo</div>
+<div class="nav"><a href="classic_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="trigon_rules.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="nexos_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="callisto_rules.html">Weiter</a></div>
+<h2>GembloQ-Regeln</h2>
+<p>GembloQ ist ein Brettspiel ähnlich wie Blokus. Die quadratischen Spielfelder
+des Bretts sind um 45 Grad gedreht. Die Diagonale des Bretts hat eine Größe von
+27 Quadraten. Zusätzlich zu den vollen Quadraten enthalten die Ränder noch
+halbe Quadrate, so dass die Ränder gerade Linien sind.</p>
+<div class="fig"><img src="../../C/pentobi/pieces_gembloq.jpg" alt=""></div>
+<div class="caption">Die 21 Spielsteine</div>
+<p>Jeder Spieler besitzt 21 Spielsteine, unter denen eine Teilmenge der in
+Blokus benutzten Spielsteine ist, jedoch zusätzlich einige Spielsteine, die ein
+halbes Quadrat beinhalten.</p>
+<div class="fig"><img src="../../C/pentobi/board_gembloq.png" alt=""></div>
+<div class="caption">Das Brett für GembloQ mit den durch farbige Punkte
+markierten Startfeldern</div>
+<p>Wie in Blokus sind die Startfelder in den Ecken, der erste Zug muss das
+Startfeld der Farbe vollständig abdecken und folgende Züge müssen einen
+Spielstein der Farbe am Zug an einer Ecke berühren, jedoch noch entlang der
+Kanten. Züge sind auch legal, wenn eine Spitze eines halben Quadrats die Kante
+eines Spielsteins der selben Farbe berührt.</p>
+<div class="fig"><img src="../../C/pentobi/position_gembloq.png" alt=""></div>
+<div class="caption">Eine Beispielstellung nach ein paar Zügen</div>
+<p>Die Punktzahl am Spielende wird wie in Blokus ermittelt, wobei halbe
+Quadrate 0,5 Punkte zählen. Bonuspunkte werden nicht verwendet. Ein Auflösen
+von Unentschieden durch Zuweisen von zusätzlichen Werten zu den Spielsteinen,
+wie in den offiziellen GembloQ-Regeln beschrieben, wird von Pentobi gegenwärtig
+nicht unterstützt.</p>
+<h3>Regeln für zwei und drei Spieler</h3>
+<p>Die Spielvarianten für zwei und drei Spieler benutzen kleinere Spielbretter
+und andere Positionen für die Startfelder. Zusätzlich zur Standardvariante für
+zwei Spieler unterstützt Pentobi auch eine Spielvariante für zwei Spieler wie
+in Blokus, in der jeder Spieler zwei Farben spielt.</p>
+<div class="nav"><a href="nexos_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="callisto_rules.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="classic_rules.html">Weiter</a></div>
+<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, GembloQ 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="gembloq_rules.html">GembloQ-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">Fenstermenü und Werkzeugleiste</a><br>
+<a href="shortcuts.html">Tastenkürzel</a><br>
+<a href="system.html">Systemvoraussetzungen</a><br>
+<a href="license.html">Lizenz</a></p>
+<div class="nav"><a href="classic_rules.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="trigon_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="nexos_rules.html">Weiter</a></div>
+<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 Polyominos bis zur Größe fünf und die Spieler bekommen zwei von
+jedem dieser Polyominos.</p>
+<div class="fig"><img src="../../C/pentobi/pieces_junior.png" alt=""></div>
+<div class="caption">Die 24 Spielsteine, die in Junior benutzt werden</div>
+<p>Bonuspunkte werden in Junior nicht benutzt.</p>
+<div class="nav"><a href="trigon_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="nexos_rules.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="system.html">Zurück</a> | <a href=
+"index.html">Inhalt</a></div>
+<h2>Lizenz</h2>
+<p>Copyright © 2011–2018 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>
+<div class="nav"><a href="system.html">Zurück</a> | <a href=
+"index.html">Inhalt</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="junior_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="gembloq_rules.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces_nexos.png" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/board_nexos.png" alt=""></div>
+<div class="caption">Das Brett für Nexos mit den durch farbige Punkte
+markierten Startkreuzungspunkten</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>
+<div class="fig"><img src="../../C/pentobi/position_nexos.png" alt=""></div>
+<div 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>
+<div class="nav"><a href="junior_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="gembloq_rules.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="window_menu.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="system.html">Weiter</a></div>
+<h2>Tastenkürzel</h2>
+<p>Zusätzlich zu den Tastenkürzeln, 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 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>Escape</dt>
+<dd>
+<p>Spielsteinauswahl löschen</p>
+</dd>
+<dt>Links, Rechts, Oben, Unten, Umschalt+Links, Umschalt+Rechts, Umschalt+Oben,
+Umschalt+Unten</dt>
+<dd>
+<p>Bewegen des ausgewählten Spielsteins. Mit der Umschalttaste wird der
+Spielstein schneller bewegt.</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>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>
+<dt>Strg+Pos1, Strg+Umschalt+Links, Strg+Links, Strg+Rechts,
+Strg+Umschalt+Rechts, Strg+Ende, Strg+Oben, Strg+Unten</dt>
+<dd>
+<p>Im Spiel navigieren: Anfang, zehn Züge zurück, zurück, vorwärts, zehn Züge
+vorwärts, Ende, vorherige Variante, nächste Variante.</p>
+</dd>
+<dt>Strg+Umschalt+H</dt>
+<dd>
+<p>Wie <i>Zug finden</i> (Strg+H), jedoch wird rückwärts durch die Liste der
+legalen Züge iteriert.</p>
+</dd>
+<dt>Strg+T</dt>
+<dd>
+<p>Ansicht zwischen Kommentar und Spielanalyse umschalten.</p>
+</dd>
+<dt>Alt+M</dt>
+<dd>
+<p>Menü öffnen.</p>
+</dd>
+</dl>
+<div class="nav"><a href="window_menu.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="system.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="shortcuts.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="license.html">Weiter</a></div>
+<h2>Systemvoraussetzungen</h2>
+<p>Minimum: 1 GB RAM, 1 GHz CPU<br>
+Empfohlen für Spielstufe 9: 4 GB RAM, 2,5 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>
+<div class="nav"><a href="shortcuts.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="license.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="duo_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="junior_rules.html">Weiter</a></div>
+<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>
+<div class="fig"><img src="../../C/pentobi/pieces_trigon.jpg" alt=""></div>
+<div 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>
+<div class="fig"><img src="../../C/pentobi/board_trigon.jpg" alt=""></div>
+<div class="caption">Das Brett mit den durch graue Punkte markierten
+Startfeldern</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>
+<div class="fig"><img src="../../C/pentobi/position_trigon.jpg" alt=""></div>
+<div 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>
+<div class="nav"><a href="duo_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="junior_rules.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="callisto_rules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="become_stronger.html">Weiter</a></div>
+<h2>Wie Sie Pentobi benutzen</h2>
+<h3>Spielbrett</h3>
+<p>Spielsteine können durch Klicken auf einen ungespielten Spielstein
+ausgewählt werden oder durch Benutzen von <a href=
+"shortcuts.html">Tastenkürzeln</a>. 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>
+<p>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>Die Punkteanzeige zeigt die gegenwärtigen Punkte für jede Farbe oder jeden
+Spieler an. 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>Einstellungen</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 für ähnliche Aufgaben benutzen will. 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> und den Navigations-Buttons
+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
+den <i>Vorwärts</i>-Button drücken). 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>
+<div class="nav"><a href="callistorules.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="become_stronger.html">Weiter</a></div>
+</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 name="viewport" content="width=device-width">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+</head>
+<body>
+<div class="nav"><a href="become_stronger.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="shortcuts.html">Weiter</a></div>
+<h2>Fenstermenü und Werkzeugleiste</h2>
+<h3>Navigations-Buttons in der Werkzeugleiste</h3>
+<dl>
+<dt>Anfang</dt>
+<dd>Geht zum Anfang des Spiels.</dd>
+<dt>Zurück 10</dt>
+<dd>Geht zehn Züge in der gegenwärtigen Variante zurück. Der Button unterstützt
+automatische Wiederholung, wenn er gedrückt gehalten wird.</dd>
+<dt>Zurück</dt>
+<dd>Geht einen Zug in der gegenwärtigen Variante zurück. Der Button 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 Button unterstützt automatische Wiederholung, wenn er
+gedrückt gehalten wird.</dd>
+<dt>Vorwärts 10</dt>
+<dd>Geht zehn Züge in der gegenwärtigen Variante vorwärts. Wenn eine
+Brettstellung mehrerer nachfolgende Varianten hat (d. h. der gegenwärtige
+Knoten im Spielbaum mehrere Kindknoten hat), wird die erste Variante benutzt.
+Der Button 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>
+</dl>
+<h3>Spiel-Menü</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>Zwischenablage öffnen</dt>
+<dd>Öffnet ein Spiel von einem Text, der in die Zwischenablage kopiert wurde.
+Der Text muss ein gültiges Spiel im Pentobi-SGF-Dateiformat sein.</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). Für ein gestochen scharfes
+Bild sollte die Bildbreite ein ganzzahliges Vielfaches der Spaltenanzahl des
+Bretts in der gegenwärtigen Spielvariante sein.</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-Menü</h3>
+<dl>
+<dt>Zugnummer</dt>
+<dd>Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen
+Variante.</dd>
+<dt>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ächster Kommentar</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-Menü</h3>
+<dl>
+<dt>Annotierung</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</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</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>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>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>Teilbaum behalten</dt>
+<dd>Wie <i>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-Menü</h3>
+<dl>
+<dt>Erscheinungsbild</dt>
+<dd>
+<dl>
+<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>Zugnummer</dt>
+<dd>Diese Option existiert nur im Desktop-Modus und zeigt die Zugnummer,
+Zugannotierung und Varianteninformation auf der rechten Seite der Statusleiste
+an.</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>Kommentar zeigen</dt>
+<dt>Sichtbarkeit des Kommentarbereichs, wenn sich die Stellung ändert. Die</dt>
+<dd>Diese Option existiert nur im Desktop-Modus und konfiguriert die
+Standardeinstellung ist, dass der Kommentarbereich nur sichtbar ist, wenn ein
+Kommentar zur aktuellen Stellung existiert.</dd>
+</dl>
+</dd>
+<dt>Kommentar</dt>
+<dd>Mach den Kommentarbereich in der aktuellen Stellung sichtbar oder nicht
+sichtbar.</dd>
+<dt>Vollbild</dt>
+<dd>Schaltet das Hauptfenster in den Vollbildmodus oder verlässt den
+Vollbildmodus. Um den Vollbildmodus ohne das Benutzen des Fenstermenüs zu
+verlassen, drücken Sie die F11-Taste.</dd>
+</dl>
+<h3>Computer-Menü</h3>
+<dl>
+<dt>Einstellungen</dt>
+<dd>Wählt aus, welche Farben vom Computer gespielt werden und 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>
+<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>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>
+</dl>
+<h3>Extras-Menü</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-Menü</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>
+<div class="nav"><a href="become_stronger.html">Zurück</a> | <a href=
+"index.html">Inhalt</a> | <a href="shortcuts.html">Weiter</a></div>
+</body>
+</html>
--- /dev/null
+configure_file(pentobi.6.in pentobi.6 @ONLY)
+install(FILES
+ ${CMAKE_CURRENT_BINARY_DIR}/pentobi.6
+ DESTINATION ${CMAKE_INSTALL_MANDIR}/man6)
+
+if(PENTOBI_BUILD_THUMBNAILER)
+ configure_file(pentobi-thumbnailer.6.in pentobi-thumbnailer.6 @ONLY)
+ install(FILES
+ ${CMAKE_CURRENT_BINARY_DIR}/pentobi-thumbnailer.6
+ DESTINATION ${CMAKE_INSTALL_MANDIR}/man6)
+endif()
--- /dev/null
+.TH PENTOBI-THUMBNAILER 6 "2017-04-17" "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.
+.TP
+.B \-h, \-\-help
+Display help and exit.
+
+.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 "2018-07-30" "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.
+.PP
+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 \-h, \-\-help
+Display help and exit.
+.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 quarter
+of the physical memory available on the system).
+Reducing the maximum level to 8 currently reduces this amount by a factor
+of 3 to 4 and lower maximum levels even more.
+.TP
+.B \-\-mobile
+Use a window layout optimized for smartphones and apply some user interface
+changes that assume that a touchscreen is the main input device. If this option
+is not used, the default layout depends on the platform. Using this option also
+changes the default style for GUI elements of QQuickControls 2 to Default if
+the style is not explicitly set with option \-style.
+.TP
+.B \-\-nobook
+Do not use opening books.
+.TP
+.B \-\-nodelay
+Do not delay fast computer moves. By default, the computer player adds a
+small delay if the move generation took less than a second to make it easier
+for the human to follow the game if the computer plays several moves in a row.
+.TP
+.B \-\-seed
+Set the seed for the random generator. Using a fixed seed makes the move
+generation deterministic if no multi-threading is used (see option --threads).
+.TP
+.B \-\-threads
+The number of threads to use in the search. By default, up to 8 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.
+.PP
+Standard options for Qt applications:
+.TP
+.B \-display
+Switches displays on X11.
+.TP
+.B \-geometry
+Window geometry using the X11 syntax.
+.TP
+.B \-style
+Set the style for the GUI elements of QQuickControls 2. If no style is chosen,
+the default style is Default if option \-\-mobile is set and Fusion otherwise.
+.SH SEE ALSO
+.BR pentobi-thumbnailer (6)
+.SH AUTHOR
+Markus Enzenberger <enz@users.sourceforge.net>
--- /dev/null
+add_subdirectory(libboardgame_sys)
+add_subdirectory(libboardgame_util)
+add_subdirectory(libboardgame_sgf)
+add_subdirectory(libboardgame_base)
+add_subdirectory(libpentobi_base)
+if(PENTOBI_BUILD_GUI OR PENTOBI_BUILD_GTP)
+ add_subdirectory(libboardgame_mcts)
+ add_subdirectory(libpentobi_mcts)
+endif()
+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()
+ add_subdirectory(learn_tool)
+endif()
+if(PENTOBI_BUILD_GUI OR PENTOBI_BUILD_THUMBNAILER)
+ add_subdirectory(libpentobi_paint)
+endif()
+if(PENTOBI_BUILD_GUI)
+ add_subdirectory(convert)
+ add_subdirectory(pentobi)
+endif()
+if(PENTOBI_BUILD_TESTS)
+ add_subdirectory(libboardgame_test)
+ add_subdirectory(unittest)
+endif()
+if(PENTOBI_BUILD_THUMBNAILER)
+ add_subdirectory(libpentobi_thumbnail)
+ add_subdirectory(pentobi_thumbnailer)
+endif()
+if(PENTOBI_BUILD_KDE_THUMBNAILER)
+ add_subdirectory(libpentobi_kde_thumbnailer)
+ add_subdirectory(pentobi_kde_thumbnailer)
+endif()
--- /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]TE[1]
+ )
+)
+)
--- /dev/null
+(
+;GM[Callisto Two-Player Four-Color]CA[UTF-8]
+(
+ ;1[h12]TE[1]
+)
+(
+ ;1[g11]TE[1]
+)
+(
+ ;1[h13]TE[1]
+)
+)
--- /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]
+ )
+ )
+ (
+ ;W[j5,i6,j6,k6,j7]TE[1]
+ )
+ (
+ ;W[j5,h6,i6,j6,i7]TE[1]
+ ;B[g6,g7,h7,h8,i8]TE[1]
+ )
+ (
+ ;W[j5,i6,j6,h7,i7]
+ ;B[g4,f5,g5,g6,g7]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[GembloQ]CA[UTF-8]
+;1[j23,k23,h24,i24,j24,k24,f25,g25,h25,i25,d26,e26,f26,g26,b27,c27,d27,e27,b28,c28]TE[1]
+;2[at23,au23,at24,au24,av24,aw24,av25,aw25,ax25,ay25,ax26,ay26,az26,ba26,az27,ba27,bb27,bc27,bb28,bc28]TE[1]
+;3[bb1,bc1,az2,ba2,bb2,bc2,ax3,ay3,az3,ba3,av4,aw4,ax4,ay4,at5,au5,av5,aw5,at6,au6]TE[1]
+;4[b1,c1,b2,c2,d2,e2,d3,e3,f3,g3,f4,g4,h4,i4,h5,i5,j5,k5,j6,k6]TE[1]
+)
--- /dev/null
+(
+;GM[GembloQ Two-Player]CA[UTF-8]
+;B[v17,w17,t18,u18,v18,w18,r19,s19,t19,u19,p20,q20,r20,s20,n21,o21,p21,q21,n22,o22]TE[1]
+)
--- /dev/null
+(
+;GM[GembloQ Two-Player Four-Color]CA[UTF-8]
+;1[j23,k23,h24,i24,j24,k24,f25,g25,h25,i25,d26,e26,f26,g26,b27,c27,d27,e27,b28,c28]TE[1]
+;2[at23,au23,at24,au24,av24,aw24,av25,aw25,ax25,ay25,ax26,ay26,az26,ba26,az27,ba27,bb27,bc27,bb28,bc28]TE[1]
+;3[bb1,bc1,az2,ba2,bb2,bc2,ax3,ay3,az3,ba3,av4,aw4,ax4,ay4,at5,au5,av5,aw5,at6,au6]TE[1]
+;4[b1,c1,b2,c2,d2,e2,d3,e3,f3,g3,f4,g4,h4,i4,h5,i5,j5,k5,j6,k6]TE[1]
+)
--- /dev/null
+(
+;GM[GembloQ Three-Player]CA[UTF-8]
+;1[z1,aa1,x2,y2,z2,aa2,v3,w3,x3,y3,t4,u4,v4,w4,t5,u5,v5,w5,v6,w6]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]
+ ;3[j7,k7,l7,m7,m8,n8]TE[1]
+ (
+ ;4[v11,w11,w12,x12,y12,z12]TE[1]
+ )
+ (
+ ;4[w10,x10,x11,y11,y12,z12]
+ ;1[n9,o9,o10,p10,p11,q11]
+ (
+ ;2[j6,k6,l6,m6,n6,n7]TE[1]
+ )
+ (
+ ;2[k5,j6,k6,l6,m6,n6]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_2_4.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_gembloq.blksgf</file>
+ <file>book_gembloq_2.blksgf</file>
+ <file>book_gembloq_2_4.blksgf</file>
+ <file>book_gembloq_3.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
+find_package(Qt5Gui REQUIRED)
+
+add_executable(convert Main.cpp)
+
+target_link_libraries(convert Qt5::Gui)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file convert/Main.cpp
+ Converts images using the Qt library.
+ Used for creating PNG icons from the SVG sources at build time.
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <iostream>
+#include <QCoreApplication>
+#include <QImageReader>
+#include <QImageWriter>
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+ QCoreApplication app(argc, argv);
+ try
+ {
+ if (argc != 3)
+ throw QStringLiteral("Need two arguments");
+ auto in = QString::fromLocal8Bit(argv[1]);
+ auto out = QString::fromLocal8Bit(argv[2]);
+ QImageReader reader(in);
+ QImage image = reader.read();
+ if (image.isNull())
+ throw QStringLiteral("%1: %2").arg(in, reader.errorString());
+ QImageWriter writer(out);
+ if (! writer.write(image))
+ throw QStringLiteral("%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_alphago_2016 Mastering the game of Go with deep neural networks and tree search.
+D. Silver, A. Huang, et al. Nature 529 (7587), pp. 484-489, 2016.
+<a href="https://storage.googleapis.com/deepmind-media/alphago/AlphaGoNaturePaper.pdf">(PDF)</a>
+
+@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)
+ - learn_tool -
+ Tool for learning the weights used for move priors in the
+ Monte-Carlo tree search in libpentobi_mcts
+
+ @section mainpage_gui Pentobi GUI Modules
+
+ The Pentobi GUI modules implement a user interface based on
+ <a href="https://www.qt.io/">Qt</a>/QtQuick.
+ They are used for the desktop versions of Pentobi.
+
+ - convert -
+ Small helper program to convert SVG icons to bitmaps at build time
+ - pentobi -
+ Main program that provides a GUI for the player in libpentobi_mcts
+ - 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
+*/
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m8e-4 12-8e-4 2.643c-0.00204 0.75 0.60132 1.353 1.35 1.357h2.65v-3.998l-2.6648-2e-3z" fill="#edd400" stroke-width=".4"/>
+ <g id="f" transform="matrix(.4 0 0 .4 -1.6 -1.6)">
+ <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(-6,-6)" x="10" y="10" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(-6,-12)" x="10" y="20" width="100%" height="100%" xlink:href="#f"/>
+ <g id="e" transform="matrix(.4 0 0 .4 -1.6 -1.6)">
+ <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use transform="translate(-6)" x="10" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(-6,-6)" x="10" y="10" width="100%" height="100%" xlink:href="#e"/>
+ <g id="d" transform="matrix(.4 0 0 .4 -1.6 -1.6)">
+ <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use transform="translate(4,-2)" y="10" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(-10 -16)" x="10" y="20" width="100%" height="100%" xlink:href="#d"/>
+ <g id="b" transform="matrix(.4 0 0 .4 2.4 -1.6)">
+ <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.4 0 0 .4 9.282 -3.388)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m3.9984 11.997-0.39844 0.39844v3.2h-3.2l-0.00392 0.0039c0.24394 0.24347 0.58036 0.39396 0.95312 0.39609h2.6508v-3.9984h-0.00156z" fill="#c4a000" stroke-width=".4"/>
+ <path d="m8e-4 11.996-8e-4 2.643c-0.00104 0.376 0.15036 0.715 0.39608 0.961l0.00392-4e-3v-3.2h3.2l0.39843-0.39844-2.6632-0.0016h-1.3344z" fill="#fce94f" stroke-width=".4"/>
+ <g transform="matrix(.4 0 0 .4 -14.01 1.2562)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.4 0 0 .4 -9.9996 -10.6)">
+ <path d="m54.999 26.5v9.9961l6.664 4e-3h3.336v-6.627c-6e-3 -1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" transform="translate(4,4)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(-4)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32" height="32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m2.0014 23-0.0014 4.6248c-0.00357 1.3125 1.0523 2.3674 2.3625 2.3751h4.6375v-6.9971l-4.6635-0.0028z" fill="#edd400" stroke-width=".69999"/>
+ <g id="f" transform="matrix(.7 0 0 .69999 -.79998 -.79993)">
+ <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(-3 -2.9999)" x="10" y="10" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(-3 -5.9999)" x="10" y="20" width="100%" height="100%" xlink:href="#f"/>
+ <g id="e" transform="matrix(.7 0 0 .69999 -.79998 -.79993)">
+ <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use transform="translate(-3 .00017)" x="10" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(-3 -2.9997)" x="10" y="10" width="100%" height="100%" xlink:href="#e"/>
+ <g id="d" transform="matrix(.7 0 0 .69999 -.79998 -.79993)">
+ <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use transform="translate(-2e-5 -2.9998)" y="10" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(-3 -5.9998)" x="10" y="20" width="100%" height="100%" xlink:href="#d"/>
+ <g id="b" transform="matrix(.7 0 0 .69999 6.2 -.79993)">
+ <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 18.244 -3.9289)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m8.9972 22.995-0.69726 0.69725v5.5999h-5.6l-0.00686 0.0069c0.4269 0.42607 1.0156 0.68941 1.668 0.69315h4.6389v-6.9972h-0.00273z" fill="#c4a000" stroke-width=".69999"/>
+ <path d="m2.0014 22.993-0.0014 4.6248c-0.00182 0.65869 0.26313 1.2523 0.69314 1.6821l0.00686-0.0063v-5.5999h5.5999l0.69726-0.69725-4.6607-0.0027h-2.3351z" fill="#fce94f" stroke-width=".69999"/>
+ <g transform="matrix(.7 0 0 .69999 -22.517 4.1983)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(.7 0 0 .69999 -15.499 -16.55)">
+ <path d="m54.999 26.5v9.9961l6.664 4e-3h3.336v-6.627c-6e-3 -1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" transform="translate(7 7)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(-7 1e-4)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="64" height="64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m6.0026 45-0.0026 8.5891c-0.00663 2.4375 1.9543 4.3966 4.3875 4.4109h8.6125v-12.995l-8.6607-0.0052z" fill="#edd400" stroke-width="1.3"/>
+ <g id="f" transform="matrix(1.3 0 0 1.3 .80003 .80012)">
+ <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(3 2.9999)" x="10" y="10" width="100%" height="100%" xlink:href="#f"/>
+ <use transform="translate(3 5.9999)" x="10" y="20" width="100%" height="100%" xlink:href="#f"/>
+ <g id="e" transform="matrix(1.3 0 0 1.3 .80003 .80012)">
+ <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use transform="translate(3 -.00012)" x="10" width="100%" height="100%" xlink:href="#e"/>
+ <use transform="translate(3 2.9999)" x="10" y="10" width="100%" height="100%" xlink:href="#e"/>
+ <g id="d" transform="matrix(1.3 0 0 1.3 .80003 .80012)">
+ <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use transform="translate(-3e-5 2.9999)" y="10" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(3 5.9999)" x="10" y="20" width="100%" height="100%" xlink:href="#d"/>
+ <g id="b" transform="matrix(1.3 0 0 1.3 13.8 .80012)">
+ <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="matrix(1.3 0 0 1.3 36.167 -5.0109)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.336z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m18.995 44.991-1.2949 1.2949v10.4h-10.4l-0.01274 0.01274c0.79282 0.79128 1.8862 1.2804 3.0976 1.2873h8.6151v-12.995h-0.0051z" fill="#c4a000" stroke-width="1.3"/>
+ <path d="m6.0026 44.987-0.0026 8.5891c-0.00338 1.2233 0.48867 2.3257 1.2873 3.1239l0.01274-0.0117v-10.4h10.4l1.2949-1.2949-8.6555-0.0051h-4.3367z" fill="#fce94f" stroke-width="1.3"/>
+ <g transform="matrix(1.3 0 0 1.3 -39.532 10.083)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.646z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="matrix(1.3 0 0 1.3 -26.499 -28.45)">
+ <path d="m54.999 26.5v9.9961l6.664 4e-3h3.336v-6.627c-6e-3 -1.865-1.508-3.362-3.373-3.373z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use id="a" transform="translate(13 13)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="translate(-13)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="48" height="48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m4.002 34-2e-3 6.6074c-0.0051 1.8742 1.5033 3.3818 3.375 3.3926h6.625v-9.9961l-6.6621-0.0039h-3.3359z" fill="#edd400"/>
+ <g id="a">
+ <rect x="24" y="4" width="10" height="10" fill="#73d216"/>
+ <path d="m24 14h10v-10l-1 1v8h-8z" fill="#4e9a06"/>
+ <path d="m24 14v-10h10l-1 1h-8v8z" fill="#8ae234"/>
+ </g>
+ <use x="10" y="10" xlink:href="#a"/>
+ <use x="10" y="20" xlink:href="#a"/>
+ <g id="b">
+ <rect x="14" y="24" width="10" height="10" fill="#3465a4"/>
+ <path d="m14 34h10v-10l-1 1v8h-8z" fill="#204a87"/>
+ <path d="m14 34v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <use x="10" xlink:href="#b"/>
+ <use x="10" y="10" xlink:href="#b"/>
+ <g id="c">
+ <rect x="4" y="14" width="10" height="10" fill="#edd400"/>
+ <path d="m4 24h10v-10l-1 1v8h-8z" fill="#c4a000"/>
+ <path d="m4 24v-10h10l-1 1h-8v8z" fill="#fce94f"/>
+ </g>
+ <use y="10" xlink:href="#c"/>
+ <use x="10" y="20" xlink:href="#c"/>
+ <g id="d" transform="translate(10)">
+ <rect x="4" y="4" width="10" height="10" fill="#c00"/>
+ <path d="m4 14h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m4 14v-10h10l-1 1h-8v8z" fill="#ef2929"/>
+ </g>
+ <g transform="translate(27.205 -4.47)">
+ <path d="m-19.832 8.47c-1.8649 0.00569-3.3624 1.5081-3.373 3.373v6.627h9.9961l0.0039-6.6641v-3.3359h-6.627z" fill="#c00"/>
+ <path d="m-23.205 18.47h10v-10l-1 1v8h-8z" fill="#a40000"/>
+ <path d="m-19.83 8.47c-1.8649 0.00569-3.3643 1.5081-3.375 3.373v6.625l1-0.99805v-8h8l1-1h-6.625zm-3.375 9.998 2e-3 2e-3 -2e-3 -2e-3z" fill="#ef2929"/>
+ </g>
+ <path d="m13.996 33.993-0.99609 0.99609v8h-8l-0.0098 0.0098c0.60986 0.60868 1.4509 0.98489 2.3828 0.99023h6.627v-9.9961h-0.0039z" fill="#c4a000"/>
+ <path d="m4.002 33.99-2e-3 6.6074c-0.0026 0.941 0.3759 1.789 0.9902 2.403l0.0098-0.0098v-8h7.9999l0.99608-0.99609-6.6581-0.0039h-3.3359z" fill="#fce94f"/>
+ <g transform="translate(-31.025 7.1404)">
+ <path d="m65.031 26.86-0.0059 7.498v2.502h6.627c1.8582-0.0057 3.3518-1.4978 3.373-3.3535v-6.6465h-9.9941z" fill="#3465a4"/>
+ <path d="m75.025 26.86-1 1v8h-8l-1 1h6.627c1.8649-0.0057 3.3624-1.5081 3.373-3.373v-6.627z" fill="#204a87"/>
+ <path d="m65.025 36.86v-10h10l-1 1h-8v8z" fill="#558bc5"/>
+ </g>
+ <g transform="translate(-20.999 -22.5)">
+ <path d="m54.999 26.5v9.9961l6.6641 0.0039h3.3359v-6.627c-0.0057-1.8649-1.5081-3.3624-3.373-3.373h-6.627z" fill="#73d216"/>
+ <path d="m64.005 27.49-0.0098 0.0098v8h-8l-0.99609 0.99609 6.6602 0.0039h3.3359v-6.627c-0.0028-0.93246-0.37834-1.7735-0.98828-2.3828-3.55e-4 -3.55e-4 -0.0016 3.55e-4 -2e-3 0z" fill="#4e9a06"/>
+ <path d="m54.999 26.504v9.9961h0.0039l0.99609-0.99609v-8h8l0.0098-0.0098c-0.60986-0.60868-1.4509-0.98489-2.3828-0.99023h-6.627z" fill="#8ae234"/>
+ </g>
+ <use transform="translate(0,10)" width="100%" height="100%" xlink:href="#d"/>
+ <use transform="translate(10,10)" width="100%" height="100%" xlink:href="#d"/>
+</svg>
--- /dev/null
+<RCC>
+<qresource prefix="/pentobi_icon">
+<file>pentobi-64.svg</file>
+</qresource>
+</RCC>
--- /dev/null
+<RCC>
+<qresource prefix="/pentobi_icon">
+<file>pentobi-16.svg</file>
+<file>pentobi-32.svg</file>
+<file>pentobi.svg</file>
+</qresource>
+</RCC>
--- /dev/null
+find_package(Threads)
+
+add_executable(learn-tool Main.cpp)
+
+target_link_libraries(learn-tool
+ pentobi_mcts
+ pentobi_base
+ boardgame_base
+ boardgame_sgf
+ boardgame_util
+ Threads::Threads
+)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file learn_tool/Main.cpp
+ Learn the parameters used in libpentobi_mcts/PriorKnowledge from existing
+ games with softmax training.
+
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <fstream>
+#include <random>
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_util/FmtSaver.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/Options.h"
+#include "libpentobi_base/Game.h"
+#include "libpentobi_base/MoveMarker.h"
+#include "libpentobi_mcts/LocalPoints.h"
+
+using namespace std;
+using libboardgame_sgf::TreeReader;
+using libboardgame_util::FmtSaver;
+using libboardgame_util::Options;
+using libboardgame_util::split;
+using libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::Color;
+using libpentobi_base::Game;
+using libpentobi_base::Geometry;
+using libpentobi_base::GridExt;
+using libpentobi_base::Move;
+using libpentobi_base::MoveList;
+using libpentobi_base::MoveMarker;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+using libpentobi_mcts::LocalPoints;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** Features, see PriorKnowledge::m_gamma_... */
+enum {
+ point_other,
+ point_opp_attach_or_nb,
+ point_second_color_attach,
+ adj_connect,
+ adj_occupied_other,
+ adj_forbidden_other,
+ adj_own_attach,
+ adj_nonforbidden,
+ attach_to_play,
+ attach_forbidden_other,
+ attach_nonforbidden_0,
+ attach_nonforbidden_1,
+ attach_nonforbidden_2,
+ attach_nonforbidden_3,
+ attach_nonforbidden_4,
+ attach_nonforbidden_5,
+ attach_nonforbidden_6,
+ attach_second_color,
+ local_move,
+ piece_score_0,
+ piece_score_1,
+ piece_score_2,
+ piece_score_3,
+ piece_score_4,
+ piece_score_5,
+ piece_score_6,
+ _nu_features
+};
+
+struct Features
+{
+ using IntType = uint_least8_t;
+
+
+ array<IntType, _nu_features> feature;
+
+
+ Features() { feature.fill(0); }
+
+ void operator+=(const Features& f)
+ {
+ for (unsigned i = 0; i < _nu_features; ++i)
+ feature[i] = static_cast<IntType>(feature[i] + f.feature[i]);
+ }
+
+ void operator|=(const Features& f)
+ {
+ for (unsigned i = 0; i < _nu_features; ++i)
+ feature[i] = feature[i] | f.feature[i];
+ }
+};
+
+struct Sample
+{
+ unsigned played_move;
+
+ vector<Features> features;
+};
+
+
+using Float = double;
+
+const Float step_size = 0.05;
+
+MoveMarker marker;
+
+MoveList moves;
+
+MoveList tmp_moves;
+
+long nu_games;
+
+long nu_positions;
+
+long nu_moves;
+
+random_device rand_dev;
+
+mt19937 rand_gen(rand_dev());
+
+vector<Float> probs;
+
+array<Float, _nu_features> weights;
+
+array<Float, _nu_features> grad_weights;
+
+GridExt<Features> feature_grid_point;
+
+GridExt<Features> feature_grid_adj;
+
+GridExt<Features> feature_grid_attach;
+
+vector<Sample> samples;
+
+LocalPoints local_points;
+
+Features feature_occured_globally;
+
+
+/** This function mirrors what is happening in PriorKnowledge::gen_children,
+ but produces feature vectors instead of a gamma value for each move. */
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+void add_sample(const Board& bd, Color to_play, Move played_mv)
+{
+ marker.clear();
+ bd.gen_moves(to_play, marker, moves);
+ nu_moves += moves.size();
+
+ local_points.init<MAX_SIZE, MAX_ADJ_ATTACH>(bd);
+ auto& geo = bd.get_geometry();
+ auto variant = bd.get_variant();
+ auto& is_forbidden = bd.is_forbidden(to_play);
+ Color second_color;
+ 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;
+ }
+ for (auto p : geo)
+ {
+ feature_grid_point[p] = Features();
+ feature_grid_adj[p] = Features();
+ feature_grid_attach[p] = Features();
+ }
+ for (auto p : geo)
+ {
+ auto& feature_point = feature_grid_point[p].feature;
+ auto& feature_adj = feature_grid_adj[p].feature;
+ auto& feature_attach = feature_grid_attach[p].feature;
+ auto s = bd.get_point_state(p);
+ if (is_forbidden[p])
+ {
+ if (s != to_play)
+ feature_attach[attach_forbidden_other] = 1;
+ else
+ feature_attach[attach_to_play] = 1;
+ if (s == connect_color)
+ feature_adj[adj_connect] = 1;
+ else if (! s.is_empty())
+ feature_adj[adj_occupied_other] = 1;
+ else
+ feature_adj[adj_forbidden_other] = 1;
+ }
+ else
+ {
+ feature_point[point_other] = 1;
+ if (bd.is_attach_point(p, to_play))
+ feature_adj[adj_own_attach] = 1;
+ else
+ feature_adj[adj_nonforbidden] = 1;
+ unsigned n = 0;
+ if (MAX_SIZE == 7 || IS_CALLISTO)
+ {
+ LIBBOARDGAME_ASSERT(geo.get_adj(p).empty());
+ for (auto pa : geo.get_diag(p))
+ n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+ }
+ else
+ for (auto pa : geo.get_adj(p))
+ n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+ switch (n)
+ {
+ case 0: feature_attach[attach_nonforbidden_0] = 1; break;
+ case 1: feature_attach[attach_nonforbidden_1] = 1; break;
+ case 2: feature_attach[attach_nonforbidden_2] = 1; break;
+ case 3: feature_attach[attach_nonforbidden_3] = 1; break;
+ case 4: feature_attach[attach_nonforbidden_4] = 1; break;
+ case 5: feature_attach[attach_nonforbidden_5] = 1; break;
+ default: feature_attach[attach_nonforbidden_6] = 1; break;
+ }
+ }
+ }
+ for (Color c : bd.get_colors())
+ {
+ if (c == to_play || c == second_color)
+ continue;
+ auto& is_forbidden = bd.is_forbidden(c);
+ for (auto p : bd.get_attach_points(c))
+ if (! is_forbidden[p])
+ {
+ feature_grid_point[p].feature[point_other] = 0;
+ feature_grid_point[p].feature[point_opp_attach_or_nb] = 1;
+ for (auto j : geo.get_adj(p))
+ if (! is_forbidden[j])
+ {
+ feature_grid_point[j].feature[point_other] = 0;
+ feature_grid_point[j].feature[point_opp_attach_or_nb] = 1;
+ }
+ }
+ }
+ if (second_color != to_play)
+ {
+ auto& is_forbidden_second_color = bd.is_forbidden(second_color);
+ for (auto p : bd.get_attach_points(second_color))
+ if (! is_forbidden_second_color[p])
+ {
+ feature_grid_point[p].feature[point_second_color_attach] = 1;
+ if (! is_forbidden[p])
+ feature_grid_attach[p].feature[attach_second_color] = 1;
+ }
+ }
+
+ Sample sample;
+ sample.played_move = moves.size() + 1;
+ sample.features.reserve(moves.size());
+ auto& bc = bd.get_board_const();
+ auto move_info_array = bc.get_move_info_array();
+ auto move_info_ext_array = bc.get_move_info_ext_array();
+ for (unsigned i = 0; i < moves.size(); ++i)
+ {
+ auto mv = moves[i];
+ if (mv == played_mv)
+ sample.played_move = i;
+ auto& info_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+ mv, move_info_ext_array);
+ auto& info = BoardConst::get_move_info<MAX_SIZE>(mv, move_info_array);
+ auto j = info.begin();
+ Features features = feature_grid_point[*j];
+ bool local = local_points.contains(*j);
+ for (unsigned k = 1; k < MAX_SIZE; ++k)
+ {
+ ++j;
+ features += feature_grid_point[*j];
+ local |= local_points.contains(*j);
+ }
+ if (local)
+ features.feature[local_move] = 1;
+ if (MAX_SIZE == 7 || IS_CALLISTO)
+ {
+ j = info_ext.begin_attach();
+ auto end = info_ext.end_attach();
+ features += feature_grid_attach[*j];
+ while (++j != end)
+ {
+ features += feature_grid_adj[*j];
+ features += feature_grid_attach[*j];
+ }
+ }
+ else
+ {
+ j = info_ext.begin_attach();
+ auto end = info_ext.end_attach();
+ features += feature_grid_attach[*j];
+ while (++j != end)
+ features += feature_grid_attach[*j];
+ j = info_ext.begin_adj();
+ end = info_ext.end_adj();
+ for ( ; j != end; ++j)
+ features += feature_grid_adj[*j];
+ }
+ switch (static_cast<unsigned>(bd.get_piece_info(info.get_piece()).get_score_points()))
+ {
+ case 0: features.feature[piece_score_0] = 1; break;
+ case 1: features.feature[piece_score_1] = 1; break;
+ case 2: features.feature[piece_score_2] = 1; break;
+ case 3: features.feature[piece_score_3] = 1; break;
+ case 4: features.feature[piece_score_4] = 1; break;
+ case 5: features.feature[piece_score_5] = 1; break;
+ default: features.feature[piece_score_6] = 1; break;
+ }
+ sample.features.push_back(features);
+ feature_occured_globally |= features;
+ }
+ if (sample.played_move == moves.size() + 1)
+ throw runtime_error("game contains illegal move");
+ samples.push_back(sample);
+}
+
+void gen_train_data(const string& file, Variant& variant)
+{
+ ifstream in(file);
+ if (! in)
+ throw runtime_error("could not open " + file);
+ Game game(variant);
+ auto& bd = game.get_board();
+ TreeReader reader;
+ bool has_more;
+ do
+ {
+ has_more = reader.read(in, false);
+ auto tree = reader.get_tree_transfer_ownership();
+ game.init(tree);
+ if (nu_games > 0 && game.get_variant() != variant)
+ throw runtime_error("Files have inconsistent game variants");
+ ++nu_games;
+ variant = game.get_variant();
+ auto max_piece_size = bd.get_board_const().get_max_piece_size();
+ auto node = &game.get_root();
+ do
+ {
+ auto mv = game.get_tree().get_move(*node);
+ if (! mv.is_null() && node->has_parent())
+ {
+ ++nu_positions;
+ game.goto_node(node->get_parent());
+ game.set_to_play(mv.color);
+ if (max_piece_size == 5 && bd.is_callisto())
+ add_sample<5, 16, true>(bd, mv.color, mv.move);
+ else if (max_piece_size == 5)
+ add_sample<5, 16, false>(bd, mv.color, mv.move);
+ else if (max_piece_size == 6)
+ add_sample<6, 22, false>(bd, mv.color, mv.move);
+ else if (max_piece_size == 7)
+ add_sample<7, 12, false>(bd, mv.color, mv.move);
+ else
+ add_sample<22, 44, false>(bd, mv.color, mv.move);
+ }
+ node = node->get_first_child_or_null();
+ }
+ while (node != nullptr);
+ cerr << '.';
+ if (nu_games % 79 == 0)
+ cerr << '\n';
+ }
+ while (has_more);
+}
+
+void print_weight(unsigned i, const char* name, bool is_member = true)
+{
+ if (is_member)
+ cout << "m_";
+ cout << "gamma_" << name << " = ";
+ if (feature_occured_globally.feature[i] == 0u)
+ cout << "1; // unused\n";
+ else
+ cout << "exp(" << weights[i] << "f / temperature);\n";
+}
+
+void print_weights()
+{
+ FmtSaver saver(cout);
+ cout << std::fixed << setprecision(3);
+ print_weight(point_other, "point_other");
+ print_weight(point_opp_attach_or_nb, "point_opp_attach_or_nb");
+ print_weight(point_second_color_attach, "point_second_color_attach");
+ print_weight(adj_connect, "adj_connect");
+ print_weight(adj_occupied_other, "adj_occupied_other");
+ print_weight(adj_forbidden_other, "adj_forbidden_other");
+ print_weight(adj_own_attach, "adj_own_attach");
+ print_weight(adj_nonforbidden, "adj_nonforbidden");
+ print_weight(attach_to_play, "attach_to_play");
+ print_weight(attach_forbidden_other, "attach_forbidden_other");
+ print_weight(attach_nonforbidden_0, "attach_nonforbidden[0]");
+ print_weight(attach_nonforbidden_1, "attach_nonforbidden[1]");
+ print_weight(attach_nonforbidden_2, "attach_nonforbidden[2]");
+ print_weight(attach_nonforbidden_3, "attach_nonforbidden[3]");
+ print_weight(attach_nonforbidden_4, "attach_nonforbidden[4]");
+ print_weight(attach_nonforbidden_5, "attach_nonforbidden[5]");
+ print_weight(attach_nonforbidden_6, "attach_nonforbidden[6]");
+ print_weight(attach_second_color, "attach_second_color");
+ print_weight(local_move, "local");
+ print_weight(piece_score_0, "piece_score_0", false);
+ print_weight(piece_score_1, "piece_score_1", false);
+ print_weight(piece_score_2, "piece_score_2", false);
+ print_weight(piece_score_3, "piece_score_3", false);
+ print_weight(piece_score_4, "piece_score_4", false);
+ print_weight(piece_score_5, "piece_score_5", false);
+ print_weight(piece_score_6, "piece_score_6", false);
+}
+
+void init_weights()
+{
+ normal_distribution<Float> distribution(0, 0.01);
+ for (auto& w : weights)
+ w = distribution(rand_gen);
+}
+
+/** Gradient descent step using softmax training. */
+void train_step(unsigned step, bool print)
+{
+ for (auto& w : grad_weights)
+ w = 0;
+
+ Float cost = 0;
+ for (auto& sample : samples)
+ {
+ auto nu_moves = sample.features.size();
+ probs.resize(nu_moves);
+ Float sum = 0;
+ for (size_t i = 0; i < nu_moves; ++i)
+ {
+ auto& feature = sample.features[i].feature;
+ probs[i] = 0;
+ for (unsigned j = 0; j < _nu_features; ++j)
+ probs[i] += weights[j] * feature[j];
+ probs[i] = exp(probs[i]);
+ sum += probs[i];
+ }
+ for (size_t i = 0; i < nu_moves; ++i)
+ probs[i] /= sum;
+ for (size_t i = 0; i < nu_moves; ++i)
+ {
+ auto p = probs[i];
+ auto& feature = sample.features[i].feature;
+ if (i == sample.played_move)
+ {
+ for (unsigned j = 0; j < _nu_features; ++j)
+ grad_weights[j] -= (1 - p) * feature[j];
+ }
+ else
+ {
+ for (unsigned j = 0; j < _nu_features; ++j)
+ grad_weights[j] -= (-p) * feature[j];
+ }
+ }
+ cost += -log(probs[sample.played_move]);
+ }
+
+ auto nu_samples = static_cast<Float>(samples.size());
+ Float decay = 1e-3;
+ for (unsigned i = 0; i < _nu_features; ++i)
+ {
+ auto& w = weights[i];
+ auto dw = grad_weights[i] / nu_samples + decay * w;
+ w += -step_size * dw;
+ }
+
+ cost /= nu_samples;
+
+ if (print)
+ {
+ LIBBOARDGAME_LOG("Step ", step);
+ LIBBOARDGAME_LOG("Cost ", cost);
+ print_weights();
+ }
+}
+
+void train(const string& file_list, unsigned steps)
+{
+ nu_games = 0;
+ nu_positions = 0;
+ nu_moves = 0;
+ auto files = split(file_list, ',');
+ Variant variant = Variant::classic_2;
+ for (auto& file : files)
+ gen_train_data(file, variant);
+ cerr << '\n';
+ LIBBOARDGAME_LOG("Files: ", file_list);
+ LIBBOARDGAME_LOG(nu_games, " games");
+ LIBBOARDGAME_LOG(nu_positions, " positions");
+ if (nu_positions == 0)
+ return;
+ LIBBOARDGAME_LOG(double(nu_moves) / double(nu_positions), " moves/pos");
+ init_weights();
+ for (unsigned i = 1; i <= steps; ++i)
+ train_step(i, i % 100 == 0 || i == steps);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+ libboardgame_util::LogInitializer log_initializer;
+ try
+ {
+ vector<string> specs = {
+ "sgffiles:",
+ "steps:"
+ };
+ Options opt(argc, argv, specs);
+ train(opt.get("sgffiles"), opt.get<unsigned>("steps", 3000));
+ }
+ catch (const exception& e)
+ {
+ LIBBOARDGAME_LOG("Error: ", e.what());
+ return 1;
+ }
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+set(boardgame_base_SRCS
+ CoordPoint.h
+ CoordPoint.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
+ )
+if (PENTOBI_BUILD_GTP)
+ set(boardgame_base_SRCS ${boardgame_base_SRCS}
+ Engine.cpp
+ Engine.h
+ )
+endif()
+
+add_library(boardgame_base STATIC ${boardgame_base_SRCS})
+
+target_link_libraries(boardgame_base boardgame_util)
+if (PENTOBI_BUILD_GTP)
+ target_link_libraries(boardgame_base boardgame_gtp)
+endif()
+
+target_include_directories(boardgame_base PUBLIC ..)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_base/CoordPoint.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CoordPoint.h"
+
+#include <iostream>
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, 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>
+#include "libboardgame_util/Assert.h"
+
+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);
+
+ CoordPoint(unsigned x, unsigned y);
+
+ bool operator==(CoordPoint p) const;
+
+ bool operator!=(CoordPoint p) const;
+
+ bool operator<(CoordPoint p) const;
+
+ CoordPoint operator+(CoordPoint p) const;
+
+ CoordPoint operator-(CoordPoint p) const;
+
+ CoordPoint& operator+=(CoordPoint p);
+
+ CoordPoint& operator-=(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 CoordPoint::CoordPoint(unsigned x, unsigned y)
+{
+ LIBBOARDGAME_ASSERT(x < numeric_limits<int>::max());
+ LIBBOARDGAME_ASSERT(y < numeric_limits<int>::max());
+ this->x = static_cast<int>(x);
+ this->y = static_cast<int>(y);
+}
+
+inline bool CoordPoint::operator==(CoordPoint p) const
+{
+ return x == p.x && y == p.y;
+}
+
+inline bool CoordPoint::operator<(CoordPoint p) const
+{
+ if (y != p.y)
+ return y < p.y;
+ return x < p.x;
+}
+
+inline bool CoordPoint::operator!=(CoordPoint p) const
+{
+ return ! operator==(p);
+}
+
+inline CoordPoint CoordPoint::operator+(CoordPoint p) const
+{
+ return {x + p.x, y + p.y};
+}
+
+inline CoordPoint& CoordPoint::operator+=(CoordPoint p)
+{
+ *this = *this + p;
+ return *this;
+}
+
+inline CoordPoint CoordPoint::operator-(CoordPoint p) const
+{
+ return {x - p.x, y - p.y};
+}
+
+inline CoordPoint& CoordPoint::operator-=(CoordPoint p)
+{
+ *this = *this - p;
+ return *this;
+}
+
+inline CoordPoint CoordPoint::null()
+{
+ return {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, 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 */
+//-----------------------------------------------------------------------------
+
+#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);
+}
+
+void Engine::cmd_cputime(Response& response)
+{
+ double time = libboardgame_sys::cpu_time();
+ if (time < 0)
+ 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(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& response);
+ void cmd_set_random_seed(Arguments args);
+
+ 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 can 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 rectangular grids.
+ @tparam P An instantiation of libboardgame_base::Point (or compatible
+ class) */
+template<class P>
+class Geometry
+{
+public:
+ using Point = P;
+
+ using IntType = typename Point::IntType;
+
+ static const unsigned max_adj = 4;
+
+ static const unsigned max_diag = 11;
+
+ /** On-board adjacent neighbors of a point. */
+ using AdjList = ArrayList<Point, max_adj, unsigned short>;
+
+ /** On-board diagonal neighbors of a point
+ Currently supports up to 11 diagonal points as used on boards
+ for GembloQ. */
+ using DiagList = ArrayList<Point, max_diag, unsigned short>;
+
+ /** Adjacent neighbors of a coordinate. */
+ using AdjCoordList = ArrayList<CoordPoint, max_adj>;
+
+ /** Diagonal neighbors of a coordinate. */
+ using DiagCoordList = ArrayList<CoordPoint, max_diag>;
+
+ 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();
+
+ /** Get points that share an edge with this point. */
+ virtual AdjCoordList get_adj_coord(int x, int y) const = 0;
+
+ /** Get points that share a corner but not an edge with this point.
+ 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. */
+ 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(m_range); }
+
+ unsigned get_point_type(CoordPoint p) const;
+
+ unsigned get_point_type(Point p) const;
+
+ bool is_onboard(CoordPoint p) const;
+
+ bool is_onboard(unsigned x, unsigned y) const;
+
+ bool is_onboard(int x, int y) const { return is_onboard(CoordPoint(x, y)); }
+
+ /** 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;
+
+ /** Return the point at a given coordinate.
+ @return The point or Point::null() if this coordinates are
+ off-board. */
+ Point get_point(int x, int y) const;
+
+ unsigned get_width() const { return m_width; }
+
+ unsigned get_height() const { return m_height; }
+
+ /** Get range used for onboard points. */
+ IntType get_range() const { return m_range; }
+
+ unsigned get_x(Point p) const;
+
+ unsigned get_y(Point p) const;
+
+ bool from_string(string::const_iterator begin, string::const_iterator end,
+ 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 = make_unique<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:
+ IntType m_range;
+
+ AdjList m_adj[Point::range_onboard];
+
+ DiagList m_diag[Point::range_onboard];
+
+ 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];
+
+#ifdef 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; // Non-inline to avoid GCC -Winline warning
+
+template<class P>
+bool Geometry<P>::from_string(string::const_iterator begin,
+ string::const_iterator end, Point& p) const
+{
+ unsigned x;
+ unsigned y;
+ if (! m_string_rep->read(begin, end, m_width, m_height, x, y)
+ || ! is_onboard(x, y))
+ return false;
+ p = get_point(x, y);
+ return true;
+}
+
+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 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 auto Geometry<P>::get_point(int x, int y) const -> Point
+{
+ if (x < 0 || static_cast<unsigned>(x) >= m_width
+ || y < 0 || static_cast<unsigned>(y) >= m_height)
+ return Point::null();
+ 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 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 < n; ++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>
+bool Geometry<P>::is_onboard(unsigned x, unsigned y) const
+{
+ return x < m_width && y < m_height && ! 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) && ! get_point(p.x, p.y).is_null();
+}
+
+#ifdef LIBBOARDGAME_DEBUG
+
+template<class P>
+inline bool Geometry<P>::is_valid(Point p) const
+{
+ return p.to_int() < m_range;
+}
+
+#endif
+
+template<class P>
+inline const string& Geometry<P>::to_string(Point p) const
+{
+ LIBBOARDGAME_ASSERT(p.to_int() < m_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 = static_cast<unsigned>(max_x - min_x + 1);
+ height = static_cast<unsigned>(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 {x, y};
+ LIBBOARDGAME_ASSERT(false);
+ return {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:
+ using Point = P;
+
+ using Geometry = libboardgame_base::Geometry<P>;
+
+
+ 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)
+{
+#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:
+ using Point = P;
+
+ using Geometry = libboardgame_base::Geometry<P>;
+
+
+ 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:
+ using Point = P;
+
+
+ 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). This may be smaller than
+ W*H if the geomtries are not rectangular.
+ @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:
+ using IntType = I;
+
+ static const unsigned range_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(range_onboard <= max_width * max_height, "");
+
+ static const unsigned range = range_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 */
+ IntType 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()
+{
+#ifdef 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 auto Point<M, W, H, I>::to_int() const -> IntType
+{
+ 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:
+ using Point = P;
+
+ virtual ~PointTransform() = default;
+
+ virtual Point get_transformed(Point p, const Geometry<P>& geo) const = 0;
+};
+
+
+//-----------------------------------------------------------------------------
+
+template<class P>
+class PointTransfIdent
+ : public PointTransform<P>
+{
+public:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfIdent<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot90<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot180<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot270<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot270Refl<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRot90Refl<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfRefl<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfReflRot180<P>::get_transformed(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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot60<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx + 0.5f * px + 1.5f * py));
+ auto 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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot120<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx - 0.5f * px + 1.5f * py));
+ auto 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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot240<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx - 0.5f * px - 1.5f * py));
+ auto 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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonRot300<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx + 0.5f * px - 1.5f * py));
+ auto 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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot60<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx + 0.5f * (-px) + 1.5f * py));
+ auto 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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot120<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx - 0.5f * (-px) + 1.5f * py));
+ auto 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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot240<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx - 0.5f * (-px) - 1.5f * py));
+ auto 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:
+ using Point = P;
+
+ Point get_transformed(Point p, const Geometry<P>& geo) const override;
+};
+
+
+template<class P>
+P PointTransfTrigonReflRot300<P>::get_transformed(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;
+ auto x = static_cast<unsigned>(round(cx + 0.5f * (-px) - 1.5f * py));
+ auto 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 */
+//-----------------------------------------------------------------------------
+
+#include "Rating.h"
+
+#include <iostream>
+#include "libboardgame_util/Assert.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+ostream& operator<<(ostream& out, Rating rating)
+{
+ out << rating.m_elo;
+ return out;
+}
+
+istream& operator>>(istream& in, Rating& rating)
+{
+ in >> rating.m_elo;
+ return in;
+}
+
+double Rating::get_expected_result(Rating elo_opponent,
+ unsigned nu_opponents) const
+{
+ auto diff = elo_opponent.m_elo - m_elo;
+ return
+ 1. / (1. + static_cast<double>(nu_opponents) * pow(10., diff / 400.));
+}
+
+void Rating::update(double game_result, Rating elo_opponent, double k_value,
+ unsigned nu_opponents)
+{
+ LIBBOARDGAME_ASSERT(k_value > 0);
+ auto 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, Rating rating);
+ friend istream& operator>>(istream& in, Rating& rating);
+
+ explicit Rating(double elo = 0) : m_elo(elo) { }
+
+ /** 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) */
+ double 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(double game_result, Rating elo_opponent, double k_value = 32,
+ unsigned nu_opponents = 1);
+
+ double get() const { return m_elo; }
+
+ /** Get rating rounded to an integer. */
+ int to_int() const { return static_cast<int>(round(m_elo)); }
+
+private:
+ double 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:
+ using Point = P;
+
+ using AdjCoordList = typename Geometry<P>::AdjCoordList;
+
+ using DiagCoordList = typename Geometry<P>::DiagCoordList;
+
+
+ RectGeometry(unsigned width, unsigned height);
+
+ /** Create or reuse an already created geometry with a given size. */
+ static const RectGeometry& get(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;
+};
+
+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)
+{
+ static map<pair<unsigned, unsigned>, shared_ptr<RectGeometry>> s_geometry;
+
+ 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
+{
+ // See Geometry::get_diag_coord() about advantageous ordering of the list
+ 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 */
+//-----------------------------------------------------------------------------
+
+#include "RectTransform.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfIdentity::get_transformed(CoordPoint p) const
+{
+ return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot90::get_transformed(CoordPoint p) const
+{
+ return {-p.y, p.x};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot180::get_transformed(CoordPoint p) const
+{
+ return {-p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot270::get_transformed(CoordPoint p) const
+{
+ return {p.y, -p.x};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRefl::get_transformed(CoordPoint p) const
+{
+ return {-p.x, p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot90Refl::get_transformed(CoordPoint p) const
+{
+ return {-p.y, -p.x};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot180Refl::get_transformed(CoordPoint p) const
+{
+ return {p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfRectRot270Refl::get_transformed(CoordPoint p) const
+{
+ return {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(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot90
+ : public Transform
+{
+public:
+ TransfRectRot90() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot180
+ : public Transform
+{
+public:
+ TransfRectRot180() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot270
+ : public Transform
+{
+public:
+ TransfRectRot270() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRefl
+ : public Transform
+{
+public:
+ TransfRectRefl() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot90Refl
+ : public Transform
+{
+public:
+ TransfRectRot90Refl() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot180Refl
+ : public Transform
+{
+public:
+ TransfRectRot180Refl() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfRectRot270Refl
+ : public Transform
+{
+public:
+ TransfRectRot270Refl() : Transform(0) {}
+
+ CoordPoint get_transformed(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 */
+//-----------------------------------------------------------------------------
+
+#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;
+
+//-----------------------------------------------------------------------------
+
+bool StdStringRep::read(string::const_iterator begin,
+ string::const_iterator end, unsigned width,
+ unsigned height, unsigned& x, unsigned& y) const
+{
+ auto p = begin;
+ while (p != end && isspace(*p) != 0)
+ ++p;
+ bool read_x = false;
+ x = 0;
+ int c;
+ while (p != end && isalpha(*p) != 0)
+ {
+ c = tolower(*(p++));
+ if (c < 'a' || c > 'z')
+ return false;
+ x = 26 * x + static_cast<unsigned>(c - 'a' + 1);
+ if (x > width)
+ return false;
+ read_x = true;
+ }
+ if (! read_x)
+ return false;
+ --x;
+ bool read_y = false;
+ y = 0;
+ while (p != end && isdigit(*p) != 0)
+ {
+ c = *(p++);
+ y = 10 * y + static_cast<unsigned>((c - '0'));
+ if (y > height)
+ return false;
+ read_y = true;
+ }
+ if (! read_y)
+ return false;
+ y = height - y;
+ while (p != end)
+ if (isspace(*(p++)) == 0)
+ return false;
+ return true;
+}
+
+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>
+#include <string>
+
+namespace libboardgame_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** String representation of points. */
+struct StringRep
+{
+ virtual ~StringRep() = default;
+
+ virtual bool read(string::const_iterator begin, string::const_iterator end,
+ 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
+{
+ bool read(string::const_iterator begin, string::const_iterator end,
+ 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 */
+//-----------------------------------------------------------------------------
+
+#include "Transform.h"
+
+namespace libboardgame_base {
+
+//-----------------------------------------------------------------------------
+
+Transform::~Transform() = default;
+
+//-----------------------------------------------------------------------------
+
+} // 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(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_point_type() const { return m_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 point_type)
+ : m_point_type(point_type)
+ {}
+
+private:
+ unsigned m_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 */
+//-----------------------------------------------------------------------------
+
+#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");
+ if (n == 1)
+ throw Failure("command needs one argument");
+ 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");
+ 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 minimum 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 minimum 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;
+ }
+#endif
+ return typeid(T).name();
+}
+
+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
+{
+ auto 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
+)
+
+target_include_directories(boardgame_gtp PUBLIC ..)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/CmdLine.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CmdLine.h"
+
+#include <limits>
+#include <sstream>
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+CmdLine::CmdLine(const string& line)
+{
+ init(line);
+}
+
+CmdLine::~CmdLine() = default; // Non-inline to avoid GCC -Winline warning
+
+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)) != 0 && ! 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)) != 0)
+ ++begin;
+ auto end = m_line.end();
+ while (end > begin && isspace(static_cast<unsigned char>(*(end - 1))) != 0)
+ --end;
+ return {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 { return m_line; }
+
+ /** Get command name. */
+ CmdLineRange get_name() const { return m_elem[m_idx_name]; }
+
+
+ void write_id(ostream& out) const;
+
+ CmdLineRange get_trimmed_line_after_elem(unsigned i) const;
+
+ const vector<CmdLineRange>& get_elements() const { return m_elem; }
+
+
+ const CmdLineRange& get_element(unsigned i) const;
+
+ unsigned get_idx_name() const { return m_idx_name; }
+
+private:
+ unsigned 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 const CmdLineRange& CmdLine::get_element(unsigned i) const
+{
+ assert(i < m_elem.size());
+ return m_elem[i];
+}
+
+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 equal(m_begin, m_end, s.begin(), s.end());
+ }
+
+ 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 static_cast<string::size_type>(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 */
+//-----------------------------------------------------------------------------
+
+#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)) == 0)
+ 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;
+ }
+ return false;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+Engine::Engine()
+{
+ add("known_command", &Engine::cmd_known_command);
+ add("list_commands", &Engine::cmd_list_commands);
+ add("quit", &Engine::cmd_quit);
+}
+
+Engine::~Engine() = default; // Non-inline to avoid GCC -Winline warning
+
+void Engine::add(const string& name, const Handler& f)
+{
+ m_handlers[name] = f;
+}
+
+void Engine::add(const string& name, const HandlerNoArgs& f)
+{
+ add(name, [f](Arguments args, Response& response) {
+ args.check_empty();
+ f(response);
+ });
+}
+
+void Engine::add(const string& name, const HandlerNoResponse& f)
+{
+ add(name, [f](Arguments args, Response&) {
+ f(args);
+ });
+}
+
+void Engine::add(const string& name, const HandlerNoArgsNoResponse& f)
+{
+ add(name, [f](Arguments args, Response&) {
+ args.check_empty();
+ f();
+ });
+}
+
+/** Return @c true if command is known, @c false otherwise. */
+void Engine::cmd_known_command(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';
+}
+
+/** Quit command loop. */
+void Engine::cmd_quit()
+{
+ m_quit = true;
+}
+
+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 != nullptr)
+ *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 != nullptr)
+ {
+ *out << (status ? '=' : '?');
+ line.write_id(*out);
+ *out << ' ';
+ response.write(*out, buffer);
+ out->flush();
+ }
+ return status;
+}
+
+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:
+ using Handler = function<void(Arguments, Response&)>;
+
+ using HandlerNoArgs = function<void(Response&)>;
+
+ using HandlerNoResponse = function<void(Arguments)>;
+
+ using HandlerNoArgsNoResponse = function<void()>;
+
+
+ /** @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_quit() @c quit @endlink</dt>
+ <dd>@copydoc cmd_quit() </dd>
+ </dl> */
+ /** @name Command handlers */
+ /** @{ */
+ void cmd_known_command(Arguments args, Response& response);
+ void cmd_list_commands(Response& response);
+ void cmd_quit();
+ /** @} */ // @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 remaining 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, const Handler& f);
+
+ void add(const string& name, const HandlerNoArgs& f);
+
+ void add(const string& name, const HandlerNoResponse& f);
+
+ void add(const string& name, const 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)(Arguments, Response&), T* t);
+
+ template<class T>
+ void add(const string& name, void (T::*f)(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)(Arguments, Response&));
+
+ template<class T>
+ void add(const string& name, void (T::*f)(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:
+ /** Flag to quit main loop. */
+ bool m_quit;
+
+ map<string, Handler> m_handlers;
+
+
+ bool handle_cmd(CmdLine& line, ostream* out, Response& response,
+ string& buffer);
+};
+
+template<class T>
+void Engine::add(const string& name, void (T::*f)(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)(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)(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)(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_FAILURE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_gtp/Response.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Response.h"
+
+namespace libboardgame_gtp {
+
+//-----------------------------------------------------------------------------
+
+void Response::clear()
+{
+ m_stream.str(string());
+ m_stream.copyfmt(m_dummy);
+}
+
+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:
+ /** Get response.
+ @return A copy of the internal response string stream */
+ string to_string() const { return m_stream.str(); }
+
+ /** Set response. */
+ void set(const string& response) { m_stream.str(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;
+
+ template<typename TYPE>
+ Response& operator<<(const TYPE& t) { m_stream << t; return *this; }
+
+private:
+ /** Response stream */
+ ostringstream m_stream;
+
+ /** Dummy for restoring default format flags*/
+ ios m_dummy{nullptr};
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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;
+
+ Atomic& operator=(T t)
+ {
+ val = t;
+ return *this;
+ }
+
+ 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;
+
+ Atomic& operator=(T t)
+ {
+ val.store(t);
+ return *this;
+ }
+
+ 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
+option(LIBBOARDGAME_MCTS_SINGLE_THREAD
+ "Slightly faster MCTS search if only single-threaded search is used" OFF)
+
+find_package(Threads)
+
+add_library(boardgame_mcts INTERFACE)
+
+if(LIBBOARDGAME_MCTS_SINGLE_THREAD)
+ target_compile_definitions(boardgame_mcts INTERFACE
+ LIBBOARDGAME_MCTS_SINGLE_THREAD)
+endif()
+
+target_include_directories(boardgame_mcts INTERFACE ..)
+
+target_link_libraries(boardgame_mcts INTERFACE
+ boardgame_util
+ Threads::Threads
+ )
--- /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 <cstddef>
+#include <random>
+#include "Atomic.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:
+ using Move = M;
+
+
+ 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_hash1[Move::range];
+
+ size_t m_hash2[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_hash1)
+ hash = generator();
+ for (auto& hash : m_hash2)
+ 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_hash1[last.to_int()] ^ m_hash2[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)
+ {
+ for (typename Move::IntType j = 0; j < Move::range; ++j)
+ m_lgr1[i][j].store(Move::null().to_int(), memory_order_relaxed);
+ for (size_t j = 0; j < hash_table_size; ++j)
+ m_lgr2[i][j].store(Move::null().to_int(), memory_order_relaxed);
+ }
+}
+
+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;
+
+//-----------------------------------------------------------------------------
+
+using NodeIdx = uint_least32_t;
+
+//-----------------------------------------------------------------------------
+
+/** %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:
+ using Move = M;
+
+ using Float = F;
+
+
+ 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. */
+ void init(const Move& mv, Float value, Float count, Float move_prior);
+
+ /** 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 { return m_move; }
+
+ /** Prior value for the move.
+ This value is used in the exploration term, see description of class
+ SearchBase. */
+ Float get_move_prior() const { return m_move_prior; }
+
+ /** 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);
+
+ /** Unlink children.
+ Only to be used in 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;
+
+ Float m_move_prior;
+
+ Atomic<unsigned short, MT> m_nu_children;
+
+ Move m_move;
+
+ Atomic<NodeIdx, MT> 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;
+ Float m_move_prior;
+ 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;
+ m_move_prior = node.m_move_prior;
+ // 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.load(memory_order_acquire);
+}
+
+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,
+ Float move_prior)
+{
+ // 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_move_prior = move_prior;
+ 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()
+{
+#ifdef 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);
+ // Even if m_first_child is only used by other threads after m_nu_children
+ // was set, we need release/acquire order for both because m_first_child
+ // can be overwritten later if two threads expand a node simultaneously.
+ m_first_child.store(first_child, memory_order_release);
+ 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);
+ // Store relaxed (wouldn't even need to be atomic)
+ m_first_child.store(first_child, memory_order_relaxed);
+ m_nu_children.store(nu_children, memory_order_relaxed);
+}
+
+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 {
+
+//-----------------------------------------------------------------------------
+
+using PlayerInt = uint_fast8_t;
+
+//-----------------------------------------------------------------------------
+
+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/RandomGenerator.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"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+using libboardgame_mcts::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 SearchBase.
+ See description of class SearchBase 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. */
+ using 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 = 1;
+
+ /** Maximum value used for Node::get_move_prior() */
+ static constexpr Float max_move_prior = 1;
+
+ /** 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;
+
+ /** Minimum count of a node to be expanded. */
+ static constexpr Float expansion_threshold = 0;
+
+ /** Increase of the expansion threshold per in-tree move played. */
+ static constexpr Float expansion_threshold_inc = 0;
+
+ /** 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.
+
+ The exploration term is not as in original UCT but has the form
+ @f$ c * P_{move} * \sqrt{N_{parent}}/N_{child} @f$ with an exploration
+ constant c and a prior move value P (similar as used in AlphaGo
+ @ref libboardgame_doc_alphago_2016).
+
+ @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:
+ using State = S;
+
+ using Move = M;
+
+ using SearchParamConst = R;
+
+ static const bool multithread = SearchParamConst::multithread;
+
+ using Float = typename SearchParamConst::Float;
+
+ using Node = libboardgame_mcts::Node<M, Float, multithread>;
+
+ using Tree = libboardgame_mcts::Tree<Node>;
+
+ using PlayerMove = libboardgame_mcts::PlayerMove<M>;
+
+
+ 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 */
+ /** @{ */
+
+ /** 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.
+ It will also reuse the tree if it is the same position but the last
+ search was aborted. The default value is true, because this is
+ the usually preferred behavior during games to save search time.
+ @see set_reuse_tree() */
+ 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.
+ The default value is false, because the usually preferred behavior
+ is to see if the search generates different moves when doing subsequent
+ searches in the same position.
+ @see set_subreuse_tree() */
+ void set_reuse_tree(bool enable);
+
+ bool get_reuse_tree() const;
+
+ /** Maximum parent visit count for applying RAVE. */
+ void set_rave_parent_max(Float n);
+
+ Float get_rave_parent_max() const;
+
+ /** Maximum child value count for applying RAVE. */
+ void set_rave_child_max(Float n);
+
+ Float get_rave_child_max() const;
+
+ /** Weight used for adding RAVE values to the node value. */
+ void set_rave_weight(Float v);
+
+ 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;
+
+#ifdef 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(const 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 explicitly 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);
+
+private:
+#ifdef LIBBOARDGAME_DEBUG
+ class AssertionHandler
+ : public libboardgame_util::AssertionHandler
+ {
+ public:
+ explicit AssertionHandler(const SearchBase& search);
+
+ 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;
+ };
+
+ /** 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:
+ using SearchFunc = function<void(ThreadState&)>;
+
+
+ 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;
+
+ 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;
+
+ bool m_last_aborted = false;
+
+ 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;
+
+#ifdef 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>::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();
+}
+
+
+#ifdef 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>
+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)
+#ifdef LIBBOARDGAME_DEBUG
+ , m_assertion_handler(*this)
+#endif
+{ }
+
+template<class S, class M, class R>
+SearchBase<S, M, R>::~SearchBase() = default; // Non-inline to avoid GCC -Winline warning
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_abort(const ThreadState& thread_state) const
+{
+#ifdef 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);
+ return check_cannot_change(thread_state, remaining_simulations);
+}
+
+template<class S, class M, class R>
+bool SearchBase<S, M, R>::check_cannot_change(ThreadState& thread_state,
+ Float remaining) const
+{
+#ifdef 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)
+ {
+ auto t = make_unique<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));
+ }
+}
+
+#ifdef 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,
+ SearchParamConst::max_move_prior);
+ 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 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 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 expansion_threshold = SearchParamConst::expansion_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);
+ expansion_threshold += SearchParamConst::expansion_threshold_inc;
+ }
+ state.finish_in_tree();
+ if (node->get_visit_count() > expansion_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)
+{
+#ifdef 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);
+ auto 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;
+ }
+ 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_last_aborted))
+ || (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;
+ m_last_aborted = get_abort();
+ 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)
+ {
+ auto 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 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::max_move_prior
+ / SearchParamConst::child_min_count;
+ auto children = m_tree.get_children_nonempty(node);
+ auto i = children.begin();
+ auto value =
+ i->get_value()
+ + i->get_move_prior() * bias_factor / i->get_value_count();
+ auto best_value = value;
+ auto limit = best_value - bias_limit;
+ auto best_child = i;
+ while (++i != children.end())
+ {
+ value = i->get_value();
+ if (value <= limit)
+ continue;
+ value += i->get_move_prior() * bias_factor / i->get_value_count();
+ if (value > best_value)
+ {
+ best_value = value;
+ limit = best_value - bias_limit;
+ best_child = i;
+ }
+ }
+ 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_nonempty(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;
+ }
+ return false;
+}
+
+template<class S, class M, class R>
+void SearchBase<S, M, R>::set_callback(
+ const function<void(double, double)>& callback)
+{
+ m_callback = callback;
+}
+
+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;
+ auto 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_nonempty(*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;
+ auto 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"
+
+namespace libboardgame_mcts {
+
+using namespace std;
+using libboardgame_util::Range;
+
+//-----------------------------------------------------------------------------
+
+/** %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:
+ using Node = N;
+
+ using Move = typename Node::Move;
+
+ using Float = typename Node::Float;
+
+ /** Range for iterating over the children of a node. */
+ using Children = Range<const Node>;
+
+
+ /** 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 verify the arguments for
+ add_child().
+ @param max_move_prior The maximum move prior used for initializing
+ children. Used only in debug mode to verify the arguments for
+ add_child(). */
+ NodeExpander(unsigned thread_id, Tree& tree, Float child_min_count,
+ Float max_move_prior);
+
+ /** 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,
+ Float move_prior);
+
+ /** 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_move_prior = -numeric_limits<Float>::max();
+
+ const Node* m_first_child;
+
+ const Node* m_best_child;
+
+#ifdef LIBBOARDGAME_DEBUG
+ Float m_child_min_count;
+
+ Float m_max_move_prior;
+#endif
+ };
+
+ Tree(size_t memory, unsigned nu_threads);
+
+
+ /** Remove all nodes but the root node. */
+ void clear();
+
+ const Node& get_root() const;
+
+ Children get_children(const Node& node) const;
+
+ Children get_children_nonempty(const Node& node) const;
+
+ 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,
+ Float max_move_prior)
+ : 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);
+#ifdef LIBBOARDGAME_DEBUG
+ m_child_min_count = child_min_count;
+ m_max_move_prior = max_move_prior;
+#else
+ LIBBOARDGAME_UNUSED(child_min_count);
+ LIBBOARDGAME_UNUSED(max_move_prior);
+#endif
+}
+
+template<typename N>
+inline void Tree<N>::NodeExpander::add_child(const Move& mv, Float value,
+ Float count, Float move_prior)
+{
+ // -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);
+ LIBBOARDGAME_ASSERT(move_prior <= m_max_move_prior);
+ auto& next = m_thread_storage.next;
+ LIBBOARDGAME_ASSERT(next < m_thread_storage.end);
+ next->init(mv, value, count, move_prior);
+ if (move_prior > m_best_move_prior)
+ {
+ m_best_child = next;
+ m_best_move_prior = move_prior;
+ }
+ ++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_children(const Node& node) const -> Children
+{
+ auto nu_children = node.get_nu_children();
+ auto begin = nu_children != 0 ? &get_node(node.get_first_child()) : nullptr;
+ auto end = begin + nu_children;
+ return Children(begin, end);
+}
+
+template<typename N>
+inline auto Tree<N>::get_children_nonempty(const Node& node) const -> Children
+{
+ auto begin = &get_node(node.get_first_child());
+ auto end = begin + node.get_nu_children();
+ return Children(begin, end);
+}
+
+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)
+{
+ if (nu_threads == 0)
+ nu_threads = 1;
+ auto max_nodes = memory / sizeof(Node);
+ // We need at least one node per thread
+ max_nodes = max(max_nodes, static_cast<size_t>(nu_threads));
+ // 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()));
+ m_nu_threads = nu_threads;
+ m_max_nodes = max_nodes;
+
+ // Using make_unique<Node[]>(max_nodes) slows down the array creation and
+ // thereby the startup time of Pentobi with GCC 7/8 because the compiler
+ // does not optimize away the call to the empty Move() constructor (last
+ // tested with GCC 7.2.0 and GCC 8.0.0 on Ubuntu 17.10).
+ m_nodes.reset(new Node[max_nodes]);
+
+ m_thread_storage = make_unique<ThreadStorage[]>(nu_threads);
+ m_nodes_per_thread = max_nodes / nu_threads;
+ for (unsigned i = 0; i < 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>
+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;
+ // Parenthesis around thread_storage.next are needed because of a bug
+ // with GCC 4 ("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)
+{
+ auto 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 {
+
+//-----------------------------------------------------------------------------
+
+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 libboardgame_mcts
+
+#endif // LIBBOARDGAME_MCTS_TREE_UTIL_H
--- /dev/null
+add_library(boardgame_sgf STATIC
+ Reader.h
+ Reader.cpp
+ SgfError.h
+ SgfError.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
+)
+
+target_include_directories(boardgame_sgf PUBLIC ..)
+
+target_link_libraries(boardgame_sgf boardgame_util)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/Reader.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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) != 0;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+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);
+}
+
+bool Reader::read(istream& in, bool check_single_tree)
+{
+ m_in = ∈
+ m_is_in_main_variation = true;
+ consume_whitespace();
+ read_tree(true);
+ while (true)
+ {
+ int c = m_in->peek();
+ if (c == EOF)
+ return false;
+ if (c == '(')
+ {
+ if (check_single_tree)
+ throw ReadError("Input has multiple game trees");
+ return true;
+ }
+ 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);
+ }
+ 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() != '[')
+ {
+ char c = read_char();
+ if (! is_ascii_space(c))
+ m_id += c;
+ }
+ 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;
+ 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;
+ };
+
+
+ virtual ~Reader() = default;
+
+
+ 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 a stream.
+ @param in The input stream containing the SGF game tree(s).
+ @param check_single_tree If true, the caller does not want to
+ handle multi-tree SGF files and a ReadError will be thrown if
+ non-whitespace characters follow after the first tree before the end of
+ the stream.
+ @return true, if there are more trees to read in the stream.
+ @throws ReadError */
+ bool read(istream& in, bool check_single_tree = true);
+
+ 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/SgfError.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfError.h"
+
+#include <string>
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+MissingProperty::MissingProperty(const string& id)
+ : SgfError("Missing SGF property '" + id + "'")
+{
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_sgf
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfError.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_SGF_SGF_ERROR_H
+#define LIBBOARDGAME_SGF_SGF_ERROR_H
+
+#include <sstream>
+#include <stdexcept>
+
+namespace libboardgame_sgf {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+/** Exception indicating 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. */
+class SgfError
+ : public runtime_error
+{
+ using runtime_error::runtime_error;
+};
+
+//-----------------------------------------------------------------------------
+
+class MissingProperty
+ : public SgfError
+{
+public:
+ explicit MissingProperty(const string& id);
+};
+
+//-----------------------------------------------------------------------------
+
+class InvalidProperty
+ : public SgfError
+{
+public:
+ template<typename T>
+ InvalidProperty(const string& id, const T& value);
+
+private:
+ template<typename T>
+ static string get_message(const string& id, const T& value);
+};
+
+template<typename T>
+InvalidProperty::InvalidProperty(const string& id, const T& value)
+ : SgfError(get_message(id, value))
+{
+}
+
+template<typename T>
+string InvalidProperty::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_SGF_ERROR_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_sgf/SgfNode.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "SgfNode.h"
+
+#include <algorithm>
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+Property::~Property() = default; // Non-inline to avoid GCC -Winline warning
+
+//-----------------------------------------------------------------------------
+
+SgfNode::~SgfNode() = default; // Non-inline to avoid GCC -Winline warning
+
+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()
+{
+ auto node = make_unique<SgfNode>();
+ node->m_parent = this;
+ SgfNode& result = *(node.get());
+ auto last_child = get_last_child();
+ if (last_child == nullptr)
+ 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())
+ throw MissingProperty(id);
+ 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 == nullptr)
+ 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 != nullptr)
+ {
+ ++n;
+ child = child->m_sibling.get();
+ }
+ return n;
+}
+
+const SgfNode* SgfNode::get_previous_sibling() const
+{
+ if (m_parent == nullptr)
+ 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 != nullptr);
+ 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;
+ 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 == nullptr)
+ {
+ 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 == nullptr)
+ 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;
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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 "SgfError.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();
+
+
+ /** Append a new child. */
+ void append(unique_ptr<SgfNode> node);
+
+ bool has_property(const string& id) const;
+
+ /** Get a property.
+ @throws MissingProperty if no such property */
+ 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.
+ @throws InvalidProperty
+ @throws MissingProperty */
+ template<typename T>
+ T parse_property(const string& id) const;
+
+ /** Get property parsed as a type with default value.
+ @throws InvalidProperty */
+ 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. */
+ void 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;
+}
+
+inline const SgfNode& SgfNode::get_first_child() const
+{
+ LIBBOARDGAME_ASSERT(has_children());
+ return *m_first_child;
+}
+
+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 InvalidProperty(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 void SgfNode::remove_children()
+{
+ m_first_child.reset();
+}
+
+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;
+ values_to_string.reserve(values.size());
+ 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 */
+//-----------------------------------------------------------------------------
+
+#include "SgfTree.h"
+
+#include <ctime>
+#include <cstdio>
+#include <cstdlib>
+#include "SgfUtil.h"
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+
+using libboardgame_sgf::find_root;
+using libboardgame_util::trim;
+
+//-----------------------------------------------------------------------------
+
+SgfTree::SgfTree()
+{
+ SgfTree::init();
+}
+
+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()
+{
+ auto node = &get_root();
+ do
+ {
+ delete_variations(*node);
+ node = node->get_first_child_or_null();
+ }
+ while (node != nullptr);
+}
+
+void SgfTree::delete_variations(const SgfNode& node)
+{
+ if (node.get_nu_children() <= 1)
+ return;
+ non_const(node).delete_variations();
+ m_modified = true;
+}
+
+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 == nullptr)
+ 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();
+ do
+ {
+ if (node->get_sibling() != nullptr)
+ return true;
+ node = node->get_first_child_or_null();
+ }
+ while (node != nullptr);
+ return false;
+}
+
+void SgfTree::init()
+{
+ auto root = make_unique<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 != nullptr && &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() != nullptr)
+ {
+ non_const(node).move_down();
+ m_modified = true;
+ }
+}
+
+void SgfTree::move_up(const SgfNode& node)
+{
+ auto parent = node.get_parent_or_null();
+ if (parent != nullptr && &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)
+{
+ 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 "SgfNode.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() = default;
+
+
+ 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(bool is_modified = true);
+
+ 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 children but the first. */
+ void delete_variations(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() */
+ void 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& round);
+
+ 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 void SgfTree::remove_children(const SgfNode& node)
+{
+ if (node.has_children())
+ m_modified = true;
+ 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(bool is_modified)
+{
+ m_modified = is_modified;
+}
+
+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 */
+//-----------------------------------------------------------------------------
+
+#include "SgfUtil.h"
+
+#include <algorithm>
+#include <sstream>
+#include "libboardgame_util/StringUtil.h"
+
+namespace libboardgame_sgf {
+
+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 == nullptr)
+ return node;
+ while (true)
+ {
+ auto parent = current->get_parent_or_null();
+ if (parent == nullptr || ! 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 != nullptr)
+ {
+ 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 SgfNode* get_next_earlier_variation(const SgfNode& node)
+{
+ auto child = &node;
+ auto current = node.get_parent_or_null();
+ while (current != nullptr && (child->get_sibling() == nullptr))
+ {
+ child = current;
+ current = current->get_parent_or_null();
+ }
+ if (current == nullptr)
+ return nullptr;
+ return child->get_sibling();
+}
+
+const SgfNode* get_next_node(const SgfNode& node)
+{
+ auto child = node.get_first_child_or_null();
+ if (child != nullptr)
+ 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 == nullptr)
+ return false;
+ while (true)
+ {
+ auto parent = current->get_parent_or_null();
+ if (parent == nullptr)
+ 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 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 {
+
+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 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 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 */
+//-----------------------------------------------------------------------------
+
+#include "TreeReader.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+TreeReader::TreeReader() = default; // Non-inline to avoid GCC -Winline warning
+
+TreeReader::~TreeReader() = default; // Non-inline to avoid GCC -Winline warning
+
+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 = make_unique<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() override;
+
+ 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 { return *m_root; }
+
+ /** 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;
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#include "TreeWriter.h"
+
+namespace libboardgame_sgf {
+
+//-----------------------------------------------------------------------------
+
+TreeWriter::TreeWriter(ostream& out, const SgfNode& root)
+ : m_root(root),
+ m_writer(out)
+{
+}
+
+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;
+ 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() = default;
+
+ /** 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 */
+//-----------------------------------------------------------------------------
+
+#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_indent >= 0)
+ m_current_indent += static_cast<unsigned>(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_indent >= 0)
+ m_current_indent -= static_cast<unsigned>(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 (unsigned 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;
+
+ unsigned 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';
+ auto indent = m_current_indent + 1 + id.size();
+ for (unsigned 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
+include(CheckIncludeFiles)
+
+add_library(boardgame_sys STATIC
+ Compiler.h
+ CpuTime.h
+ CpuTime.cpp
+ Memory.h
+ Memory.cpp
+)
+
+check_include_files(sys/sysctl.h HAVE_SYS_SYSCTL_H)
+if(HAVE_SYS_SYSCTL_H)
+ target_compile_definitions(boardgame_sys PRIVATE HAVE_SYS_SYSCTL_H)
+endif()
+check_include_files(sys/times.h HAVE_SYS_TIMES_H)
+if(HAVE_SYS_TIMES_H)
+ target_compile_definitions(boardgame_sys PRIVATE HAVE_SYS_TIMES_H)
+endif()
+check_include_files(unistd.h HAVE_UNISTD_H)
+if(HAVE_UNISTD_H)
+ target_compile_definitions(boardgame_sys PRIVATE HAVE_UNISTD_H)
+endif()
+
+target_include_directories(boardgame_sys PUBLIC ..)
--- /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 */
+//-----------------------------------------------------------------------------
+
+#include "CpuTime.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+#ifdef 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 defined HAVE_UNISTD_H && defined HAVE_SYS_TIMES_H
+ static auto 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 */
+//-----------------------------------------------------------------------------
+
+#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 defined 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
+)
+
+target_include_directories(boardgame_test PUBLIC ..)
+
+target_link_libraries(boardgame_test boardgame_util)
+
+add_library(boardgame_test_main STATIC Main.cpp)
+
+target_link_libraries(boardgame_test_main boardgame_test)
--- /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_test/Test.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Test.h"
+
+#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, const 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;
+ }
+ 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[])
+{
+ libboardgame_util::LogInitializer log_initializer;
+ 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 <functional>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+namespace libboardgame_test {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using TestFunction = function<void()>;
+
+//-----------------------------------------------------------------------------
+
+class TestFail
+ : public logic_error
+{
+public:
+ TestFail(const char* file, int line, const string& s);
+};
+
+//-----------------------------------------------------------------------------
+
+void add_test(const string& name, const 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, const TestFunction& function)
+ {
+ add_test(name, function);
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_test
+
+//-----------------------------------------------------------------------------
+
+#define LIBBOARDGAME_TEST_CASE(name) \
+ static void name(); \
+ static 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; \
+ const auto& result1 = (expr1); \
+ const 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__, \
+ "Unexpected 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) > (tolerance) * result1 / 100) \
+ { \
+ ostringstream msg; \
+ msg << "Difference between " << result1 << " and " \
+ << result2 << " exceeds " << ((tolerance) / 100 ) \
+ << " 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
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/Abort.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Abort.h"
+
+//----------------------------------------------------------------------------
+
+namespace libboardgame_util {
+
+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 = false; }
+
+inline bool get_abort() { return abort; }
+
+inline void set_abort() { abort = true; }
+
+//-----------------------------------------------------------------------------
+
+} // 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:
+ using IntType = I;
+
+ static_assert(numeric_limits<IntType>::is_integer, "");
+
+ static const IntType max_size = M;
+
+ using iterator = typename array<T, max_size>::iterator;
+
+ using const_iterator = typename array<T, max_size>::const_iterator;
+
+ using value_type = T;
+
+
+ ArrayList() = default;
+
+ ArrayList(const ArrayList& l);
+
+ 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>
+ArrayList<T, M, I>::ArrayList(const ArrayList& l)
+{
+ *this = l;
+}
+
+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
+{
+ return equal(begin(), end(), array_list.begin(), array_list.end());
+}
+
+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 */
+//-----------------------------------------------------------------------------
+
+#include "Assert.h"
+
+#include <list>
+
+#ifdef LIBBOARDGAME_DEBUG
+#include <algorithm>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <vector>
+#include "Log.h"
+#endif
+
+#ifdef LIBBOARDGAME_DISABLE_LOG
+#include "Unused.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);
+}
+
+//----------------------------------------------------------------------------
+
+#ifdef LIBBOARDGAME_DEBUG
+
+void handle_assertion(const char* expression, const char* file, int line)
+{
+ static bool is_during_handle_assertion = false;
+#ifdef LIBBOARDGAME_DISABLE_LOG
+ LIBBOARDGAME_UNUSED(expression);
+ LIBBOARDGAME_UNUSED(file);
+ LIBBOARDGAME_UNUSED(line);
+#else
+ LIBBOARDGAME_LOG(file, ":", line, ": Assertion '", expression, "' failed");
+#endif
+ 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();
+
+ AssertionHandler(const AssertionHandler&) = delete;
+ AssertionHandler& operator=(const AssertionHandler&) = delete;
+
+ virtual void run() = 0;
+};
+
+#ifdef 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. */
+#ifdef 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 */
+//-----------------------------------------------------------------------------
+
+#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
+)
+
+target_compile_options(boardgame_util PUBLIC
+ "$<$<CONFIG:DEBUG>:-DLIBBOARDGAME_DEBUG>")
+
+target_include_directories(boardgame_util PUBLIC ..)
+
+target_link_libraries(boardgame_util boardgame_sys)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libboardgame_util/CpuTimeSource.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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 */
+//-----------------------------------------------------------------------------
+
+#include "IntervalChecker.h"
+
+#include <limits>
+#include "Assert.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+IntervalChecker::IntervalChecker(TimeSource& time_source, double time_interval,
+ const function<bool()>& f)
+ : m_time_source(time_source),
+ m_time_interval(time_interval),
+ m_function(f)
+{
+ 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 = static_cast<unsigned>(new_count_interval);
+ m_result = m_function();
+ }
+ else
+ {
+ 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 "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,
+ const 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();
+ 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 */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_DISABLE_LOG
+
+//-----------------------------------------------------------------------------
+
+#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 = nullptr;
+
+//-----------------------------------------------------------------------------
+
+void _log(const string& s)
+{
+ if (_log_stream == nullptr)
+ 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
+ _log_stream = &cerr;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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;
+
+//-----------------------------------------------------------------------------
+
+#ifndef LIBBOARDGAME_DISABLE_LOG
+extern ostream* _log_stream;
+#endif
+
+inline void disable_logging()
+{
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ _log_stream = nullptr;
+#endif
+}
+
+inline ostream* get_log_stream()
+{
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ return _log_stream;
+#else
+ return nullptr;
+#endif
+}
+
+inline void flush_log()
+{
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ if (_log_stream != nullptr)
+ _log_stream->flush();
+#endif
+}
+
+//-----------------------------------------------------------------------------
+
+#ifndef 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()
+ {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ _log_init();
+#endif
+ }
+
+ ~LogInitializer()
+ {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ _log_close();
+#endif
+ }
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libboardgame_util
+
+//-----------------------------------------------------------------------------
+
+#ifndef 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 {
+
+//-----------------------------------------------------------------------------
+
+/** 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;
+}
+
+/** Modulus operation with always positive result. */
+inline int mod(int a, int b)
+{
+ return ((a % b) + b) % b;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#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.compare(0, 1, "-") == 0 && arg != "-")
+ {
+ if (arg == "--")
+ {
+ end_of_options = true;
+ continue;
+ }
+ string name;
+ string value;
+ bool needs_arg = false;
+ if (arg.compare(0, 2, "--") == 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 (string::size_type 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() = default; // Non-inline to avoid GCC -Winline warning
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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:
+ using Generator = minstd_rand;
+
+ using ResultType = Generator::result_type;
+
+
+ /** 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 explicitly 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();
+
+ RandomGenerator(const RandomGenerator&) = delete;
+ RandomGenerator& operator=(const RandomGenerator&) = delete;
+
+ 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 empty() const { return m_begin == m_end; }
+
+ 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 */
+//-----------------------------------------------------------------------------
+
+#include "StringUtil.h"
+
+#include <cctype>
+#include <cmath>
+#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)
+{
+ auto int_seconds = static_cast<int>(round(seconds));
+ 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]) != 0)
+ ++begin;
+ while (end > begin && isspace(s[end - 1]) != 0)
+ --end;
+ return s.substr(begin, end - begin);
+}
+
+string trim_right(const string& s)
+{
+ auto end = s.size();
+ while (end > 0 && isspace(s[end - 1]) != 0)
+ --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 */
+//-----------------------------------------------------------------------------
+
+#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 */
+//-----------------------------------------------------------------------------
+
+#include "TimeSource.h"
+
+namespace libboardgame_util {
+
+//-----------------------------------------------------------------------------
+
+TimeSource::~TimeSource() = default;
+
+//----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#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&) { }
+
+#ifdef 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 */
+//-----------------------------------------------------------------------------
+
+#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 */
+//-----------------------------------------------------------------------------
+
+#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,
+ bool is_gembloq)
+{
+ for (unsigned i = 0; i < offset; ++i)
+ out << ' ';
+ if (is_gembloq)
+ {
+ char c = 'A';
+ char c1 = ' ';
+ out << ' ';
+ for (unsigned x = 0; x < width; ++x, ++c)
+ {
+ if (x % 2 != 0)
+ out << c1;
+ if (x > 0 && x % 26 == 0)
+ {
+ c = 'A';
+ if (c1 == ' ')
+ c1 = 'A';
+ else
+ ++c1;
+ }
+ out << c;
+ }
+ }
+ else
+ {
+ 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();
+#ifdef 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();
+ if (! m_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 (m_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 (! m_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).empty())
+ 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 = get_break_ties();
+ 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<>());
+ 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
+{
+ if (m_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 (! m_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;
+ if (m_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 && m_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 == nullptr)
+ {
+ 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 (variant == Variant::duo)
+ {
+ m_color_name[Color(0)] = "Purple";
+ m_color_name[Color(1)] = "Orange";
+ m_color_esc_sequence[Color(0)] = "\x1B[1;35;47m";
+ m_color_esc_sequence[Color(1)] = "\x1B[1;33;47m";
+ m_color_esc_sequence_text[Color(0)] = "\x1B[1;35m";
+ m_color_esc_sequence_text[Color(1)] = "\x1B[1;33m";
+ }
+ else if (variant == Variant::junior)
+ {
+ m_color_name[Color(0)] = "Green";
+ m_color_name[Color(1)] = "Orange";
+ m_color_esc_sequence[Color(0)] = "\x1B[1;32;47m";
+ m_color_esc_sequence[Color(1)] = "\x1B[1;33;47m";
+ m_color_esc_sequence_text[Color(0)] = "\x1B[1;32m";
+ m_color_esc_sequence_text[Color(1)] = "\x1B[1;33m";
+ }
+ else 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_geometry_type = libpentobi_base::get_geometry_type(variant);
+ m_is_callisto = (m_geometry_type == GeometryType::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::gembloq)
+ m_needed_starting_points = 4;
+ else
+ m_needed_starting_points = 1;
+ if (m_is_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_colors);
+ 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_name() == "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;
+ has_attach_point |= static_cast<int>(is_attach_point(*i, c));
+ }
+ 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();
+ unsigned n = 0;
+ do
+ if (is_colorless_starting_point(*i)
+ || (is_colored_starting_point(*i)
+ && get_starting_point_color(*i) == c))
+ if (++n >= m_needed_starting_points)
+ 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 if (m_max_piece_size == 7)
+ for (Color c : get_colors())
+ for (Move mv : setup.placements[c])
+ place<7, 12>(c, mv);
+ else
+ for (Color c : get_colors())
+ for (Move mv : setup.placements[c])
+ place<22, 44>(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 if (m_max_piece_size == 7)
+ play<7, 12>(c, mv);
+ else
+ play<22, 44>(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);
+ }
+ auto width = m_geo->get_width();
+ auto 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_gembloq = (m_piece_set == PieceSet::gembloq);
+ 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(static_cast<int>(x),
+ static_cast<int>(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)
+ && (! 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()
+ && 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 if (is_gembloq)
+ {
+ set_color(out, "\x1B[1;30;47m");
+ if (point_type == 1)
+ out << '/';
+ else if (point_type == 3)
+ out << '\\';
+ }
+ else
+ {
+ set_color(out, "\x1B[1;30;47m");
+ out << ' ';
+ }
+ }
+ if (is_offboard)
+ {
+ if (is_trigon && m_geo->is_onboard(x - 1, y))
+ {
+ set_color(out, "\x1B[1;30;47m");
+ out << (point_type == 1 ? '\\' : '/');
+ }
+ else if (is_gembloq && m_geo->is_onboard(x - 1, y))
+ {
+ set_color(out, "\x1B[1;30;47m");
+ if (point_type == 1)
+ out << '/';
+ else if (point_type == 3)
+ out << '\\';
+ }
+ else if (m_is_callisto && x == 0)
+ {
+ set_color(out, "\x1B[0m");
+ out << ' ';
+ }
+ else if (is_gembloq)
+ {
+ set_color(out, "\x1B[0m");
+ if (point_type == 1 || point_type == 3)
+ out << " ";
+ else
+ 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 || is_gembloq)
+ 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 (m_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(static_cast<int>(width - 1),
+ static_cast<int>(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, is_gembloq);
+ 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 "Geometry.h"
+#include "MoveList.h"
+#include "PointList.h"
+#include "PointState.h"
+#include "Setup.h"
+#include "StartingPoints.h"
+#include "Variant.h"
+
+namespace libpentobi_base {
+
+class MoveMarker;
+
+//-----------------------------------------------------------------------------
+
+/** Blokus board.
+ The implementation is speed-optimized for Monte-Carlo tree search. Only
+ data that is needed during the MCTS search is computed incrementally.
+ For the same reason, it does not provide an undo function, but instead
+ a snapshot state that can can be restored quickly at the start of each
+ MCTS simulation.
+ @note @ref libboardgame_avoid_stack_allocation */
+class Board
+{
+public:
+ using PointStateGrid = Grid<PointState>;
+
+ /** Maximum number of pieces per player in any game variant. */
+ static const unsigned max_pieces = Setup::max_pieces;
+
+ using PiecesLeftList = ArrayList<Piece, Piece::max_pieces>;
+
+ static const unsigned max_player_moves = max_pieces;
+
+ /** Maximum number of moves in any game variant. */
+ static const unsigned max_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_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; }
+
+ GeometryType get_geometry_type() const { return m_geometry_type; }
+
+ bool is_callisto() const { return m_is_callisto; }
+
+ /** Whether ties are broken in the current game variant. */
+ bool get_break_ties() const { return m_is_callisto; }
+
+ 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() */
+ bool from_string(Move& mv, const string& s) const {
+ return m_bc->from_string(mv, s); }
+
+ 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;
+
+ /** Number of starting points the first move needs to cover.
+ This is needed for GembloQ Three-Player to ensure that the first
+ player covers all four triangles of the starting square. */
+ unsigned get_needed_starting_points() const { return m_needed_starting_points; }
+
+ /** 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;
+
+ GeometryType m_geometry_type;
+
+ 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;
+
+ /** See get_needed_starting_points() */
+ unsigned m_needed_starting_points;
+
+ /** 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_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 unsigned Board::get_adj_status(Point p, Color c) const
+{
+ LIBBOARDGAME_ASSERT(m_bc->has_adj_status_points(p));
+ auto i = m_bc->get_adj_status_points(p).begin();
+ auto result = static_cast<unsigned>(is_forbidden(*i, c));
+ for (unsigned j = 1; j < PrecompMoves::adj_status_nu_adj; ++j)
+ result |= (static_cast<unsigned>(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_range());
+ 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_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);
+ if (m_nu_players == 2)
+ return get_score_multicolor(c);
+ 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;
+ 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);
+ 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;
+ 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 (MAX_SIZE == 22) // GembloQ
+ {
+ LIBBOARDGAME_ASSERT(m_bonus_all_pieces == 0);
+ LIBBOARDGAME_ASSERT(m_bonus_one_piece == 0);
+ }
+ else 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 */
+//-----------------------------------------------------------------------------
+
+#include "BoardConst.h"
+
+#include <algorithm>
+#include "Marker.h"
+#include "PieceTransformsClassic.h"
+#include "PieceTransformsGembloQ.h"
+#include "PieceTransformsTrigon.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sys::get_type_name;
+
+//-----------------------------------------------------------------------------
+
+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, 44>, 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 less = [](CoordPoint a, CoordPoint b)
+ {
+ return ((a.y == b.y && a.x < b.x) || a.y > b.y);
+ };
+ auto check = [&](unsigned short a, unsigned short b)
+ {
+ if (! less(points[a], 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;
+ case 1:
+ break;
+ default:
+ sort(points.begin(), points.end(), less);
+ }
+}
+
+vector<PieceInfo> create_pieces_callisto(const Geometry& geo,
+ const PieceTransforms& transforms)
+{
+ auto geometry_type = GeometryType::callisto;
+ vector<PieceInfo> pieces;
+ pieces.reserve(19);
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("L",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("Z",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(1, -1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("V",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("I",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_classic(const Geometry& geo,
+ const PieceTransforms& transforms)
+{
+ auto geometry_type = GeometryType::classic;
+ 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("L4",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(0, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("Z4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(1, -1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("V3",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_gembloq(const Geometry& geo,
+ const PieceTransforms& transforms)
+{
+ auto geometry_type = GeometryType::gembloq;
+ vector<PieceInfo> pieces;
+ pieces.reserve(21);
+ pieces.emplace_back("P",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(3, 1), CoordPoint(3, 2),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2),
+ CoordPoint(-1, -2), CoordPoint(0, -2),
+ CoordPoint(1, -2), CoordPoint(2, -2),
+ CoordPoint(-1, -3), CoordPoint(0, -3) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("I5",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(-5, -2), CoordPoint(-4, -2),
+ CoordPoint(-3, -2), CoordPoint(-2, -2),
+ CoordPoint(-5, -3), CoordPoint(-4, -3),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(3, 1), CoordPoint(4, 1),
+ CoordPoint(3, 2), CoordPoint(4, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("X",
+ PiecePoints{ CoordPoint(-3, 0), CoordPoint(-2, 0),
+ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2),
+ CoordPoint(1, -2), CoordPoint(2, -2),
+ CoordPoint(-3, 1), CoordPoint(-2, 1),
+ CoordPoint(1, 1), CoordPoint(2, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("W",
+ PiecePoints{ CoordPoint(-5, 0), CoordPoint(-4, 0),
+ CoordPoint(-3, 0), CoordPoint(-2, 0),
+ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(3, 0), CoordPoint(4, 0),
+ CoordPoint(-5, -1), CoordPoint(-4, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(3, -1), CoordPoint(4, -1),
+ CoordPoint(-3, 1), CoordPoint(-2, 1),
+ CoordPoint(1, 1), CoordPoint(2, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("Z",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(3, 0), CoordPoint(4, 0),
+ CoordPoint(-5, 0), CoordPoint(-4, 0),
+ CoordPoint(-5, -1), CoordPoint(-4, -1),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(3, -1), CoordPoint(4, -1),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("Y",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-3, 0),
+ CoordPoint(-2, 0), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(-2, -1), CoordPoint(-1, -1),
+ CoordPoint(0, -1), CoordPoint(-3, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2),
+ CoordPoint(-3, 1), CoordPoint(-2, 1),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(3, 1), CoordPoint(4, 1),
+ CoordPoint(3, 2), CoordPoint(4, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("N5",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(-3, -1),
+ CoordPoint(-2, -1), CoordPoint(-1, -1),
+ CoordPoint(-4, -2), CoordPoint(-3, -2),
+ CoordPoint(-5, -2), CoordPoint(-5, -3),
+ CoordPoint(-2, -2), CoordPoint(-4, -3),
+ CoordPoint(-3, 0), CoordPoint(-2, 0),
+ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(-3, 1), CoordPoint(-2, 1),
+ CoordPoint(-1, 1), CoordPoint(0, 1),
+ CoordPoint(-1, 2), CoordPoint(0, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("T5",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-5, 0),
+ CoordPoint(-4, 0), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(-5, -1), CoordPoint(-4, -1),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2),
+ CoordPoint(-1, -2), CoordPoint(0, -2),
+ CoordPoint(-1, -3), CoordPoint(0, -3) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("L5",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(-5, -2), CoordPoint(-4, -2),
+ CoordPoint(-5, -3), CoordPoint(-4, -3),
+ CoordPoint(3, 0), CoordPoint(4, 0),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(3, -1), CoordPoint(4, -1),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("N4.5",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(-3, -1),
+ CoordPoint(-2, -1), CoordPoint(-1, -1),
+ CoordPoint(-4, -2), CoordPoint(-3, -2),
+ CoordPoint(-2, -2), CoordPoint(-4, -3),
+ CoordPoint(-3, 0), CoordPoint(-2, 0),
+ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(-3, 1), CoordPoint(-2, 1),
+ CoordPoint(-1, 1), CoordPoint(0, 1),
+ CoordPoint(-1, 2), CoordPoint(0, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-3, 0),
+ CoordPoint(-2, 0), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(-3, 1), CoordPoint(-2, 1),
+ CoordPoint(-1, 1), CoordPoint(0, 1),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(-1, 2), CoordPoint(0, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-3, 0),
+ CoordPoint(-2, 0), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(1, -2), CoordPoint(2, -2),
+ CoordPoint(-3, 1), CoordPoint(-2, 1),
+ CoordPoint(1, 1), CoordPoint(2, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("L4",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(3, 0), CoordPoint(4, 0),
+ CoordPoint(1, 1), CoordPoint(2, 1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(3, -1), CoordPoint(4, -1),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(-2, -1), CoordPoint(-3, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2),
+ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(2, 1), CoordPoint(1, 1),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(4, 2), CoordPoint(3, 2),
+ CoordPoint(3, 1), CoordPoint(4, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("L3.5",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(1, -2), CoordPoint(2, -2),
+ CoordPoint(-5, -2), CoordPoint(-4, -2),
+ CoordPoint(-3, -2), CoordPoint(-2, -2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("V",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(1, -2), CoordPoint(2, -2),
+ CoordPoint(-3, -2), CoordPoint(-2, -2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(1, 1), CoordPoint(2, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("L2.5",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-3, -1), CoordPoint(-2, -1),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, -1), CoordPoint(2, -1),
+ CoordPoint(-3, -2), CoordPoint(-2, -2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(2, 0),
+ CoordPoint(1, 1), CoordPoint(2, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("1.5",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-1, -1), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(2, 0)},
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(-1, 0),
+ CoordPoint(-1, -1), CoordPoint(0, -1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_junior(const Geometry& geo,
+ const PieceTransforms& transforms)
+{
+ auto geometry_type = GeometryType::classic;
+ 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(1, -1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("T4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("Z4",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("L4",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(1, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(0, 1), CoordPoint(0, 0),
+ CoordPoint(0, -1), CoordPoint(0, -2) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("V3",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(0, -1),
+ CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0), 2);
+ return pieces;
+}
+
+// Note that the pieces for Trigon are currently used for both trigon_3 and
+// the other Trigon variants even if the point types of their geometries are
+// not compatible (e.g. whether the point with coordinates 0,0 is an upward or
+// downward triangle). This requires special handling of Trigon at several
+// places. In the future, we should probably use a separate set of Trigon
+// pieces for even-sized and odd-sized boards instead.
+vector<PieceInfo> create_pieces_trigon(const Geometry& geo,
+ const PieceTransforms& transforms)
+{
+ auto geometry_type = GeometryType::trigon;
+ 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(1, 0));
+ pieces.emplace_back("I4",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(-1, 1), CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("C4",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("A4",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0), CoordPoint(2, 0) },
+ geo, transforms, geometry_type, CoordPoint(1, 0));
+ pieces.emplace_back("I3",
+ PiecePoints{ CoordPoint(1, -1), CoordPoint(0, 0),
+ CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(1, 0));
+ pieces.emplace_back("2",
+ PiecePoints{ CoordPoint(0, 0), CoordPoint(1, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(0, 0) },
+ geo, transforms, geometry_type, CoordPoint(0, 0));
+ return pieces;
+}
+
+vector<PieceInfo> create_pieces_nexos(const Geometry& geo,
+ const PieceTransforms& transforms)
+{
+ auto geometry_type = GeometryType::nexos;
+ 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(-1, 0));
+ pieces.emplace_back("W",
+ PiecePoints{ CoordPoint(-2, -1), CoordPoint(-1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 2)},
+ geo, transforms, geometry_type, 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, geometry_type, 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, geometry_type, CoordPoint(0, 1));
+ pieces.emplace_back("E",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(-1, 2)},
+ geo, transforms, geometry_type, 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, geometry_type, CoordPoint(-1, 0));
+ pieces.emplace_back("X",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(-1, 0),
+ CoordPoint(1, 0), CoordPoint(0, 1)},
+ geo, transforms, geometry_type, CoordPoint(0, -1));
+ pieces.emplace_back("F",
+ PiecePoints{ CoordPoint(1, -2), CoordPoint(0, -1),
+ CoordPoint(1, 0), CoordPoint(0, 1)},
+ geo, transforms, geometry_type, CoordPoint(0, -1));
+ pieces.emplace_back("H",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(2, 1)},
+ geo, transforms, geometry_type, 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, geometry_type, CoordPoint(-1, 0));
+ pieces.emplace_back("G",
+ PiecePoints{ CoordPoint(2, -1), CoordPoint(1, 0),
+ CoordPoint(0, 1), CoordPoint(1, 2)},
+ geo, transforms, geometry_type, CoordPoint(1, 0));
+ pieces.emplace_back("O",
+ PiecePoints{ CoordPoint(1, 0), CoordPoint(2, 1),
+ CoordPoint(0, 1), CoordPoint(1, 2)},
+ geo, transforms, geometry_type, 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, geometry_type, CoordPoint(0, 1));
+ pieces.emplace_back("L3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1), CoordPoint(1, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 1));
+ pieces.emplace_back("T3",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(1, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 1));
+ pieces.emplace_back("Z3",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, 1),
+ CoordPoint(1, 2) },
+ geo, transforms, geometry_type, CoordPoint(0, 1));
+ pieces.emplace_back("U3",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(1, 0),
+ CoordPoint(2, -1) },
+ geo, transforms, geometry_type, CoordPoint(1, 0));
+ pieces.emplace_back("V2",
+ PiecePoints{ CoordPoint(-1, 0), CoordPoint(0, -1) },
+ geo, transforms, geometry_type, CoordPoint(-1, 0));
+ pieces.emplace_back("I2",
+ PiecePoints{ CoordPoint(0, -1), CoordPoint(0, 0),
+ CoordPoint(0, 1) },
+ geo, transforms, geometry_type, CoordPoint(0, 1));
+ pieces.emplace_back("1",
+ PiecePoints{ CoordPoint(1, 0) },
+ geo, transforms, geometry_type, 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_range = Move::onboard_moves_classic;
+ break;
+ case BoardType::trigon:
+ m_range = Move::onboard_moves_trigon;
+ break;
+ case BoardType::trigon_3:
+ m_range = Move::onboard_moves_trigon_3;
+ break;
+ case BoardType::duo:
+ if (piece_set == PieceSet::classic)
+ m_range = Move::onboard_moves_duo;
+ else
+ {
+ LIBBOARDGAME_ASSERT(piece_set == PieceSet::junior);
+ m_range = Move::onboard_moves_junior;
+ }
+ break;
+ case BoardType::nexos:
+ m_range = Move::onboard_moves_nexos;
+ break;
+ case BoardType::callisto:
+ m_range = Move::onboard_moves_callisto;
+ break;
+ case BoardType::callisto_2:
+ m_range = Move::onboard_moves_callisto_2;
+ break;
+ case BoardType::callisto_3:
+ m_range = Move::onboard_moves_callisto_3;
+ break;
+ case BoardType::gembloq:
+ m_range = Move::onboard_moves_gembloq;
+ break;
+ case BoardType::gembloq_2:
+ m_range = Move::onboard_moves_gembloq_2;
+ break;
+ case BoardType::gembloq_3:
+ m_range = Move::onboard_moves_gembloq_3;
+ break;
+ }
+ ++m_range; // Move::null()
+ switch (piece_set)
+ {
+ case PieceSet::classic:
+ m_transforms = make_unique<PieceTransformsClassic>();
+ m_pieces = create_pieces_classic(m_geo, *m_transforms);
+ m_max_piece_size = 5;
+ m_max_adj_attach = 16;
+ m_move_info.reset(calloc(m_range, sizeof(MoveInfo<5>)));
+ m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<16>)));
+ break;
+ case PieceSet::junior:
+ m_transforms = make_unique<PieceTransformsClassic>();
+ m_pieces = create_pieces_junior(m_geo, *m_transforms);
+ m_max_piece_size = 5;
+ m_max_adj_attach = 16;
+ m_move_info.reset(calloc(m_range, sizeof(MoveInfo<5>)));
+ m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<16>)));
+ break;
+ case PieceSet::trigon:
+ m_transforms = make_unique<PieceTransformsTrigon>();
+ m_pieces = create_pieces_trigon(m_geo, *m_transforms);
+ m_max_piece_size = 6;
+ m_max_adj_attach = 22;
+ m_move_info.reset(calloc(m_range, sizeof(MoveInfo<6>)));
+ m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<22>)));
+ break;
+ case PieceSet::nexos:
+ m_transforms = make_unique<PieceTransformsClassic>();
+ m_pieces = create_pieces_nexos(m_geo, *m_transforms);
+ m_max_piece_size = 7;
+ m_max_adj_attach = 12;
+ m_move_info.reset(calloc(m_range, sizeof(MoveInfo<7>)));
+ m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<12>)));
+ break;
+ case PieceSet::callisto:
+ m_transforms = make_unique<PieceTransformsClassic>();
+ m_pieces = create_pieces_callisto(m_geo, *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_range, sizeof(MoveInfo<5>)));
+ m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<16>)));
+ break;
+ case PieceSet::gembloq:
+ m_transforms = make_unique<PieceTransformsGembloQ>();
+ m_pieces = create_pieces_gembloq(m_geo, *m_transforms);
+ m_max_piece_size = 22;
+ m_max_adj_attach = 44;
+ m_move_info.reset(calloc(m_range, sizeof(MoveInfo<22>)));
+ m_move_info_ext.reset(calloc(m_range, sizeof(MoveInfoExt<44>)));
+ break;
+ }
+ m_move_info_ext_2 = make_unique<MoveInfoExt2[]>(m_range);
+ m_nu_pieces = static_cast<Piece::IntType>(m_pieces.size());
+ for (Point p : m_geo)
+ if (has_adj_status_points(p))
+ init_adj_status_points(p);
+ 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;
+ case PieceSet::gembloq:
+ LIBBOARDGAME_ASSERT(m_nu_pieces == 21);
+ 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>();
+ else if (board_type == BoardType::gembloq_2)
+ init_symmetry_info<22>();
+}
+
+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_range);
+ 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)
+ {
+ LIBBOARDGAME_ASSERT(has_adj_status_points(*i));
+ auto j = m_adj_status_points[*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 if (m_max_piece_size == 7)
+ create_moves<7, 12>(moves_created, piece);
+ else
+ create_moves<22, 44>(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_range);
+ 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 = static_cast<int>(m_geo.get_x(p));
+ auto y = static_cast<int>(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)
+ {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ auto& transform = *transforms[i];
+ LIBBOARDGAME_LOG("Transformation ", get_type_name(transform));
+#endif
+ }
+ if (transforms[i]->get_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(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));
+ }
+ }
+}
+
+bool BoardConst::from_string(Move& mv, const string& s) const
+{
+ if (s == "null")
+ {
+ mv = Move::null();
+ return true;
+ }
+ MovePoints points;
+ auto begin = s.begin();
+ auto end = begin;
+ while (true)
+ {
+ while (end != s.end() && *end != ',')
+ ++end;
+ Point p;
+ if (! m_geo.from_string(begin, end, p))
+ return false;
+ if (points.size() == MovePoints::max_size)
+ return false;
+ points.push_back(p);
+ if (end == s.end())
+ break;
+ ++end;
+ begin = end;
+ }
+ return find_move(points, 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;
+}
+
+Piece BoardConst::get_move_piece(Move mv) const
+{
+ if (m_max_piece_size == 5)
+ return get_move_piece<5>(mv);
+ if (m_max_piece_size == 6)
+ return get_move_piece<6>(mv);
+ if (m_max_piece_size == 7)
+ return get_move_piece<7>(mv);
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+ return get_move_piece<22>(mv);
+}
+
+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
+{
+ if (points.empty())
+ return false;
+ 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 (equal(sorted_points.begin(), sorted_points.end(),
+ info_ext_2.begin_scored_points(),
+ info_ext_2.end_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;
+}
+
+/** Builds the list of neighboring points that is used for the adjacent
+ status for matching precompted move lists. */
+void BoardConst::init_adj_status_points(Point p)
+{
+ // The order of points affects the size of the precomputed lists. The
+ // following algorithm does well but is not optimal for all geometries.
+ auto& points = m_adj_status_points[p];
+ const auto max_size = PrecompMoves::adj_status_nu_adj;
+ unsigned n = 0;
+ auto add_adj = [&](Point p)
+ {
+ for (Point pp : m_geo.get_adj(p))
+ {
+ if (n == max_size)
+ return;
+ auto end = points.begin() + n;
+ if (find(points.begin(), end, pp) == end)
+ points[n++] = pp;
+ }
+ };
+ auto add_diag = [&](Point p)
+ {
+ for (Point pp : m_geo.get_diag(p))
+ {
+ if (n == max_size)
+ return;
+ auto end = points.begin() + n;
+ if (find(points.begin(), end, pp) == end)
+ points[n++] = pp;
+ }
+ };
+ add_adj(p);
+ add_diag(p);
+ auto old_n = n;
+ if (n < max_size)
+ {
+ for (unsigned i = 0; i < old_n; ++i)
+ {
+ add_adj(points[i]);
+ if (n == max_size)
+ break;
+ }
+ }
+ if (n < max_size)
+ {
+ for (unsigned i = 0; i < old_n; ++i)
+ {
+ add_diag(points[i]);
+ if (n == max_size)
+ break;
+ }
+ }
+
+ LIBBOARDGAME_ASSERT(n == max_size);
+}
+
+template<unsigned MAX_SIZE>
+void BoardConst::init_symmetry_info()
+{
+ m_symmetric_points.init(m_geo);
+ for (Move::IntType i = 1; i < m_range; ++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;
+ }
+ }
+}
+
+void BoardConst::sort(MovePoints& points) const
+{
+ auto less = [this](Point a, Point b)
+ {
+ return this->m_compare_val[a] < this->m_compare_val[b];
+ };
+ auto check = [&](unsigned short a, unsigned short b)
+ {
+ if (! less(points[a], 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;
+ case 1:
+ break;
+ default:
+ std::sort(points.begin(), points.end(), less);
+ }
+}
+
+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 "MoveInfo.h"
+#include "PieceInfo.h"
+#include "PrecompMoves.h"
+#include "SymmetricPoints.h"
+#include "Variant.h"
+#include "libboardgame_util/Range.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+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_points() */
+ using AdjStatusPoints = array<Point, PrecompMoves::adj_status_nu_adj>;
+
+ /** 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(). */
+ using MoveInfoArray = const void*;
+
+ /** 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(). */
+ using MoveInfoExtArray = const void*;
+
+
+ /** 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;
+
+ Move::IntType get_range() const { return m_range; }
+
+ 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;
+
+ /** Array containing the points used for the adjacent status.
+ Contains a selection of first-order or second-order adjacent and
+ diagonal neighbor points.
+ @pre has_adj_status_points(p) */
+ const AdjStatusPoints& get_adj_status_points(Point p) const
+ {
+ return m_adj_status_points[p];
+ }
+
+ /** Adjacent status arrays are not initialized for junction points in
+ Nexos. */
+ bool has_adj_status_points(Point p) const
+ {
+ return m_board_type != BoardType::nexos || m_geo.get_point_type(p) != 0;
+ }
+
+ /** Only initialized in game variants with central symmetry of board
+ including starting 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;
+
+ bool from_string(Move& mv, 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;
+
+ Move::IntType m_range;
+
+ 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<AdjStatusPoints> m_adj_status_points;
+
+ 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_points(Point p);
+
+ 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_range);
+ 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 Range<const Point> BoardConst::get_move_points(Move mv) const
+{
+ if (m_max_piece_size == 5)
+ {
+ auto& info = get_move_info<5>(mv);
+ return {info.begin(), info.end()};
+ }
+ if (m_max_piece_size == 6)
+ {
+ auto& info = get_move_info<6>(mv);
+ return {info.begin(), info.end()};
+ }
+ if (m_max_piece_size == 7)
+ {
+ auto& info = get_move_info<7>(mv);
+ return {info.begin(), info.end()};
+ }
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+ auto& info = get_move_info<22>(mv);
+ return {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);
+ if (m_max_piece_size == 6)
+ return get_move_points_begin<6>(mv);
+ if (m_max_piece_size == 7)
+ return get_move_points_begin<7>(mv);
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+ return get_move_points_begin<22>(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_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 */
+//-----------------------------------------------------------------------------
+
+#include "BoardUpdater.h"
+
+#include "BoardUtil.h"
+#include "NodeUtil.h"
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::SgfError;
+using libboardgame_sgf::get_path_from_root;
+using libpentobi_base::get_current_position_as_setup;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+/** List to hold remaining pieces of a color with one entry for each instance
+ of the same piece. */
+using AllPiecesLeftList =
+ ArrayList<Piece, PieceInfo::max_instances * Piece::max_pieces>;
+
+/** 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;
+ for (auto& s : node.get_multi_property(id))
+ {
+ Move mv;
+ if (! bd.from_string(mv, s))
+ throw SgfError("invalid move " + s);
+ Piece piece = bd.get_move_piece(mv);
+ if (! pieces_left[c].remove(piece))
+ throw SgfError("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;
+ for (auto& s : node.get_multi_property("AE"))
+ {
+ Move mv;
+ if (! bd.from_string(mv, s))
+ throw SgfError("invalid move " + s);
+ for (Color c : bd.get_colors())
+ if (setup.placements[c].remove(mv))
+ {
+ Piece piece = bd.get_move_piece(mv);
+ pieces_left[c].push_back(piece);
+ break;
+ }
+ }
+}
+
+/** 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::get_player(node, bd.get_nu_colors(),
+ 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::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 SgfError("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 */
+//-----------------------------------------------------------------------------
+
+#include "BoardUtil.h"
+
+#include "PentobiSgfUtil.h"
+#ifdef LIBBOARDGAME_DEBUG
+#include <sstream>
+#endif
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+#ifdef 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))
+ {
+ auto& placements = setup.placements[c];
+ if (placements.empty())
+ continue;
+ vector<string> values;
+ values.reserve(placements.size());
+ for (Move mv : placements)
+ values.push_back(board_const.to_string(mv, false));
+ writer.write_property(get_setup_id(variant, c), values);
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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_BOARD_UTIL_H
+#define LIBPENTOBI_BASE_BOARD_UTIL_H
+
+#include "Board.h"
+#include "libboardgame_sgf/Writer.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::Writer;
+
+//-----------------------------------------------------------------------------
+
+#ifdef 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 libpentobi_base
+
+#endif // LIBPENTOBI_BASE_BOARD_UTIL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/Book.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Book.h"
+
+#include "BoardUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_util/Log.h"
+
+//-----------------------------------------------------------------------------
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::TreeReader;
+
+//-----------------------------------------------------------------------------
+
+Book::Book(Variant variant)
+ : m_tree(variant)
+{
+ get_transforms(variant, m_transforms, m_inv_transforms);
+}
+
+Book::~Book() = default; // Non-inline to avoid GCC -Winline warning
+
+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 == nullptr)
+ return false;
+ }
+ node = select_child(bd, c, m_tree, *node, inv_transform);
+ if (node == nullptr)
+ 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 (SgfTree::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());
+ auto 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:
+ using PointTransform = libboardgame_base::PointTransform<Point>;
+
+
+ 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_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
+ ColorMap.h
+ ColorMove.h
+ Game.h
+ Game.cpp
+ GembloQGeometry.h
+ GembloQGeometry.cpp
+ GembloQTransform.h
+ GembloQTransform.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
+ PieceTransformsGembloQ.h
+ PieceTransformsGembloQ.cpp
+ PieceTransforms.h
+ PieceTransforms.cpp
+ PieceTransformsTrigon.h
+ PieceTransformsTrigon.cpp
+ PlayerBase.h
+ PlayerBase.cpp
+ Point.h
+ PointList.h
+ PointState.h
+ 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_SRCS ${pentobi_base_SRCS}
+ Engine.cpp
+ Engine.h
+ )
+endif()
+
+add_library(pentobi_base STATIC ${pentobi_base_SRCS})
+
+target_include_directories(pentobi_base PUBLIC ..)
+
+target_link_libraries(pentobi_base boardgame_sgf boardgame_base)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/CallistoGeometry.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CallistoGeometry.h"
+
+#include <map>
+#include <memory>
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+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
+
+//-----------------------------------------------------------------------------
+
+CallistoGeometry::CallistoGeometry(unsigned nu_colors)
+{
+ unsigned sz = get_size_callisto(nu_colors);
+ m_edge = get_edge_callisto(nu_colors);
+ Geometry::init(sz, sz);
+}
+
+const CallistoGeometry& CallistoGeometry::get(unsigned nu_colors)
+{
+ static map<unsigned, shared_ptr<CallistoGeometry>> s_geometry;
+
+ auto pos = s_geometry.find(nu_colors);
+ if (pos != s_geometry.end())
+ return *pos->second;
+ shared_ptr<CallistoGeometry> geometry(new CallistoGeometry(nu_colors));
+ return *s_geometry.insert(make_pair(nu_colors, 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_colors)
+{
+ auto size = get_size_callisto(nu_colors);
+ 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 "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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_colors The number of colors (2, 3, or 4). */
+ static const CallistoGeometry& get(unsigned nu_colors);
+
+ static bool is_center_section(unsigned x, unsigned y, unsigned nu_colors);
+
+
+ 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:
+ unsigned m_edge;
+
+
+ explicit CallistoGeometry(unsigned nu_colors);
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_CALLISTO_GEOMETRY_H
--- /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 "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+class Color
+{
+public:
+ using IntType = uint_fast8_t;
+
+ 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);
+
+ bool operator==(Color c) const;
+
+ bool operator!=(Color c) const;
+
+ bool operator<(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()
+{
+#ifdef LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+inline Color::Color(IntType i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = i;
+}
+
+inline bool Color::operator==(Color c) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(c.is_initialized());
+ return m_i == c.m_i;
+}
+
+inline bool Color::operator!=(Color c) const
+{
+ return ! operator==(c);
+}
+
+inline bool Color::operator<(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;
+}
+
+//-----------------------------------------------------------------------------
+
+/** 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 "Color.h"
+#include "Move.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+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==(ColorMove mv) const;
+
+ /** Inequality operator.
+ @pre move, color, mv.move, mv.color are initialized. */
+ bool operator!=(ColorMove mv) const;
+
+ bool is_null() const;
+};
+
+inline ColorMove::ColorMove(Color c, Move mv)
+ : color(c),
+ move(mv)
+{
+}
+
+inline bool ColorMove::operator==(ColorMove mv) const
+{
+ return move == mv.move && color == mv.color;
+}
+
+inline bool ColorMove::operator!=(ColorMove mv) const
+{
+ return ! operator==(mv);
+}
+
+inline bool ColorMove::is_null() const
+{
+ return move.is_null();
+}
+
+inline ColorMove ColorMove::null()
+{
+ return {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 */
+//-----------------------------------------------------------------------------
+
+#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::TreeReader;
+using libboardgame_sgf::get_last_node;
+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(Arguments args, Response& response)
+{
+ auto& bd = get_board();
+ auto moves = make_unique<MoveList>();
+ auto marker = make_unique<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(Arguments args, Response& response)
+{
+ genmove(get_color_arg(args), response);
+}
+
+void Engine::cmd_get_place(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(Arguments args)
+{
+ args.check_size_less_equal(2);
+ string file = args.get(0);
+ unsigned move_number = 0;
+ if (args.get_size() == 2)
+ move_number = args.parse_min<unsigned>(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 > 0)
+ node = m_game.get_tree().get_node_before_move_number(move_number - 1);
+ if (node == nullptr)
+ 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(Arguments args, Response& response)
+{
+ auto& bd = get_board();
+ Move mv;
+ try
+ {
+ mv = Move(args.parse<Move::IntType>());
+ }
+ catch (const Failure&)
+ {
+ if (! bd.from_string(mv, args.get()))
+ {
+ 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(Arguments args)
+{
+ play(get_board().get_to_play(), args, 0);
+}
+
+void Engine::cmd_param_base(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(Arguments args)
+{
+ play(get_color_arg(args, 0), args, 1);
+}
+
+void Engine::cmd_point_integers(Response& response)
+{
+ auto& geo = get_board().get_geometry();
+ Grid<Point::IntType> grid;
+ for (Point p : geo)
+ grid[p] = p.to_int();
+ response << '\n' << grid.to_string(geo);
+}
+
+void Engine::cmd_reg_genmove(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(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(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(Arguments args) const
+{
+ if (args.get_size() > 1)
+ throw Failure("too many arguments");
+ return get_color_arg(args, 0);
+}
+
+Color Engine::get_color_arg(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 == nullptr)
+ throw Failure("no player set");
+ return *m_player;
+}
+
+void Engine::play(Color c, Arguments args, unsigned arg_move_begin)
+{
+ auto& bd = get_board();
+ if (bd.get_nu_moves() >= Board::max_moves)
+ throw Failure("too many moves");
+ Move mv;
+ if (arg_move_begin == 0)
+ {
+ if (! bd.from_string(mv, args.get_line()))
+ throw Failure("invalid move ");
+ }
+ else
+ {
+ if (! bd.from_string(mv, args.get_remaining_line(arg_move_begin - 1)))
+ throw Failure("invalid move ");
+ }
+ if (mv.is_null())
+ throw Failure("play pass not supported (anymore)");
+ // Board::play() can handle illegal moves at arbitrary positions, even
+ // overlapping, but it does not check (for performance reasons) if the
+ // piece-left count is already zero.
+ if (! bd.is_piece_left(c, bd.get_move_piece(mv)))
+ throw Failure("piece already played");
+ 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 "Game.h"
+#include "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(Arguments args, Response& response);
+ void cmd_clear_board();
+ void cmd_final_score(Response& response);
+ void cmd_g(Response& response);
+ void cmd_genmove(Arguments args, Response& response);
+ void cmd_get_place(Arguments args, Response& response);
+ void cmd_loadsgf(Arguments args);
+ void cmd_move_info(Arguments args, Response& response);
+ void cmd_p(Arguments args);
+ void cmd_param_base(Arguments args, Response& response);
+ void cmd_play(Arguments args);
+ void cmd_point_integers(Response& response);
+ void cmd_showboard(Response& response);
+ void cmd_reg_genmove(Arguments args, Response& response);
+ void cmd_savesgf(Arguments args);
+ void cmd_set_game(Arguments args);
+ 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(Arguments args, unsigned i) const;
+
+ Color get_color_arg(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, 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 */
+//-----------------------------------------------------------------------------
+
+#include "Game.h"
+
+#include "libboardgame_sgf/SgfError.h"
+#include "libboardgame_sgf/SgfUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::SgfError;
+using libboardgame_sgf::back_to_main_variation;
+using libboardgame_sgf::is_main_variation;
+
+//-----------------------------------------------------------------------------
+
+Game::Game(Variant variant)
+ : m_bd(new Board(variant)),
+ m_tree(variant)
+{
+ init(variant);
+}
+
+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();
+}
+
+string Game::get_charset() const
+{
+ return get_root().get_property("CA", "");
+}
+
+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);
+ do
+ {
+ auto mv = tree.get_move(*node);
+ if (! mv.is_null())
+ {
+ next = bd.get_next(mv.color);
+ break;
+ }
+ Color c;
+ if (libpentobi_base::get_player(*node, bd.get_nu_colors(), c))
+ return c;
+ node = node->get_parent_or_null();
+ }
+ while (node != nullptr);
+ return bd.get_effective_to_play(next);
+}
+
+void Game::goto_node(const SgfNode& node)
+{
+ auto old = m_current;
+ try
+ {
+ update(node);
+ }
+ catch (const SgfError&)
+ {
+ // Try to restore the old state.
+ if (&node != old)
+ {
+ try
+ {
+ update(*old);
+ }
+ catch (const SgfError&)
+ {
+ }
+ }
+ 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 = &m_tree.get_root();
+ 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 = &m_tree.get_root();
+ goto_node(m_tree.get_root());
+}
+
+void Game::keep_only_subtree()
+{
+ m_tree.keep_only_subtree(*m_current);
+ m_current = &m_tree.get_root();
+ 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 != nullptr)
+ 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()
+{
+ if (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.has_move(*m_current));
+ 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 explicitly 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);
+
+
+ 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 SgfError 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 SgfError if the game was constructed with an
+ external SGF tree and the tree contained invalid property values
+ (syntactically or semantically, 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_tree().has_move(get_current())
+ @pre get_current()->has_parent()
+ @note Even if the implementation of this function calls goto_node(),
+ it cannot throw an InvalidProperty 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;
+
+ /** Add final score to root node if the current node is in the main
+ variation. */
+ void set_result(int score);
+
+ string get_charset() const;
+
+ void set_charset(const string& charset);
+
+ void remove_move_annotation(const SgfNode& node);
+
+ double get_bad_move(const SgfNode& node) const;
+
+ double get_good_move(const SgfNode& node) const;
+
+ bool is_doubtful_move(const SgfNode& node) const;
+
+ bool is_interesting_move(const SgfNode& node) const;
+
+ void set_bad_move(const SgfNode& node, double value = 1);
+
+ void set_good_move(const SgfNode& node, double value = 1);
+
+ void set_doubtful_move(const SgfNode& node);
+
+ void set_interesting_move(const SgfNode& node);
+
+ 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::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(bool is_modified = true);
+
+ 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 SgfNode& node) const
+{
+ return SgfTree::get_bad_move(node);
+}
+
+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 SgfNode& node) const
+{
+ return SgfTree::get_good_move(node);
+}
+
+inline ColorMove Game::get_move() const
+{
+ return m_tree.get_move(*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::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 SgfNode& node) const
+{
+ return SgfTree::is_doubtful_move(node);
+}
+
+inline bool Game::is_interesting_move(const SgfNode& node) const
+{
+ return SgfTree::is_interesting_move(node);
+}
+
+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(const SgfNode& node)
+{
+ m_tree.remove_move_annotation(node);
+}
+
+inline void Game::set_application(const string& name, const string& version)
+{
+ m_tree.set_application(name, version);
+}
+
+inline void Game::set_bad_move(const SgfNode& node, double value)
+{
+ m_tree.set_bad_move(node, 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(const SgfNode& node)
+{
+ m_tree.set_doubtful_move(node);
+}
+
+inline void Game::set_good_move(const SgfNode& node, double value)
+{
+ m_tree.set_good_move(node, value);
+}
+
+inline void Game::set_interesting_move(const SgfNode& node)
+{
+ m_tree.set_interesting_move(node);
+}
+
+inline void Game::set_modified(bool is_modified)
+{
+ m_tree.set_modified(is_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/GembloQGeometry.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GembloQGeometry.h"
+
+#include <map>
+#include <memory>
+#include "libboardgame_util/MathUtil.h"
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::CoordPoint;
+using libboardgame_util::mod;
+
+//-----------------------------------------------------------------------------
+
+GembloQGeometry::GembloQGeometry(unsigned nu_players)
+{
+ unsigned height;
+ if (nu_players == 2)
+ {
+ height = 22;
+ m_edge = 4;
+ }
+ else if (nu_players == 3)
+ {
+ height = 26;
+ m_edge = 6;
+ }
+ else
+ {
+ LIBBOARDGAME_ASSERT(nu_players == 4);
+ height = 28;
+ m_edge = 13;
+ }
+ Geometry::init(2 * height, height);
+}
+
+const GembloQGeometry& GembloQGeometry::get(unsigned nu_players)
+{
+ static map<unsigned, shared_ptr<GembloQGeometry>> s_geometry;
+
+ auto pos = s_geometry.find(nu_players);
+ if (pos != s_geometry.end())
+ return *pos->second;
+ shared_ptr<GembloQGeometry> geometry(new GembloQGeometry(nu_players));
+ return *s_geometry.insert(make_pair(nu_players, geometry)).first->second;
+}
+
+auto GembloQGeometry::get_adj_coord(int x, int y) const -> AdjCoordList
+{
+ AdjCoordList l;
+ l.push_back(CoordPoint(x + 1, y));
+ l.push_back(CoordPoint(x - 1, y));
+ switch (get_point_type(x, y))
+ {
+ case 0:
+ case 3:
+ l.push_back(CoordPoint(x, y - 1));
+ break;
+ case 1:
+ case 2:
+ l.push_back(CoordPoint(x, y + 1));
+ break;
+ }
+ return l;
+}
+
+auto GembloQGeometry::get_diag_coord(int x, int y) const -> DiagCoordList
+{
+ // See Geometry::get_diag_coord() about advantageous ordering of the list
+ DiagCoordList l;
+ switch (get_point_type(x, y))
+ {
+ case 0:
+ l.push_back(CoordPoint(x + 2, 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 + 3, y));
+ l.push_back(CoordPoint(x - 2, y + 1));
+ l.push_back(CoordPoint(x + 1, y + 1));
+ l.push_back(CoordPoint(x + 3, y - 1));
+ l.push_back(CoordPoint(x - 2, y));
+ l.push_back(CoordPoint(x + 2, y));
+ l.push_back(CoordPoint(x + 1, y - 1));
+ break;
+ case 1:
+ l.push_back(CoordPoint(x - 2, 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 - 3, y));
+ l.push_back(CoordPoint(x + 2, y - 1));
+ l.push_back(CoordPoint(x - 1, y - 1));
+ l.push_back(CoordPoint(x - 3, y + 1));
+ l.push_back(CoordPoint(x + 2, y));
+ l.push_back(CoordPoint(x - 2, y));
+ l.push_back(CoordPoint(x - 1, y + 1));
+ break;
+ case 2:
+ l.push_back(CoordPoint(x - 2, y - 1));
+ l.push_back(CoordPoint(x + 3, y + 1));
+ l.push_back(CoordPoint(x - 1, y + 1));
+ l.push_back(CoordPoint(x, y - 1));
+ l.push_back(CoordPoint(x + 3, y));
+ l.push_back(CoordPoint(x + 2, y + 1));
+ l.push_back(CoordPoint(x + 1, 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));
+ break;
+ case 3:
+ l.push_back(CoordPoint(x - 3, y - 1));
+ l.push_back(CoordPoint(x + 2, y + 1));
+ l.push_back(CoordPoint(x + 1, y - 1));
+ l.push_back(CoordPoint(x, y + 1));
+ l.push_back(CoordPoint(x - 3, y));
+ l.push_back(CoordPoint(x - 2, y - 1));
+ l.push_back(CoordPoint(x - 1, 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));
+ break;
+ }
+ return l;
+}
+
+unsigned GembloQGeometry::get_period_x() const
+{
+ return 4;
+}
+
+unsigned GembloQGeometry::get_period_y() const
+{
+ return 2;
+}
+
+unsigned GembloQGeometry::get_point_type(int x, int y) const
+{
+ return mod(x + 2 * static_cast<int>(y % 2 != 0), 4);
+}
+
+bool GembloQGeometry::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 = (width - 4 * m_edge) / 2 - 1 > 2 * dy ?
+ (width - 4 * m_edge) / 2 - 1 - 2 * dy : 0;
+ unsigned max_x = width - min_x - 1;
+ return x >= min_x && x <= max_x;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/GembloQGeometry.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_GEMBLOQ_GEOMETRY_H
+#define LIBPENTOBI_BASE_GEMBLOQ_GEOMETRY_H
+
+#include "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** Geometry for the board game GembloQ.
+ Each square on the board consists of four triangles, each half-square of
+ two triangles. The coordinates are like this:
+ <tt>
+ 0 1 2 3 4 5 6 7 8
+ 0 | / | \ | / | \ | /
+ 1 | \ | / | \ | / | \
+ 2 | / | \ | / | \ | /
+ 3 | \ | / | \ | / | \
+ </tt>
+ The point types are determined by the location of the right angle of the
+ triangle: 0: top/left, 1=down/right, 2=down/left, 3=up/right. */
+class GembloQGeometry final
+ : public Geometry
+{
+public:
+ /** Create or reuse an already created geometry.
+ @param nu_players The number of players (2, 3, or 4). */
+ static const GembloQGeometry& get(unsigned nu_players);
+
+
+ explicit GembloQGeometry(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:
+ unsigned m_edge;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_GEMBLOQ_GEOMETRY_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/GembloQTransform.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GembloQTransform.h"
+
+#include "libboardgame_util/MathUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_util::mod;
+
+//-----------------------------------------------------------------------------
+
+namespace
+{
+
+/** Divide integer by 2 and round down. */
+int div2(int a)
+{
+ return a < 0 ? (a - 1) / 2 : a / 2;
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQIdentity::get_transformed(CoordPoint p) const
+{
+ return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot90::get_transformed(CoordPoint p) const
+{
+ auto y = div2(p.x);
+ auto x = -2 * p.y;
+ switch (mod(p.x, 4))
+ {
+ case 0:
+ case 3:
+ x -= static_cast<int>(p.y % 2 != 0);
+ break;
+ case 1:
+ case 2:
+ x -= static_cast<int>(p.y % 2 == 0);
+ break;
+ }
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot180::get_transformed(CoordPoint p) const
+{
+ return {-p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot270::get_transformed(CoordPoint p) const
+{
+ auto y = -div2(p.x);
+ auto x = 2 * p.y;
+ switch (mod(p.x, 4))
+ {
+ case 0:
+ case 3:
+ x += static_cast<int>(p.y % 2 != 0);
+ break;
+ case 1:
+ case 2:
+ x += static_cast<int>(p.y % 2 == 0);
+ break;
+ }
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRefl::get_transformed(CoordPoint p) const
+{
+ return {-p.x, p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot90Refl::get_transformed(CoordPoint p) const
+{
+ auto y = -div2(p.x);
+ auto x = -2 * p.y;
+ switch (mod(p.x, 4))
+ {
+ case 0:
+ case 3:
+ x -= static_cast<int>(p.y % 2 != 0);
+ break;
+ case 1:
+ case 2:
+ x -= static_cast<int>(p.y % 2 == 0);
+ break;
+ }
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot180Refl::get_transformed(CoordPoint p) const
+{
+ return {p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfGembloQRot270Refl::get_transformed(CoordPoint p) const
+{
+ auto y = div2(p.x);
+ auto x = 2 * p.y;
+ switch (mod(p.x, 4))
+ {
+ case 0:
+ case 3:
+ x += static_cast<int>(p.y % 2 != 0);
+ break;
+ case 1:
+ case 2:
+ x += static_cast<int>(p.y % 2 == 0);
+ break;
+ }
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/GembloQTransform.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_GEMBLOQ_TRANSFORM_H
+#define LIBPENTOBI_BASE_GEMBLOQ_TRANSFORM_H
+
+#include "libboardgame_base/Transform.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::CoordPoint;
+using libboardgame_base::Transform;
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQIdentity
+ : public Transform
+{
+public:
+ TransfGembloQIdentity() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot90
+ : public Transform
+{
+public:
+ TransfGembloQRot90() : Transform(3) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot180
+ : public Transform
+{
+public:
+ TransfGembloQRot180() : Transform(1) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot270
+ : public Transform
+{
+public:
+ TransfGembloQRot270() : Transform(2) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRefl
+ : public Transform
+{
+public:
+ TransfGembloQRefl() : Transform(3) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot90Refl
+ : public Transform
+{
+public:
+ TransfGembloQRot90Refl() : Transform(1) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot180Refl
+ : public Transform
+{
+public:
+ TransfGembloQRot180Refl() : Transform(2) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfGembloQRot270Refl
+ : public Transform
+{
+public:
+ TransfGembloQRot270Refl() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_GEMBLOQ_TRANSFORM_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 {
+
+//-----------------------------------------------------------------------------
+
+using Geometry = libboardgame_base::Geometry<Point>;
+
+//-----------------------------------------------------------------------------
+
+} // 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 {
+
+//-----------------------------------------------------------------------------
+
+using Marker = libboardgame_base::Marker<Point>;
+
+//-----------------------------------------------------------------------------
+
+} // 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. */
+ using IntType = uint_least16_t;
+
+ 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;
+
+ static const IntType onboard_moves_gembloq = 31254;
+
+ static const IntType onboard_moves_gembloq_2 = 15018;
+
+ static const IntType onboard_moves_gembloq_3 = 23518;
+
+ /** Integer range of moves.
+ The maximum is given by the number of on-board moves in any game
+ variant, plus a null move. */
+ static const IntType range = onboard_moves_trigon + 1;
+
+ static Move null();
+
+ Move();
+
+ explicit Move(IntType i);
+
+ bool operator==(Move mv) const;
+
+ bool operator!=(Move mv) const;
+
+ bool operator<(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()
+{
+#ifdef LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+inline Move::Move(IntType i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = i;
+}
+
+inline bool Move::operator==(Move mv) const
+{
+ LIBBOARDGAME_ASSERT(is_initialized());
+ LIBBOARDGAME_ASSERT(mv.is_initialized());
+ return m_i == mv.m_i;
+}
+
+inline bool Move::operator!=(Move mv) const
+{
+ return ! operator==(mv);
+}
+
+inline bool Move::operator<(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); }
+
+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 initialized 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() */
+using MoveList = libboardgame_util::ArrayList<Move, Move::range - 1>;
+
+//-----------------------------------------------------------------------------
+
+} // 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;
+
+//-----------------------------------------------------------------------------
+
+using MovePoints = ArrayList<Point, PieceInfo::max_size, unsigned short>;
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#include "NexosGeometry.h"
+
+#include <memory>
+#include "libboardgame_util/Unused.h"
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+NexosGeometry::NexosGeometry()
+{
+ Geometry::init(25, 25);
+}
+
+const NexosGeometry& NexosGeometry::get()
+{
+ static unique_ptr<NexosGeometry> s_geometry;
+
+ if (! s_geometry)
+ s_geometry = make_unique<NexosGeometry>();
+ return *s_geometry;
+}
+
+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;
+ 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(static_cast<int>(x), static_cast<int>(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 "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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. */
+ static const NexosGeometry& get();
+
+
+ NexosGeometry();
+
+ 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;
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#include "NodeUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::InvalidProperty;
+
+//-----------------------------------------------------------------------------
+
+bool has_move(const SgfNode& node, Variant variant)
+{
+ // See also comment in get_move()
+ switch (get_nu_colors(variant))
+ {
+ case 2:
+ for (auto& prop : node.get_properties())
+ {
+ auto& id = prop.id;
+ if (id == "B" || id == "W" || id == "1" || id == "2"
+ || id == "BLUE" || id == "GREEN")
+ return true;
+ }
+ break;
+ case 3:
+ for (auto& prop : node.get_properties())
+ {
+ auto& id = prop.id;
+ if (id == "1" || id == "2" || id == "3" || id == "BLUE"
+ || id == "YELLOW" || id == "RED")
+ return true;
+ }
+ break;
+ case 4:
+ for (auto& prop : node.get_properties())
+ {
+ auto& id = prop.id;
+ if (id == "1" || id == "2" || id == "3" || id == "4"
+ || id == "BLUE" || id == "YELLOW" || id == "RED"
+ || id == "GREEN")
+ return true;
+ }
+ break;
+ default:
+ LIBBOARDGAME_ASSERT(false);
+ }
+ return false;
+}
+
+bool get_move(const SgfNode& node, Variant variant, Color& c,
+ MovePoints& points)
+{
+ auto nu_colors = get_nu_colors(variant);
+ 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 (nu_colors == 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() || c.to_int() >= nu_colors)
+ return false;
+ // 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);
+ for (auto& s : node.get_multi_property(id))
+ {
+ auto begin = s.begin();
+ auto end = begin;
+ while (true)
+ {
+ while (end != s.end() && *end != ',')
+ ++end;
+ Point p;
+ if (! geo.from_string(begin, end, p)
+ || points.size() == MovePoints::max_size)
+ throw InvalidProperty(id, string(begin, end));
+ points.push_back(p);
+ if (end == s.end())
+ break;
+ ++end;
+ begin = end;
+ }
+ }
+ return true;
+}
+
+bool get_player(const SgfNode& node, Color::IntType nu_colors, 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" && nu_colors > 2)
+ c = Color(2);
+ else if (value == "4" && nu_colors > 3)
+ c = Color(3);
+ else
+ return false;
+ 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 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 {
+
+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);
+
+bool has_move(const SgfNode& node, Variant variant);
+
+/** 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::IntType nu_colors, Color& c);
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#include "PentobiSgfUtil.h"
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+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 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 {
+
+//-----------------------------------------------------------------------------
+
+/** 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 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 */
+//-----------------------------------------------------------------------------
+
+#include "PentobiTree.h"
+
+#include "BoardUpdater.h"
+#include "BoardUtil.h"
+
+namespace libpentobi_base {
+
+using libboardgame_sgf::InvalidProperty;
+using libboardgame_sgf::SgfError;
+using libpentobi_base::get_current_position_as_setup;
+
+//-----------------------------------------------------------------------------
+
+PentobiTree::PentobiTree(Variant variant)
+{
+ init_variant(variant);
+}
+
+PentobiTree::PentobiTree(unique_ptr<SgfNode>& root)
+{
+ PentobiTree::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;
+ auto 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);
+ auto add_color = get_setup_property(*result, id);
+ if (! add_color.contains(mv)
+ && add_color.size() < Setup::PlacementList::max_size)
+ {
+ add_color.push_back(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::get_move(node, m_variant, c, points))
+ return ColorMove::null();
+ Move mv;
+ if (! m_bc->find_move(points, mv))
+ throw SgfError("Tree contains illegal move");
+ return {c, mv};
+}
+
+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 (has_move(child) && 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
+{
+ Setup::PlacementList result;
+ if (node.has_property(id))
+ for (auto& s : node.get_multi_property(id))
+ {
+ if (result.size() == Setup::PlacementList::max_size)
+ throw InvalidProperty(id, s);
+ Move mv;
+ if (! m_bc->from_string(mv, s))
+ throw InvalidProperty(id, s);
+ result.push_back(mv);
+ }
+ return result;
+}
+
+Variant PentobiTree::get_variant(const SgfNode& root)
+{
+ string game = root.get_property("GM");
+ Variant variant;
+ if (! parse_variant(game, variant))
+ throw InvalidProperty("GM", game);
+ return variant;
+}
+
+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 != nullptr)
+ {
+ if (has_move(*current) || has_setup(*current))
+ {
+ create_new_setup = true;
+ break;
+ }
+ current = current->get_parent_or_null();
+ }
+ }
+ if (create_new_setup)
+ {
+ auto bd = make_unique<Board>(m_variant);
+ BoardUpdater updater;
+ updater.update(*bd, *this, node);
+ Setup setup;
+ get_current_position_as_setup(*bd, setup);
+ 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();
+}
+
+bool PentobiTree::remove_player(const SgfNode& node)
+{
+ return 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
+ {
+ auto add_empty = get_setup_property(*result, "AE");
+ if (! add_empty.contains(mv)
+ && add_empty.size() < Setup::PlacementList::max_size)
+ {
+ add_empty.push_back(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;
+ values.reserve(placements.size());
+ 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 "NodeUtil.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 InvalidProperty */
+ 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);
+
+ bool has_move(const SgfNode& node) const;
+
+ /** Return move or ColorMove::null() if node has no move property.
+ @throws SgfError if the node has a move property with an invalid
+ value. */
+ ColorMove get_move(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;
+
+ 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() */
+ bool 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 get_color_id(m_variant, c);
+}
+
+inline const char* PentobiTree::get_setup_prop_id(Color c) const
+{
+ return 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 libpentobi_base::has_move(node, m_variant);
+}
+
+inline void PentobiTree::set_move(const SgfNode& node, ColorMove mv)
+{
+ set_move(node, mv.color, mv.move);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PENTOBI_TREE_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PentobiTreeWriter.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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())
+{
+}
+
+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);
+
+ 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 <cstdint>
+#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:
+ using IntType = uint_fast8_t;
+
+ /** 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==(Piece piece) const;
+
+ bool operator!=(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()
+{
+#ifdef LIBBOARDGAME_DEBUG
+ m_i = value_uninitialized;
+#endif
+}
+
+inline Piece::Piece(IntType i)
+{
+ LIBBOARDGAME_ASSERT(i < range);
+ m_i = i;
+}
+
+inline bool Piece::operator==(Piece piece) const
+{
+ return m_i == piece.m_i;
+}
+
+inline bool Piece::operator!=(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 */
+//-----------------------------------------------------------------------------
+
+#include "PieceInfo.h"
+
+#include <algorithm>
+#include "libboardgame_base/GeometryUtil.h"
+#include "libboardgame_util/Assert.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_sys/Compiler.h"
+
+namespace libpentobi_base {
+
+using libboardgame_base::geometry_util::normalize_offset;
+using libboardgame_base::geometry_util::type_match_shift;
+using libboardgame_sys::get_type_name;
+
+//-----------------------------------------------------------------------------
+
+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;
+ }
+};
+
+#ifdef 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,
+ GeometryType geometry_type, CoordPoint label_pos,
+ unsigned nu_instances)
+ : m_nu_instances(nu_instances),
+ m_points(points),
+ m_label_pos(label_pos),
+ 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);
+ auto& all_transforms = transforms.get_all();
+ vector<NormalizedPoints> all_transformed_points;
+ all_transformed_points.reserve(all_transforms.size());
+ m_transforms.reserve(all_transforms.size()); // Upper limit
+ PiecePoints transformed_points;
+ for (auto transform : all_transforms)
+ {
+ if (log_piece_creation)
+ LIBBOARDGAME_LOG("Transformation ", get_type_name(*transform));
+ transformed_points = points;
+ transform->transform(transformed_points.begin(),
+ transformed_points.end());
+ NormalizedPoints normalized = normalize(transformed_points,
+ transform->get_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_transforms.size(), ")");
+ m_equivalent_transform[transform] = transform;
+ m_transforms.push_back(transform);
+ }
+ all_transformed_points.push_back(normalized);
+ }
+ if (geometry_type == GeometryType::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 (geometry_type == GeometryType::gembloq)
+ m_score_points = 0.25f * static_cast<ScoreType>(points.size());
+ else if (points.size() == 1 && geometry_type == GeometryType::callisto)
+ m_score_points = 0;
+ else
+ m_score_points = static_cast<ScoreType>(points.size());
+}
+
+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_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_transforms.begin();
+ auto end = m_transforms.end();
+ auto pos = find(begin, end, transform);
+ LIBBOARDGAME_ASSERT(pos != end);
+ if (pos + 1 == end)
+ return *begin;
+ return *(pos + 1);
+}
+
+const Transform* PieceInfo::get_previous_transform(
+ const Transform* transform) const
+{
+ transform = get_equivalent_transform(transform);
+ auto begin = m_transforms.begin();
+ auto end = m_transforms.end();
+ auto pos = find(begin, end, transform);
+ LIBBOARDGAME_ASSERT(pos != end);
+ if (pos == begin)
+ return *(end - 1);
+ 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;
+
+//-----------------------------------------------------------------------------
+
+using ScoreType = float;
+
+//-----------------------------------------------------------------------------
+
+class PieceInfo
+{
+public:
+ /** Maximum number of points in a piece. */
+ static const unsigned max_size = 22;
+
+ /** Maximum number of scored points in a piece.
+ Currently the same as max_size, needed for GembloQ. If Nexos was the
+ game with the largest pieces, some memory could be saved because
+ junction points in Nexos are not scored. */
+ static const unsigned max_scored_size = 22;
+
+ /** Maximum number of instances of a piece per player. */
+ static const unsigned max_instances = 3;
+
+ using Points = ArrayList<CoordPoint, max_size>;
+
+
+ /** Constructor.
+ @param name A short unique name for the piece.
+ @param points The coordinates of the piece elements.
+ @param geo
+ @param transforms
+ @param geometry_type
+ @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,
+ GeometryType geometry_type, 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_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;
+
+ 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;
+
+ string m_name;
+
+ vector<const Transform*> m_transforms;
+
+ map<const Transform*, const Transform*> m_equivalent_transform;
+};
+
+//-----------------------------------------------------------------------------
+
+using PiecePoints = PieceInfo::Points;
+
+//-----------------------------------------------------------------------------
+
+} // 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);
+
+ 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>
+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 */
+//-----------------------------------------------------------------------------
+
+#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;
+
+ 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 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 */
+//-----------------------------------------------------------------------------
+
+#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_rot90refl);
+ m_all.push_back(&m_rot180refl);
+ m_all.push_back(&m_rot270refl);
+ m_all.push_back(&m_refl);
+}
+
+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_BASE_PIECE_TRANSFORMS_CLASSIC_H
+#define LIBPENTOBI_BASE_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 final
+ : 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_BASE_PIECE_TRANSFORMS_CLASSIC_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsGembloQ.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PieceTransformsGembloQ.h"
+
+#include "libboardgame_util/Assert.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+PieceTransformsGembloQ::PieceTransformsGembloQ()
+{
+ 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_rot90refl);
+ m_all.push_back(&m_rot180refl);
+ m_all.push_back(&m_rot270refl);
+ m_all.push_back(&m_refl);
+}
+
+const Transform* PieceTransformsGembloQ::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* PieceTransformsGembloQ::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* PieceTransformsGembloQ::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* PieceTransformsGembloQ::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/PieceTransformsGembloQ.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_PIECE_TRANSFORMS_GEMBLOQ_H
+#define LIBPENTOBI_BASE_PIECE_TRANSFORMS_GEMBLOQ_H
+
+#include "PieceTransforms.h"
+#include "GembloQTransform.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class PieceTransformsGembloQ final
+ : public PieceTransforms
+{
+public:
+ PieceTransformsGembloQ();
+
+ 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:
+ TransfGembloQIdentity m_identity;
+
+ TransfGembloQRot90 m_rot90;
+
+ TransfGembloQRot180 m_rot180;
+
+ TransfGembloQRot270 m_rot270;
+
+ TransfGembloQRefl m_refl;
+
+ TransfGembloQRot90Refl m_rot90refl;
+
+ TransfGembloQRot180Refl m_rot180refl;
+
+ TransfGembloQRot270Refl m_rot270refl;
+};
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_PIECE_TRANSFORMS_GEMBLOQ_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PieceTransformsTrigon.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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_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);
+ m_all.push_back(&m_refl);
+}
+
+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_BASE_PIECE_TRANSFORMS_TRIGON_H
+#define LIBPENTOBI_BASE_PIECE_TRANSFORMS_TRIGON_H
+
+#include "PieceTransforms.h"
+#include "TrigonTransform.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+class PieceTransformsTrigon final
+ : 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;
+
+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_BASE_PIECE_TRANSFORMS_TRIGON_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PlayerBase.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PlayerBase.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+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() = default;
+
+ 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. */
+using Point = libboardgame_base::Point<1564, 56, 28, unsigned short>;
+
+//-----------------------------------------------------------------------------
+
+} // 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 {
+
+//-----------------------------------------------------------------------------
+
+using PointList = libboardgame_util::ArrayList<Point, Point::range_onboard>;
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINT_LIST_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_base/PointState.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_BASE_POINT_STATE_H
+#define LIBPENTOBI_BASE_POINT_STATE_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:
+ using IntType = Color::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==(PointState s) const;
+
+ bool operator!=(PointState s) const;
+
+ bool operator==(Color c) const;
+
+ bool operator!=(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()
+{
+#ifdef 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==(PointState s) const
+{
+ return m_i == s.m_i;
+}
+
+inline bool PointState::operator==(Color c) const
+{
+ return m_i == c.to_int();
+}
+
+inline bool PointState::operator!=(PointState s) const
+{
+ return ! operator==(s);
+}
+
+inline bool PointState::operator!=(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;
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_base
+
+#endif // LIBPENTOBI_BASE_POINT_STATE_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. */
+#ifdef 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 == 5 ? 2356736 : 2628840;
+ static_assert(adj_status_nu_adj == 5 || 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. */
+ using Range = libboardgame_util::Range<const Move>;
+
+
+ /** 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();
+ return {begin, begin + range.size()};
+ }
+
+ 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 + 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)
+ {
+ if (sorted[j] == adjusted[i])
+ {
+ sum += factor * float_j;
+ ++n;
+ }
+ ++float_j;
+ }
+ 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"
+#include "libboardgame_util/ArrayList.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;
+
+ using PlacementList = libboardgame_util::ArrayList<Move, max_pieces>;
+
+
+ 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 */
+//-----------------------------------------------------------------------------
+
+#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;
+ case BoardType::gembloq:
+ add_colored_starting_point(geo, 1, 0, Color(0));
+ add_colored_starting_point(geo, 2, 0, Color(0));
+ add_colored_starting_point(geo, 1, 1, Color(0));
+ add_colored_starting_point(geo, 2, 1, Color(0));
+ add_colored_starting_point(geo, 53, 0, Color(1));
+ add_colored_starting_point(geo, 54, 0, Color(1));
+ add_colored_starting_point(geo, 53, 1, Color(1));
+ add_colored_starting_point(geo, 54, 1, Color(1));
+ add_colored_starting_point(geo, 53, 26, Color(2));
+ add_colored_starting_point(geo, 54, 26, Color(2));
+ add_colored_starting_point(geo, 53, 27, Color(2));
+ add_colored_starting_point(geo, 54, 27, Color(2));
+ add_colored_starting_point(geo, 1, 26, Color(3));
+ add_colored_starting_point(geo, 2, 26, Color(3));
+ add_colored_starting_point(geo, 1, 27, Color(3));
+ add_colored_starting_point(geo, 2, 27, Color(3));
+ break;
+ case BoardType::gembloq_2:
+ add_colored_starting_point(geo, 13, 0, Color(0));
+ add_colored_starting_point(geo, 14, 0, Color(0));
+ add_colored_starting_point(geo, 13, 1, Color(0));
+ add_colored_starting_point(geo, 14, 1, Color(0));
+ add_colored_starting_point(geo, 29, 20, Color(1));
+ add_colored_starting_point(geo, 30, 20, Color(1));
+ add_colored_starting_point(geo, 29, 21, Color(1));
+ add_colored_starting_point(geo, 30, 21, Color(1));
+ break;
+ case BoardType::gembloq_3:
+ add_colored_starting_point(geo, 25, 24, Color(0));
+ add_colored_starting_point(geo, 26, 24, Color(0));
+ add_colored_starting_point(geo, 25, 25, Color(0));
+ add_colored_starting_point(geo, 26, 25, Color(0));
+ add_colored_starting_point(geo, 1, 6, Color(1));
+ add_colored_starting_point(geo, 2, 6, Color(1));
+ add_colored_starting_point(geo, 1, 7, Color(1));
+ add_colored_starting_point(geo, 2, 7, Color(1));
+ add_colored_starting_point(geo, 49, 6, Color(2));
+ add_colored_starting_point(geo, 50, 6, Color(2));
+ add_colored_starting_point(geo, 49, 7, Color(2));
+ add_colored_starting_point(geo, 50, 7, Color(2));
+ 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 */
+//-----------------------------------------------------------------------------
+
+#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 */
+//-----------------------------------------------------------------------------
+
+#include "TreeUtil.h"
+
+#include "NodeUtil.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+const SgfNode* get_move_node(const PentobiTree& tree, const SgfNode& node,
+ unsigned n)
+{
+ auto move_number = get_move_number(tree, node);
+ if (n == move_number)
+ return &node;
+ if (n < move_number)
+ {
+ auto current = &node;
+ do
+ {
+ if (tree.has_move(*current))
+ {
+ if (move_number == n)
+ return current;
+ --move_number;
+ }
+ if (libpentobi_base::has_setup(*current))
+ break;
+ current = current->get_parent_or_null();
+ }
+ while (current != nullptr);
+ }
+ else
+ {
+ auto current = node.get_first_child_or_null();
+ while (current != nullptr)
+ {
+ if (libpentobi_base::has_setup(*current))
+ break;
+ if (tree.has_move(*current))
+ {
+ ++move_number;
+ if (move_number == n)
+ return current;
+ }
+ current = current->get_first_child_or_null();
+ }
+ }
+ return nullptr;
+}
+
+unsigned get_move_number(const PentobiTree& tree, const SgfNode& node)
+{
+ unsigned move_number = 0;
+ auto current = &node;
+ do
+ {
+ if (tree.has_move(*current))
+ ++move_number;
+ if (libpentobi_base::has_setup(*current))
+ break;
+ current = current->get_parent_or_null();
+ }
+ while (current != nullptr);
+ 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 != nullptr)
+ {
+ if (libpentobi_base::has_setup(*current))
+ break;
+ if (tree.has_move(*current))
+ ++moves_left;
+ current = current->get_first_child_or_null();
+ }
+ return moves_left;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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 {
+
+//-----------------------------------------------------------------------------
+
+/** Get the node with a given move number in the variation of a given node. */
+const SgfNode* get_move_node(const PentobiTree& tree, const SgfNode& node,
+ unsigned n);
+
+/** 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);
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#include "TrigonGeometry.h"
+
+#include <map>
+#include <memory>
+
+namespace libpentobi_base {
+
+using namespace std;
+using libboardgame_base::CoordPoint;
+
+//-----------------------------------------------------------------------------
+
+TrigonGeometry::TrigonGeometry(unsigned sz)
+{
+ m_sz = sz;
+ Geometry::init(sz * 4 - 1, sz * 2);
+}
+
+const TrigonGeometry& TrigonGeometry::get(unsigned sz)
+{
+ static map<unsigned, shared_ptr<TrigonGeometry>> s_geometry;
+
+ 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
+{
+ // See Geometry::get_diag_coord() about advantageous ordering of the list
+ 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;
+ return y % 2 != 0 ? 1 : 0;
+ }
+ if (x % 2 != 0)
+ return y % 2 == 0 ? 1 : 0;
+ 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 "Geometry.h"
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+/** 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);
+
+
+ explicit TrigonGeometry(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:
+ unsigned m_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 */
+//-----------------------------------------------------------------------------
+
+#include "TrigonTransform.h"
+
+#include <cmath>
+
+namespace libpentobi_base {
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonIdentity::get_transformed(CoordPoint p) const
+{
+ return p;
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRefl::get_transformed(CoordPoint p) const
+{
+ return {-p.x, p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot60::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::ceil(0.5f * px - 1.5f * py));
+ auto y = static_cast<int>(std::floor(0.5f * px + 0.5f * py));
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot120::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::ceil(-0.5f * px - 1.5f * py));
+ auto y = static_cast<int>(std::ceil(0.5f * px - 0.5f * py));
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot180::get_transformed(CoordPoint p) const
+{
+ return {-p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot240::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::floor(-0.5f * px + 1.5f * py));
+ auto y = static_cast<int>(std::ceil(-0.5f * px - 0.5f * py));
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonRot300::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::floor(0.5f * px + 1.5f * py));
+ auto y = static_cast<int>(std::floor(-0.5f * px + 0.5f * py));
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot60::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::ceil(0.5f * (-px) - 1.5f * py));
+ auto y = static_cast<int>(std::floor(0.5f * (-px) + 0.5f * py));
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot120::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::ceil(-0.5f * (-px) - 1.5f * py));
+ auto y = static_cast<int>(std::ceil(0.5f * (-px) - 0.5f * py));
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot180::get_transformed(CoordPoint p) const
+{
+ return {p.x, -p.y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot240::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::floor(-0.5f * (-px) + 1.5f * py));
+ auto y = static_cast<int>(std::ceil(-0.5f * (-px) - 0.5f * py));
+ return {x, y};
+}
+
+//-----------------------------------------------------------------------------
+
+CoordPoint TransfTrigonReflRot300::get_transformed(CoordPoint p) const
+{
+ auto px = static_cast<float>(p.x);
+ auto py = static_cast<float>(p.y);
+ auto x = static_cast<int>(std::floor(0.5f * (-px) + 1.5f * py));
+ auto y = static_cast<int>(std::floor(-0.5f * (-px) + 0.5f * py));
+ return {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(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot60
+ : public Transform
+{
+public:
+ TransfTrigonRot60() : Transform(1) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot120
+ : public Transform
+{
+public:
+ TransfTrigonRot120() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot180
+ : public Transform
+{
+public:
+ TransfTrigonRot180() : Transform(1) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot240
+ : public Transform
+{
+public:
+ TransfTrigonRot240() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRot300
+ : public Transform
+{
+public:
+ TransfTrigonRot300() : Transform(1) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonRefl
+ : public Transform
+{
+public:
+ TransfTrigonRefl() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot60
+ : public Transform
+{
+public:
+ TransfTrigonReflRot60() : Transform(1) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot120
+ : public Transform
+{
+public:
+ TransfTrigonReflRot120() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot180
+ : public Transform
+{
+public:
+ TransfTrigonReflRot180() : Transform(1) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot240
+ : public Transform
+{
+public:
+ TransfTrigonReflRot240() : Transform(0) {}
+
+ CoordPoint get_transformed(CoordPoint p) const override;
+};
+
+//-----------------------------------------------------------------------------
+
+class TransfTrigonReflRot300
+ : public Transform
+{
+public:
+ TransfTrigonReflRot300() : Transform(1) {}
+
+ CoordPoint get_transformed(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 */
+//-----------------------------------------------------------------------------
+
+#include "Variant.h"
+
+#include "CallistoGeometry.h"
+#include "GembloQGeometry.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:
+ case Variant::callisto_2_4:
+ result = BoardType::callisto;
+ break;
+ case Variant::callisto_2:
+ result = BoardType::callisto_2;
+ break;
+ case Variant::callisto_3:
+ result = BoardType::callisto_3;
+ break;
+ case Variant::gembloq:
+ case Variant::gembloq_2_4:
+ result = BoardType::gembloq;
+ break;
+ case Variant::gembloq_2:
+ result = BoardType::gembloq_2;
+ break;
+ case Variant::gembloq_3:
+ result = BoardType::gembloq_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();
+ 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;
+ case BoardType::gembloq:
+ result = &GembloQGeometry::get(4);
+ break;
+ case BoardType::gembloq_2:
+ result = &GembloQGeometry::get(2);
+ break;
+ case BoardType::gembloq_3:
+ result = &GembloQGeometry::get(3);
+ break;
+ }
+ return *result;
+}
+
+const Geometry& get_geometry(Variant variant)
+{
+ return get_geometry(get_board_type(variant));
+}
+
+GeometryType get_geometry_type(Variant variant)
+{
+ GeometryType result = GeometryType::classic; // Init to avoid compiler warning
+ switch (variant)
+ {
+ case Variant::classic:
+ case Variant::classic_2:
+ case Variant::classic_3:
+ case Variant::duo:
+ case Variant::junior:
+ result = GeometryType::classic;
+ break;
+ case Variant::trigon:
+ case Variant::trigon_2:
+ case Variant::trigon_3:
+ result = GeometryType::trigon;
+ break;
+ case Variant::nexos:
+ case Variant::nexos_2:
+ result = GeometryType::nexos;
+ break;
+ case Variant::callisto:
+ case Variant::callisto_2:
+ case Variant::callisto_2_4:
+ case Variant::callisto_3:
+ result = GeometryType::callisto;
+ break;
+ case Variant::gembloq:
+ case Variant::gembloq_2:
+ case Variant::gembloq_2_4:
+ case Variant::gembloq_3:
+ result = GeometryType::gembloq;
+ break;
+ }
+ return result;
+}
+
+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:
+ case Variant::gembloq_2:
+ result = 2;
+ break;
+ case Variant::trigon_3:
+ case Variant::callisto_3:
+ case Variant::gembloq_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:
+ case Variant::callisto_2_4:
+ case Variant::gembloq:
+ case Variant::gembloq_2_4:
+ 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:
+ case Variant::callisto_2_4:
+ case Variant::gembloq_2:
+ case Variant::gembloq_2_4:
+ result = 2;
+ break;
+ case Variant::classic_3:
+ case Variant::trigon_3:
+ case Variant::callisto_3:
+ case Variant::gembloq_3:
+ result = 3;
+ break;
+ case Variant::classic:
+ case Variant::trigon:
+ case Variant::nexos:
+ case Variant::callisto:
+ case Variant::gembloq:
+ 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_2_4:
+ case Variant::callisto_3:
+ result = PieceSet::callisto;
+ break;
+ case Variant::gembloq:
+ case Variant::gembloq_2:
+ case Variant::gembloq_2_4:
+ case Variant::gembloq_3:
+ result = PieceSet::gembloq;
+ 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::gembloq:
+ case BoardType::gembloq_2:
+ case BoardType::gembloq_3:
+ case BoardType::nexos:
+ break;
+ case BoardType::trigon_3:
+ // Can we use the same as for BoardType::trigon?
+ break;
+ }
+}
+
+bool has_central_symmetry(Variant variant)
+{
+ return variant == Variant::duo || variant == Variant::junior
+ || variant == Variant::trigon_2 || variant == Variant::callisto_2
+ || variant == Variant::gembloq_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 two-player four-color")
+ variant = Variant::callisto_2_4;
+ else if (t == "callisto three-player")
+ variant = Variant::callisto_3;
+ else if (t == "gembloq")
+ variant = Variant::gembloq;
+ else if (t == "gembloq two-player")
+ variant = Variant::gembloq_2;
+ else if (t == "gembloq two-player four-color")
+ variant = Variant::gembloq_2_4;
+ else if (t == "gembloq three-player")
+ variant = Variant::gembloq_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_2_4" || t == "ca24")
+ variant = Variant::callisto_2_4;
+ else if (t == "callisto_3" || t == "ca3")
+ variant = Variant::callisto_3;
+ else if (t == "gembloq" || t == "g")
+ variant = Variant::gembloq;
+ else if (t == "gembloq_2" || t == "g2")
+ variant = Variant::gembloq_2;
+ else if (t == "gembloq_2_4" || t == "g24")
+ variant = Variant::gembloq_2_4;
+ else if (t == "gembloq_3" || t == "g3")
+ variant = Variant::gembloq_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_2_4:
+ result = "Callisto Two-Player Four-Color";
+ break;
+ case Variant::callisto_3:
+ result = "Callisto Three-Player";
+ break;
+ case Variant::gembloq:
+ result = "GembloQ";
+ break;
+ case Variant::gembloq_2:
+ result = "GembloQ Two-Player";
+ break;
+ case Variant::gembloq_2_4:
+ result = "GembloQ Two-Player Four-Color";
+ break;
+ case Variant::gembloq_3:
+ result = "GembloQ 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_2_4:
+ result = "callisto_2_4";
+ break;
+ case Variant::callisto_3:
+ result = "callisto_3";
+ break;
+ case Variant::gembloq:
+ result = "gembloq";
+ break;
+ case Variant::gembloq_2:
+ result = "gembloq_2";
+ break;
+ case Variant::gembloq_2_4:
+ result = "gembloq_2_4";
+ break;
+ case Variant::gembloq_3:
+ result = "gembloq_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,
+
+ gembloq
+};
+
+//-----------------------------------------------------------------------------
+
+enum class BoardType
+{
+ classic,
+
+ duo,
+
+ trigon,
+
+ trigon_3,
+
+ nexos,
+
+ callisto,
+
+ callisto_2,
+
+ callisto_3,
+
+ gembloq,
+
+ gembloq_2,
+
+ gembloq_3
+};
+
+//-----------------------------------------------------------------------------
+
+enum class GeometryType
+{
+ classic,
+
+ trigon,
+
+ nexos,
+
+ callisto,
+
+ gembloq
+};
+
+//-----------------------------------------------------------------------------
+
+/** Game variant. */
+enum class Variant
+{
+ classic,
+
+ classic_2,
+
+ classic_3,
+
+ duo,
+
+ junior,
+
+ trigon,
+
+ trigon_2,
+
+ trigon_3,
+
+ nexos,
+
+ nexos_2,
+
+ callisto,
+
+ callisto_2,
+
+ /** Callisto two-player four-color. */
+ callisto_2_4,
+
+ callisto_3,
+
+ gembloq,
+
+ gembloq_2,
+
+ /** GembloQ two-player four-color. */
+ gembloq_2_4,
+
+ gembloq_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);
+
+GeometryType get_geometry_type(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
+# 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).
+
+find_package(Qt5Gui 5.9 REQUIRED)
+
+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/Reader.cpp
+ ../libboardgame_sgf/SgfError.cpp
+ ../libboardgame_sgf/SgfNode.cpp
+ ../libboardgame_sgf/SgfTree.cpp
+ ../libboardgame_sgf/TreeReader.cpp
+ ../libpentobi_base/CallistoGeometry.cpp
+ ../libpentobi_base/GembloQGeometry.cpp
+ ../libpentobi_base/NexosGeometry.cpp
+ ../libpentobi_base/NodeUtil.cpp
+ ../libpentobi_base/TrigonGeometry.cpp
+ ../libpentobi_base/Variant.cpp
+ ../libpentobi_paint/Paint.cpp
+ ../libpentobi_thumbnail/CreateThumbnail.cpp
+)
+
+target_include_directories(pentobi_kde_thumbnailer PRIVATE ..)
+
+target_compile_definitions(pentobi_kde_thumbnailer PRIVATE
+ QT_DEPRECATED_WARNINGS
+ QT_DISABLE_DEPRECATED_BEFORE=0x051100)
+
+target_link_libraries(pentobi_kde_thumbnailer Qt5::Gui)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/AnalyzeGame.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "AnalyzeGame.h"
+
+#include "Search.h"
+#include "libboardgame_util/Log.h"
+#include "libboardgame_util/WallTimeSource.h"
+
+namespace libpentobi_mcts {
+
+using libboardgame_sgf::SgfError;
+using libboardgame_util::clear_abort;
+using libboardgame_util::get_abort;
+using libboardgame_util::WallTimeSource;
+using libpentobi_base::BoardUpdater;
+
+//-----------------------------------------------------------------------------
+
+void AnalyzeGame::clear()
+{
+ m_moves.clear();
+ m_values.clear();
+}
+
+void AnalyzeGame::run(const Game& game, Search& search, size_t nu_simulations,
+ const 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;
+ do
+ {
+ if (tree.has_move(*node))
+ ++total_moves;
+ node = node->get_first_child_or_null();
+ }
+ while (node != nullptr);
+ WallTimeSource time_source;
+ clear_abort();
+ node = &root;
+ unsigned move_number = 0;
+ auto tie_value = Search::SearchParamConst::tie_value;
+ const auto 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 dummy;
+ do
+ {
+ 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(static_cast<double>(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());
+ search.search(dummy, *bd, mv.color, max_count,
+ min_simulations, max_time, time_source);
+ if (get_abort())
+ break;
+ m_moves.push_back(mv);
+ m_values.push_back(static_cast<double>(
+ search.get_root_val().get_mean()));
+ }
+ catch (const SgfError&)
+ {
+ break;
+ }
+ }
+ ++move_number;
+ }
+ if (! node->has_children())
+ {
+ updater.update(*bd, tree, *node);
+ LIBBOARDGAME_LOG("Analyzing last position");
+ Color c;
+ if (bd->is_game_over() && ! m_moves.empty())
+ // If game is over, analyze last position from viewpoint of
+ // color that played the last move to avoid using a color that
+ // might have run out of moves much earlier.
+ c = m_moves.back().color;
+ else
+ c = bd->get_effective_to_play();
+ search.search(dummy, *bd, c, max_count, min_simulations, max_time,
+ time_source);
+ if (get_abort())
+ break;
+ m_moves.emplace_back(c, Move::null());
+ m_values.push_back(static_cast<double>(
+ search.get_root_val().get_mean()));
+ }
+ node = node->get_first_child_or_null();
+ }
+ while (node != nullptr);
+}
+
+void AnalyzeGame::set(Variant variant, const vector<ColorMove>& moves,
+ const vector<double>& values)
+{
+ LIBBOARDGAME_ASSERT(moves.size() == values.size());
+ m_variant = variant;
+ m_moves = moves;
+ m_values = values;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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:
+ void clear();
+
+ /** 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,
+ const 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;
+
+ void set(Variant variant, const vector<ColorMove>& moves,
+ const vector<double>& values);
+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
+set(LIBPENTOBI_MCTS_FLOAT_TYPE "float" CACHE STRING
+ "Floating-point type for MCTS values")
+
+add_library(pentobi_mcts STATIC
+ AnalyzeGame.h
+ AnalyzeGame.cpp
+ Float.h
+ History.h
+ History.cpp
+ LocalPoints.h
+ LocalPoints.cpp
+ Player.h
+ Player.cpp
+ PlayoutFeatures.h
+ 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
+)
+
+if(NOT LIBPENTOBI_MCTS_FLOAT_TYPE STREQUAL "float")
+ target_compile_definitions(pentobi_mcts PUBLIC
+ LIBPENTOBI_MCTS_FLOAT_TYPE=${LIBPENTOBI_MCTS_FLOAT_TYPE})
+endif()
+
+target_include_directories(pentobi_mcts PUBLIC ..)
+
+target_link_libraries(pentobi_mcts pentobi_base boardgame_mcts)
--- /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
+using Float = LIBPENTOBI_MCTS_FLOAT_TYPE;
+#else
+using 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 */
+//----------------------------------------------------------------------------
+
+#include "History.h"
+
+#include "libpentobi_base/BoardUtil.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+using libpentobi_base::get_current_position_as_setup;
+
+//----------------------------------------------------------------------------
+
+void History::get_as_setup(Variant& variant, Setup& setup) const
+{
+ LIBBOARDGAME_ASSERT(is_valid());
+ variant = m_variant;
+ auto bd = make_unique<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_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/LocalPoints.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "LocalPoints.h"
+
+namespace libpentobi_mcts {
+
+//-----------------------------------------------------------------------------
+
+LocalPoints::LocalPoints()
+{
+ m_is_local.fill_all(false);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/LocalPoints.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_MCTS_LOCAL_POINTS_H
+#define LIBPENTOBI_MCTS_LOCAL_POINTS_H
+
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/Grid.h"
+
+namespace libpentobi_mcts {
+
+using libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::Color;
+using libpentobi_base::GridExt;
+using libpentobi_base::Point;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Find attach points of recent opponent moves on the board. */
+class LocalPoints
+{
+public:
+ LocalPoints();
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+ void init(const Board& bd);
+
+ bool contains(Point p) const { return m_is_local[p]; }
+
+private:
+ GridExt<bool> m_is_local;
+
+ /** Points in m_is_local with value true. */
+ PointList m_points;
+};
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH>
+inline void LocalPoints::init(const Board& bd)
+{
+ for (Point p : m_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] && ! m_is_local[*j])
+ {
+ m_points.get_unchecked(nu_local++) = *j;
+ m_is_local[*j] = true;
+ }
+ while (++j != end);
+ }
+ m_points.resize(nu_local);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_LOCAL_POINTS_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_mcts/Player.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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.
+// * We only calibrate the numbers for the game variants we care most about.
+// For other game variants, we use the numbers of game variants with similar
+// playing strength and speed of simulations.
+
+const float counts_classic[Player::max_supported_level] =
+ { 3, 30, 90, 181, 667, 5028, 69809, 349044, 1745221 };
+
+const float counts_duo[Player::max_supported_level] =
+ { 3, 21, 77, 213, 861, 7280, 221867, 1109339, 5546695 };
+
+const float counts_trigon[Player::max_supported_level] =
+ { 100, 246, 457, 876, 1882, 5506, 19819, 99092, 495465 };
+
+const float counts_nexos[Player::max_supported_level] =
+ { 250, 347, 625, 1223, 3117, 8270, 20954, 104774, 523877 };
+
+const float counts_callisto_2[Player::max_supported_level] =
+ { 30, 87, 300, 1017, 4729, 20435, 122778, 613905, 3069529 };
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+Player::Player(Variant initial_variant, unsigned max_level,
+ const string& books_dir, unsigned nu_threads)
+ : m_is_book_loaded(false),
+ m_use_book(true),
+ m_resign(false),
+ m_books_dir(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;
+ }
+ }
+}
+
+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:
+ case BoardType::gembloq_2:
+ 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:
+ case BoardType::gembloq:
+ case BoardType::gembloq_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:
+ case BoardType::gembloq_2:
+ 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:
+ case BoardType::gembloq:
+ case BoardType::gembloq_3:
+ 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
+ size_t reasonable = available / 4;
+ 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 calibrated and the ratings are also used for other game
+ // variants that we assume have comparable strength (e.g. multi-player 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: // Measured for classic_2
+ {
+ // Anchor 1000, scale 0.6
+ static double elo[Player::max_supported_level] =
+ { 1000, 1142, 1283, 1425, 1567, 1708, 1850, 1951, 2030 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::duo:
+ {
+ // Anchor 1000, scale 0.74
+ static double elo[Player::max_supported_level] =
+ { 1000, 1189, 1378, 1567, 1755, 1945, 2134, 2185, 2209 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::callisto_2:
+ {
+ // Anchor 1000, scale 0.49
+ static double elo[Player::max_supported_level] =
+ { 1000, 1113, 1225, 1338, 1450, 1563, 1675, 1783, 1868 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::trigon: // Measured for trigon_2
+ case BoardType::trigon_3:
+ {
+ // Anchor 1000, scale 0.48
+ static double elo[Player::max_supported_level] =
+ { 1000, 1110, 1220, 1330, 1440, 1550, 1660, 1765, 1897 };
+ result = Rating(elo[level - 1]);
+ }
+ break;
+ case BoardType::nexos: // Measured for nexos_2
+ case BoardType::callisto:
+ case BoardType::callisto_3:
+ case BoardType::gembloq:
+ case BoardType::gembloq_2:
+ case BoardType::gembloq_3:
+ {
+ // Anchor 1000, scale 0.60
+ static double 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 = make_unique<CpuTimeSource>();
+ else
+ m_time_source = make_unique<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, const string& books_dir,
+ unsigned nu_threads = 0);
+
+ 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.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 libpentobi_base::Board;
+using libpentobi_base::BoardConst;
+using libpentobi_base::Color;
+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:
+ /** Integer type used in the implementation.
+ Should be fast and have enough space for the masks used. Note that
+ logically, we want uint_fast32_t, but that is an 8-byte unsigned
+ with GCC on Intel CPUs, which is *slower* than a 4-byte unsigned. */
+ using IntType = uint_least32_t;
+
+ /** 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;
+ static_assert(max_local < 0x01000u, ""); // Value for forbidden status
+ static_assert(PieceInfo::max_size <= 0xff, ""); // Mask for forbidden status
+
+ /** 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 & 0xff000u) != 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;
+
+
+ /** 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, bool IS_CALLISTO>
+ 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;
+
+
+ void add_local(const Board& bd, Point p, Color to_play,
+ unsigned& nu_local);
+};
+
+
+inline void PlayoutFeatures::add_local(const Board& bd, Point p, Color to_play,
+ unsigned& nu_local)
+{
+ if (m_point_value[p] == 0)
+ {
+ m_local_points.get_unchecked(nu_local++) = p;
+ m_point_value[p] =
+ 1 + static_cast<IntType>(bd.is_attach_point(p, to_play));
+ }
+}
+
+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] ? 0x01000u : 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] = 0x01000u;
+ 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] = 0x01000u;
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+inline void PlayoutFeatures::set_local(const Board& bd)
+{
+ // Clear old info about local points
+ for (Point p : m_local_points)
+ m_point_value[p] &= 0xff000u;
+ 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;
+ add_local(bd, *j, to_play, nu_local);
+ if (MAX_SIZE == 7 || IS_CALLISTO)
+ {
+ // Nexos or Callisto don't use adjacent points, use 2nd-order
+ // "diagonal" points instead
+ LIBBOARDGAME_ASSERT(geo.get_adj(*j).empty());
+ for (Point k : geo.get_diag(*j))
+ if (! is_forbidden[k])
+ add_local(bd, k, to_play, nu_local);
+ }
+ else
+ for (Point k : geo.get_adj(*j))
+ if (! is_forbidden[k])
+ add_local(bd, k, to_play, nu_local);
+ }
+ 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 */
+//-----------------------------------------------------------------------------
+
+#include "PriorKnowledge.h"
+
+#include <cmath>
+
+namespace libpentobi_mcts {
+
+using libpentobi_base::BoardType;
+using libpentobi_base::GeometryType;
+using libpentobi_base::Color;
+using libpentobi_base::PointState;
+using libpentobi_base::PieceSet;
+
+//-----------------------------------------------------------------------------
+
+PriorKnowledge::PriorKnowledge(const Board& bd)
+{
+ init_variant(bd);
+}
+
+void PriorKnowledge::init_variant(const Board& bd)
+{
+ auto variant = bd.get_variant();
+ m_variant = variant;
+ auto& geo = bd.get_geometry();
+ auto board_type = bd.get_board_type();
+ auto piece_set = bd.get_piece_set();
+ auto geometry_type = bd.get_geometry_type();
+
+ // Init m_dist_to_center
+ auto width = static_cast<float>(geo.get_width());
+ auto 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)
+ {
+ auto x = static_cast<float>(geo.get_x(p));
+ auto 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 (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:
+ case Variant::callisto_2_4:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 8;
+ m_max_dist_diff = 4;
+ 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 = 6;
+ m_max_dist_diff = 3;
+ 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;
+ case Variant::gembloq:
+ case Variant::gembloq_2_4:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 12;
+ m_max_dist_diff = 0.5f;
+ break;
+ case Variant::gembloq_3:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 9;
+ m_max_dist_diff = 0.5f;
+ break;
+ case Variant::gembloq_2:
+ m_check_dist_to_center.fill(true);
+ m_dist_to_center_max_pieces = 4;
+ m_max_dist_diff = 0.5f;
+ 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;
+ }
+
+ // Init gammas. The values are learned using pentobi/src/learn_tool.
+ Float gamma_piece_score_0;
+ Float gamma_piece_score_1;
+ Float gamma_piece_score_2;
+ Float gamma_piece_score_3;
+ Float gamma_piece_score_4;
+ Float gamma_piece_score_5;
+ Float gamma_piece_score_6;
+ if (variant == Variant::duo || variant == Variant::junior)
+ {
+ Float temperature = 0.84f;
+ // Tuned for duo
+ m_gamma_point_other = exp(0.394f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(1.399f / temperature);
+ m_gamma_point_second_color_attach = 1; // unused
+ m_gamma_adj_connect = 1; // unused
+ m_gamma_adj_occupied_other = exp(0.359f / temperature);
+ m_gamma_adj_forbidden_other = exp(0.404f / temperature);
+ m_gamma_adj_own_attach = exp(-1.164f / temperature);
+ m_gamma_adj_nonforbidden = exp(-0.461f / temperature);
+ m_gamma_attach_to_play = exp(-0.082f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.305f / temperature);
+ m_gamma_attach_nonforbidden[0] = exp(-0.200f / temperature);
+ m_gamma_attach_nonforbidden[1] = exp(-0.116f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(0.331f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(0.588f / temperature);
+ m_gamma_attach_nonforbidden[4] = exp(0.923f / temperature);
+ m_gamma_attach_nonforbidden[5] = 1; // unused
+ m_gamma_attach_nonforbidden[6] = 1; // unused
+ m_gamma_attach_second_color = 1; // unused
+ m_gamma_local = exp(0.336f / temperature);
+ gamma_piece_score_0 = 1; // unused
+ gamma_piece_score_1 = exp(0.330f / temperature);
+ gamma_piece_score_2 = exp(-0.402f / temperature);
+ gamma_piece_score_3 = exp(-0.845f / temperature);
+ gamma_piece_score_4 = exp(-0.245f / temperature);
+ gamma_piece_score_5 = exp(1.149f / temperature);
+ gamma_piece_score_6 = 1; // unused
+ }
+ else if (variant == Variant::callisto_2)
+ {
+ Float temperature = 0.84f;
+ m_gamma_point_other = exp(0.305f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(2.047f / temperature);
+ m_gamma_point_second_color_attach = 1; // unused
+ m_gamma_adj_connect = exp(-0.022f / temperature);
+ m_gamma_adj_occupied_other = exp(-0.049f / temperature);
+ m_gamma_adj_forbidden_other = 1; // unused
+ m_gamma_adj_own_attach = exp(-0.631f / temperature);
+ m_gamma_adj_nonforbidden = exp(0.221f / temperature);
+ m_gamma_attach_to_play = exp(-0.273f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.597f / temperature);
+ m_gamma_attach_nonforbidden[0] = 1; // unused
+ m_gamma_attach_nonforbidden[1] = exp(-0.070f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(0.140f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(0.075f / temperature);
+ m_gamma_attach_nonforbidden[4] = exp(0.199f / temperature);
+ m_gamma_attach_nonforbidden[5] = 1; // unused
+ m_gamma_attach_nonforbidden[6] = 1; // unused
+ m_gamma_attach_second_color = 1; // unused
+ m_gamma_local = exp(0.203f / temperature);
+ gamma_piece_score_0 = exp(0.942f / temperature);
+ gamma_piece_score_1 = 1; // unused
+ gamma_piece_score_2 = exp(-1.642f / temperature);
+ gamma_piece_score_3 = exp(-0.800f / temperature);
+ gamma_piece_score_4 = exp(0.436f / temperature);
+ gamma_piece_score_5 = exp(1.082f / temperature);
+ gamma_piece_score_6 = 1; // unused
+ }
+ else if (variant == Variant::gembloq_2)
+ {
+ Float temperature = 0.84f;
+ m_gamma_point_other = exp(0.120f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(0.315f / temperature);
+ m_gamma_point_second_color_attach = 1; // unused
+ m_gamma_adj_connect = 1; // unused
+ m_gamma_adj_occupied_other = exp(0.350f / temperature);
+ m_gamma_adj_forbidden_other = exp(0.511f / temperature);
+ m_gamma_adj_own_attach = exp(0.285f / temperature);
+ m_gamma_adj_nonforbidden = exp(0.095f / temperature);
+ m_gamma_attach_to_play = exp(0.181f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.127f / temperature);
+ m_gamma_attach_nonforbidden[0] = exp(-0.165f / temperature);
+ m_gamma_attach_nonforbidden[1] = exp(-0.031f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(-0.058f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(-0.057f / temperature);
+ m_gamma_attach_nonforbidden[4] = 1; // unused
+ m_gamma_attach_nonforbidden[5] = 1; // unused
+ m_gamma_attach_nonforbidden[6] = 1; // unused
+ m_gamma_attach_second_color = 1; // unused
+ m_gamma_local = exp(1.109f / temperature);
+ gamma_piece_score_0 = 1; // unused
+ gamma_piece_score_1 = exp(-0.468f / temperature);
+ gamma_piece_score_2 = exp(-0.647f / temperature);
+ gamma_piece_score_3 = exp(-0.386f / temperature);
+ gamma_piece_score_4 = exp(0.250f / temperature);
+ gamma_piece_score_5 = exp(1.283f / temperature);
+ gamma_piece_score_6 = 1; // unused
+ }
+ else if (piece_set == PieceSet::trigon)
+ {
+ Float temperature = 0.84f;
+ // Tuned for trigon_2
+ m_gamma_point_other = exp(0.182f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(0.828f / temperature);
+ m_gamma_point_second_color_attach = exp(0.016f / temperature);
+ m_gamma_adj_connect = exp(1.032f / temperature);
+ m_gamma_adj_occupied_other = exp(1.024f / temperature);
+ m_gamma_adj_forbidden_other = exp(0.671f / temperature);
+ m_gamma_adj_own_attach = exp(0.193f / temperature);
+ m_gamma_adj_nonforbidden = exp(-0.155f / temperature);
+ m_gamma_attach_to_play = exp(-0.153f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.382f / temperature);
+ m_gamma_attach_nonforbidden[0] = exp(-0.220f / temperature);
+ m_gamma_attach_nonforbidden[1] = exp(-0.263f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(-0.155f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(0.059f / temperature);
+ m_gamma_attach_nonforbidden[4] = 1; // unused
+ m_gamma_attach_nonforbidden[5] = 1; // unused
+ m_gamma_attach_nonforbidden[6] = 1; // unused
+ m_gamma_attach_second_color = exp(-0.051f / temperature);
+ m_gamma_local = exp(0.536f / temperature);
+ gamma_piece_score_0 = 1; // unused
+ gamma_piece_score_1 = exp(0.453f / temperature);
+ gamma_piece_score_2 = exp(0.083f / temperature);
+ gamma_piece_score_3 = exp(-0.620f / temperature);
+ gamma_piece_score_4 = exp(-0.687f / temperature);
+ gamma_piece_score_5 = exp(-0.373f / temperature);
+ gamma_piece_score_6 = exp(1.153f / temperature);
+ }
+ else if (piece_set == PieceSet::nexos)
+ {
+ Float temperature = 0.84f;
+ // Tuned for nexos_2
+ m_gamma_point_other = exp(0.601f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(1.525f / temperature);
+ m_gamma_point_second_color_attach = exp(0.112f / temperature);
+ m_gamma_adj_connect = exp(0.026f / temperature);
+ m_gamma_adj_occupied_other = exp(0.002f / temperature);
+ m_gamma_adj_forbidden_other = 1; // unused
+ m_gamma_adj_own_attach = exp(-0.251f / temperature);
+ m_gamma_adj_nonforbidden = exp(0.036f / temperature);
+ m_gamma_attach_to_play = exp(0.021f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.037f / temperature);
+ m_gamma_attach_nonforbidden[0] = 1; // unused
+ m_gamma_attach_nonforbidden[1] = exp(0.074f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(-0.104f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(-0.067f / temperature);
+ m_gamma_attach_nonforbidden[4] = exp(-0.113f / temperature);
+ m_gamma_attach_nonforbidden[5] = exp(-0.035f / temperature);
+ m_gamma_attach_nonforbidden[6] = exp(0.127f / temperature);
+ m_gamma_attach_second_color = exp(0.075f / temperature);
+ m_gamma_local = exp(1.101f / temperature);
+ gamma_piece_score_0 = 1; // unused
+ gamma_piece_score_1 = exp(-0.167f / temperature);
+ gamma_piece_score_2 = exp(-0.387f / temperature);
+ gamma_piece_score_3 = exp(-0.306f / temperature);
+ gamma_piece_score_4 = exp(0.852f / temperature);
+ gamma_piece_score_5 = 1; // unused
+ gamma_piece_score_6 = 1; // unused
+ }
+ else if (geometry_type == GeometryType::callisto)
+ {
+ Float temperature = 0.84f;
+ // Tuned for callisto_2_4
+ m_gamma_point_other = exp(0.310f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(2.043f / temperature);
+ m_gamma_point_second_color_attach = exp(-0.017f / temperature);
+ m_gamma_adj_connect = exp(0.189f / temperature);
+ m_gamma_adj_occupied_other = exp(-0.033f / temperature);
+ m_gamma_adj_forbidden_other = 1; // unused
+ m_gamma_adj_own_attach = exp(-0.500f / temperature);
+ m_gamma_adj_nonforbidden = exp(0.100f / temperature);
+ m_gamma_attach_to_play = exp(-0.239f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.545f / temperature);
+ m_gamma_attach_nonforbidden[0] = 1; // unused
+ m_gamma_attach_nonforbidden[1] = exp(0.152f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(0.159f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(0.104f / temperature);
+ m_gamma_attach_nonforbidden[4] = exp(0.122f / temperature);
+ m_gamma_attach_nonforbidden[5] = 1; // unused
+ m_gamma_attach_nonforbidden[6] = 1; // unused
+ m_gamma_attach_second_color = exp(-0.107f / temperature);
+ m_gamma_local = exp(0.182f / temperature);
+ gamma_piece_score_0 = exp(0.823f / temperature);
+ gamma_piece_score_1 = 1; // unused
+ gamma_piece_score_2 = exp(-1.507f / temperature);
+ gamma_piece_score_3 = exp(-0.726f / temperature);
+ gamma_piece_score_4 = exp(0.436f / temperature);
+ gamma_piece_score_5 = exp(1.003f / temperature);
+ gamma_piece_score_6 = 1; // unused
+ }
+ else if (piece_set == PieceSet::gembloq)
+ {
+ Float temperature = 0.84f;
+ // Tuned for gembloq_2_4
+ m_gamma_point_other = exp(0.174f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(0.304f / temperature);
+ m_gamma_point_second_color_attach = exp(0.098f / temperature);
+ m_gamma_adj_connect = exp(0.296f / temperature);
+ m_gamma_adj_occupied_other = exp(0.314f / temperature);
+ m_gamma_adj_forbidden_other = exp(0.141f / temperature);
+ m_gamma_adj_own_attach = exp(0.138f / temperature);
+ m_gamma_adj_nonforbidden = exp(-0.195f / temperature);
+ m_gamma_attach_to_play = exp(0.191f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.129f / temperature);
+ m_gamma_attach_nonforbidden[0] = exp(-0.123f / temperature);
+ m_gamma_attach_nonforbidden[1] = exp(0.104f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(-0.005f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(0.045f / temperature);
+ m_gamma_attach_nonforbidden[4] = 1; // unused
+ m_gamma_attach_nonforbidden[5] = 1; // unused
+ m_gamma_attach_nonforbidden[6] = 1; // unused
+ m_gamma_attach_second_color = exp(0.128f / temperature);
+ m_gamma_local = exp(1.078f / temperature);
+ gamma_piece_score_0 = 1; // unused
+ gamma_piece_score_1 = exp(-0.094f / temperature);
+ gamma_piece_score_2 = exp(-0.563f / temperature);
+ gamma_piece_score_3 = exp(-0.661f / temperature);
+ gamma_piece_score_4 = exp(-0.021f / temperature);
+ gamma_piece_score_5 = exp(1.349f / temperature);
+ gamma_piece_score_6 = 1; // unused
+ }
+ else
+ {
+ // Tuned for classic_2
+ Float temperature = 0.84f;
+ m_gamma_point_other = exp(0.137f / temperature);
+ m_gamma_point_opp_attach_or_nb = exp(0.898f / temperature);
+ m_gamma_point_second_color_attach = exp(-0.248f / temperature);
+ m_gamma_adj_connect = exp(0.616f / temperature);
+ m_gamma_adj_occupied_other = exp(0.568f / temperature);
+ m_gamma_adj_forbidden_other = exp(0.544f / temperature);
+ m_gamma_adj_own_attach = exp(-0.849f / temperature);
+ m_gamma_adj_nonforbidden = exp(-0.115f / temperature);
+ m_gamma_attach_to_play = exp(0.007f / temperature);
+ m_gamma_attach_forbidden_other = exp(-0.439f / temperature);
+ m_gamma_attach_nonforbidden[0] = exp(-0.177f / temperature);
+ m_gamma_attach_nonforbidden[1] = exp(-0.002f / temperature);
+ m_gamma_attach_nonforbidden[2] = exp(0.232f / temperature);
+ m_gamma_attach_nonforbidden[3] = exp(0.342f / temperature);
+ m_gamma_attach_nonforbidden[4] = exp(0.694f / temperature);
+ m_gamma_attach_nonforbidden[5] = 1; // unused
+ m_gamma_attach_nonforbidden[6] = 1; // unused
+ m_gamma_attach_second_color = exp(-0.011f / temperature);
+ m_gamma_local = exp(0.610f / temperature);
+ gamma_piece_score_0 = 1; // unused
+ gamma_piece_score_1 = exp(0.476f / temperature);
+ gamma_piece_score_2 = exp(-0.316f / temperature);
+ gamma_piece_score_3 = exp(-0.842f / temperature);
+ gamma_piece_score_4 = exp(-0.301f / temperature);
+ gamma_piece_score_5 = exp(0.969f / temperature);
+ gamma_piece_score_6 = 1; // unused
+ }
+ for (Piece::IntType i = 0; i < bd.get_nu_uniq_pieces(); ++i)
+ switch (static_cast<unsigned>(
+ bd.get_piece_info(Piece(i)).get_score_points()))
+ {
+ case 0: m_gamma_piece_score[Piece(i)] = gamma_piece_score_0; break;
+ case 1: m_gamma_piece_score[Piece(i)] = gamma_piece_score_1; break;
+ case 2: m_gamma_piece_score[Piece(i)] = gamma_piece_score_2; break;
+ case 3: m_gamma_piece_score[Piece(i)] = gamma_piece_score_3; break;
+ case 4: m_gamma_piece_score[Piece(i)] = gamma_piece_score_4; break;
+ case 5: m_gamma_piece_score[Piece(i)] = gamma_piece_score_5; break;
+ default: m_gamma_piece_score[Piece(i)] = gamma_piece_score_6; break;
+ }
+}
+
+void PriorKnowledge::start_search(const Board& bd)
+{
+ if (bd.get_variant() != m_variant)
+ init_variant(bd);
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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 "LocalPoints.h"
+#include "SearchParamConst.h"
+#include "libboardgame_mcts/Tree.h"
+#include "libpentobi_base/Board.h"
+
+namespace libpentobi_mcts {
+
+using namespace std;
+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::Piece;
+using libpentobi_base::PieceMap;
+using libpentobi_base::Point;
+using libpentobi_base::PointList;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Initializes newly created nodes with move prior, count and value.
+ Computes move priors of the form exp(phi*x) with a weight vector phi and a
+ feature vector x. These weights can be learned with softmax training from
+ existing games (see pentobi/src/learn_tool).
+
+ The move generation also prunes certain moves in some game variants (e.g.
+ opening moves that don't go towards the center). */
+class PriorKnowledge
+{
+public:
+ using Node =
+ libboardgame_mcts::Node<Move, Float, SearchParamConst::multithread>;
+
+ using Tree = libboardgame_mcts::Tree<Node>;
+
+
+ explicit PriorKnowledge(const Board& bd);
+
+ 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 IS_CALLISTO>
+ bool gen_children(const Board& bd, const MoveList& moves,
+ bool is_symmetry_broken, Tree::NodeExpander& expander,
+ Float root_val);
+
+private:
+ struct MoveFeatures
+ {
+ /** Heuristic unnormalized probability of the move. */
+ Float gamma;
+
+ /** 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;
+
+ /** @name Gammas for move scores. */
+ /** @{ */
+
+ Float m_gamma_point_other;
+
+ /** Point is opponent attach point or adjacent to it. */
+ Float m_gamma_point_opp_attach_or_nb;
+
+ /** Point is attach point of second color. */
+ Float m_gamma_point_second_color_attach;
+
+ /** Adjacent point connects two own colors. */
+ Float m_gamma_adj_connect;
+
+ /** Adjacent point is occupied by opponent. */
+ Float m_gamma_adj_occupied_other;
+
+ Float m_gamma_adj_forbidden_other;
+
+ /** Adjacent point is own attach point. */
+ Float m_gamma_adj_own_attach;
+
+ /** Adjacent point is not already forbidden. */
+ Float m_gamma_adj_nonforbidden;
+
+ Float m_gamma_attach_to_play;
+
+ Float m_gamma_attach_forbidden_other;
+
+ /** Attach point is attach point of another own color. */
+ Float m_gamma_attach_second_color;
+
+ /** Move occupies an attach point of a recent opponent move. */
+ Float m_gamma_local;
+
+ PieceMap<Float> m_gamma_piece_score;
+
+ /** Attach point is nonforbidden and has n non-forbidden neighbors.
+ Nexos/Callisto use "diagonal" neighbors instead of "adjacent", so the
+ index is [0..6] */
+ array<Float, 7> m_gamma_attach_nonforbidden;
+
+ /** @} */ // @name
+
+ /** Maximum of Features::gamma for all moves. */
+ Float m_max_gamma;
+
+ /** Sum of Features::gamma for all moves. */
+ Float m_sum_gamma;
+
+ bool m_has_connect_move;
+
+ ColorMap<bool> m_check_dist_to_center;
+
+ Variant m_variant;
+
+ unsigned m_dist_to_center_max_pieces;
+
+ float m_min_dist_to_center;
+
+ float m_max_dist_diff;
+
+ LocalPoints m_local_points;
+
+ /** Distance to center heuristic. */
+ GridExt<float> m_dist_to_center;
+
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+ void compute_features(const Board& bd, const MoveList& moves,
+ bool check_dist_to_center, bool check_connect);
+
+ void init_variant(const Board& bd);
+};
+
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+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 feature without
+ // needing an extra check below because adj_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> gamma_point;
+ gamma_point[Point::null()] = 1;
+ Grid<Float> gamma_attach;
+ Grid<Float> gamma_adj;
+ for (Point p : geo)
+ {
+ auto s = bd.get_point_state(p);
+ if (is_forbidden[p])
+ {
+ // No need to initialize gamma_point[p] for forbidden points
+ if (s != to_play)
+ gamma_attach[p] = m_gamma_attach_forbidden_other;
+ else
+ gamma_attach[p] = m_gamma_attach_to_play;
+ if (s == connect_color)
+ gamma_adj[p] = m_gamma_adj_connect;
+ else if (! s.is_empty())
+ // Occupied by opponent (no need to check if s == to_play,
+ // such moves are illegal)
+ gamma_adj[p] = m_gamma_adj_occupied_other;
+ else
+ gamma_adj[p] = m_gamma_adj_forbidden_other;
+ }
+ else
+ {
+ gamma_point[p] = m_gamma_point_other;
+ if (bd.is_attach_point(p, to_play))
+ gamma_adj[p] = m_gamma_adj_own_attach;
+ else
+ gamma_adj[p] = m_gamma_adj_nonforbidden;
+ unsigned n = 0;
+ if (MAX_SIZE == 7 || IS_CALLISTO)
+ {
+ // Nexos and Callisto don't use "adjacent" points, use
+ // "diagonal" instead
+ LIBBOARDGAME_ASSERT(geo.get_adj(p).empty());
+ for (auto pa : geo.get_diag(p))
+ n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+ }
+ else
+ for (auto pa : geo.get_adj(p))
+ n += 1u - static_cast<unsigned>(is_forbidden[pa]);
+ LIBBOARDGAME_ASSERT(n < m_gamma_attach_nonforbidden.size());
+ gamma_attach[p] = m_gamma_attach_nonforbidden[n];
+ }
+ }
+ 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])
+ {
+ gamma_point[p] = m_gamma_point_opp_attach_or_nb;
+ if (MAX_SIZE == 7 || IS_CALLISTO)
+ // Nexos or Callisto
+ LIBBOARDGAME_ASSERT(geo.get_adj(p).empty());
+ else
+ for (Point j : geo.get_adj(p))
+ if (! is_forbidden[j])
+ gamma_point[j] = m_gamma_point_opp_attach_or_nb;
+ }
+ }
+ if (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])
+ {
+ gamma_point[p] *= m_gamma_point_second_color_attach;
+ if (! is_forbidden[p])
+ gamma_attach[p] *= m_gamma_attach_second_color;
+ }
+ }
+ m_max_gamma = -numeric_limits<Float>::max();
+ m_sum_gamma = 0;
+ 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_ext = BoardConst::get_move_info_ext<MAX_ADJ_ATTACH>(
+ mv, move_info_ext_array);
+ auto& features = m_features[i];
+ auto& info = BoardConst::get_move_info<MAX_SIZE>(mv, move_info_array);
+ auto j = info.begin();
+ Float gamma = gamma_point[*j];
+ bool local = m_local_points.contains(*j);
+ if (! check_dist_to_center)
+ for (unsigned k = 1; k < MAX_SIZE; ++k)
+ {
+ ++j;
+ gamma *= gamma_point[*j];
+ local |= m_local_points.contains(*j);
+ }
+ else
+ {
+ features.dist_to_center = m_dist_to_center[*j];
+ for (unsigned k = 1; k < MAX_SIZE; ++k)
+ {
+ ++j;
+ gamma *= gamma_point[*j];
+ local |= m_local_points.contains(*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);
+ }
+ if (local)
+ gamma *= m_gamma_local;
+ j = info_ext.begin_attach();
+ auto end = info_ext.end_attach();
+ gamma *= gamma_attach[*j];
+ while (++j != end)
+ gamma *= gamma_attach[*j];
+ if (MAX_SIZE == 7 || IS_CALLISTO)
+ {
+ // Nexos and Callisto don't use "adjacent" points, only "diagonal"
+ // Use the features of gamma_adj also for the attach points
+ LIBBOARDGAME_ASSERT(info_ext.size_adj_points == 0);
+ LIBBOARDGAME_ASSERT(! check_connect);
+ j = info_ext.begin_attach();
+ end = info_ext.end_attach();
+ for ( ; j != end; ++j)
+ {
+ gamma *= gamma_attach[*j];
+ gamma *= gamma_adj[*j];
+ }
+ }
+ else
+ {
+ j = info_ext.begin_adj();
+ end = info_ext.end_adj();
+ if (! check_connect)
+ {
+ for ( ; j != end; ++j)
+ gamma *= gamma_adj[*j];
+ }
+ else
+ {
+ features.connect = (bd.get_point_state(*j) == second_color);
+ for ( ; j != end; ++j)
+ {
+ gamma *= gamma_adj[*j];
+ if (bd.get_point_state(*j) == second_color)
+ features.connect = true;
+ }
+ if (features.connect)
+ m_has_connect_move = true;
+ }
+ }
+ gamma *= m_gamma_piece_score[info.get_piece()];
+ m_sum_gamma += gamma;
+ if (gamma > m_max_gamma)
+ m_max_gamma = gamma;
+ features.gamma = gamma;
+ }
+}
+
+template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+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 in-tree phase of the search assumes alternating
+ // moves, because the color of a move is not stored in the nodes and
+ // it wouldn't know who is to play otherwise without generating moves.
+ if (! expander.check_capacity(1))
+ return false;
+ expander.add_child(Move::null(), root_val,
+ SearchParamConst::child_min_count, 1);
+ return true;
+ }
+ m_local_points.init<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, IS_CALLISTO>(
+ bd, moves, check_dist_to_center, check_connect);
+ if (! m_has_connect_move)
+ check_connect = false;
+ 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)
+ {
+ // If a symmetric draw is still possible, encourage exploring
+ // the move that keeps the symmetry
+ ColorMove last = bd.get_move(nu_moves - 1);
+ Move symmetric_mv =
+ bd.get_move_info_ext_2(last.move).symmetric_move;
+ for (unsigned i = 0; i < moves.size(); ++i)
+ if (moves[i] == symmetric_mv)
+ {
+ m_sum_gamma -= m_features[i].gamma;
+ m_features[i].gamma *= 100.f;
+ m_sum_gamma += m_features[i].gamma;
+ if (m_features[i].gamma > m_max_gamma)
+ m_max_gamma = m_features[i].gamma;
+ break;
+ }
+ }
+ }
+ 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;
+ auto inv_max_gamma = 1.f / m_max_gamma;
+ auto inv_sum_gamma = 1.f / m_sum_gamma;
+
+ 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 ((check_dist_to_center
+ && features.dist_to_center > m_min_dist_to_center)
+ || (check_connect && ! features.connect))
+ continue;
+ auto mv = moves[i];
+ // If a symmetric draw is still possible, consider only moves that
+ // break the symmetry
+ if (has_symmetry_breaker
+ && ! bd.get_move_info_ext_2(mv).breaks_symmetry)
+ continue;
+ Float move_prior = features.gamma * inv_sum_gamma;
+ // Empirical good formula for value initialization
+ Float value = root_val * sqrt(features.gamma * inv_max_gamma);
+ LIBBOARDGAME_ASSERT(bd.is_legal(to_play, mv));
+ expander.add_child(mv, value, SearchParamConst::child_min_count,
+ move_prior);
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#include "Search.h"
+
+#include "Util.h"
+
+namespace libpentobi_mcts {
+
+//-----------------------------------------------------------------------------
+
+Search::Search(Variant initial_variant, unsigned nu_threads, size_t memory)
+ : SearchBase(nu_threads == 0 ? get_nu_threads() : nu_threads, memory),
+ m_variant(initial_variant),
+ m_shared_const(m_to_play)
+{
+ set_default_param(m_variant);
+ create_threads();
+}
+
+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 make_unique<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 (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_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:
+ case Variant::gembloq:
+ case Variant::gembloq_2_4:
+ case Variant::gembloq_3:
+ // Tuned for classic_2
+ set_exploration_constant(4.5f);
+ set_rave_parent_max(50000);
+ break;
+ case Variant::duo:
+ case Variant::junior:
+ case Variant::gembloq_2:
+ // Tuned for duo
+ set_exploration_constant(4.0f);
+ set_rave_parent_max(25000);
+ break;
+ case Variant::trigon:
+ case Variant::trigon_2:
+ case Variant::trigon_3:
+ case Variant::callisto:
+ case Variant::callisto_2_4:
+ case Variant::callisto_3:
+ // Tuned for trigon_2
+ set_exploration_constant(6.0f);
+ set_rave_parent_max(50000);
+ break;
+ case Variant::nexos:
+ case Variant::nexos_2:
+ // Tuned for nexos_2
+ set_exploration_constant(3.7f);
+ set_rave_parent_max(50000);
+ break;
+ case Variant::callisto_2:
+ set_exploration_constant(4.0f);
+ 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 one additional pseudo-player for each real player that shares the
+ game result with the main color of the real player.
+ The maximum number of players is 6, which occurs in Classic 3 with 3
+ real players and 3 pseudo-players.
+
+ Some user-changeable parameters that have different optimal values for
+ different game variants are automatically changed whenever the game variant
+ changes.
+ @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);
+
+
+ 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);
+
+ /** @} */ // @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:
+ /** 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_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());
+ return to_play;
+}
+
+inline Color Search::get_to_play() const
+{
+ return m_to_play;
+}
+
+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
+{
+ using Float = libpentobi_mcts::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;
+
+#ifdef 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 max_move_prior = 1;
+
+ static constexpr Float tie_value = 0.5f;
+
+ static constexpr Float prune_count_start = 16;
+
+ static constexpr Float expansion_threshold = 1;
+
+ static constexpr Float expansion_threshold_inc = 0.5f;
+
+ 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 */
+//-----------------------------------------------------------------------------
+
+#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;
+}
+
+/** Check if a point is a useless move for the 1-piece in Callisto.
+ @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 is_useless_one_piece_point(const Board& bd, Point p)
+{
+ for (Point pp: bd.get_geometry().get_diag(p))
+ if (bd.get_point_state(pp).is_empty())
+ return false;
+ return true;
+}
+
+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::gembloq_2:
+ if (nu_moves < nu_colors)
+ {
+ is_piece_considered.fill(false);
+ set_piece_considered(bc, "I5", is_piece_considered);
+ }
+ else 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::gembloq:
+ if (nu_moves < nu_colors)
+ {
+ is_piece_considered.fill(false);
+ set_piece_considered(bc, "I5", is_piece_considered);
+ }
+ else if (nu_moves < 2 * nu_colors)
+ {
+ is_piece_considered.fill(false);
+ set_piece_considered(bc, "I5", is_piece_considered);
+ set_piece_considered(bc, "I4", is_piece_considered);
+ set_piece_considered(bc, "L5", is_piece_considered);
+ set_piece_considered(bc, "N5", is_piece_considered);
+ set_piece_considered(bc, "Y", is_piece_considered);
+ }
+ else if (nu_moves < 3 * nu_colors)
+ filter_min_size(bc, 5, is_piece_considered);
+ 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::gembloq_3:
+ if (nu_moves < nu_colors)
+ {
+ is_piece_considered.fill(false);
+ set_piece_considered(bc, "I5", is_piece_considered);
+ set_piece_considered(bc, "L5", is_piece_considered);
+ }
+ else 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, 5, is_piece_considered);
+ 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::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
+ PointList points;
+ unsigned n = 0;
+ for (Point p : bd)
+ if (bd.get_point_state(p).is_empty() && bc.has_adj_status_points(p))
+ points.get_unchecked(n++) = p;
+ points.resize(n);
+ 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();
+
+ // 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));
+
+ for (Point p : points)
+ if (! bd.is_forbidden(p, c))
+ {
+ auto adj_status = bd.get_adj_status(p, c);
+ for (Piece piece : pieces)
+ {
+ 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);
+ }
+ }
+ if (! is_followup)
+ for (Point p : points)
+ 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 : points)
+ {
+ 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(bd, 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(bd, 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.is_callisto();
+ for (auto i = bd.get_nu_onboard_pieces(); i < Board::max_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);
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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_moves> is_piece_considered;
+
+ /** List of unique values for is_piece_considered. */
+ ArrayList<PieceMap<bool>, Board::max_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::range_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();
+};
+
+//-----------------------------------------------------------------------------
+
+} // 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 */
+//-----------------------------------------------------------------------------
+
+#include "State.h"
+
+#include "libboardgame_util/MathUtil.h"
+#include "libpentobi_base/ScoreUtil.h"
+#ifdef 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 {
+
+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),
+ m_prior_knowledge(m_bd)
+{
+}
+
+template<unsigned MAX_SIZE>
+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>(
+ mv, get_move_info<MAX_SIZE>(mv), gamma_piece, moves,
+ nu_moves, playout_features, total_gamma))
+ marker.set(mv);
+ }
+}
+
+void State::add_callisto_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;
+ bool is_gembloq = (m_bd.get_piece_set() == PieceSet::gembloq);
+ for (Piece piece : pieces)
+ for (Move mv : get_moves(c, piece, p, 0))
+ {
+ // In GembloQ, not all moves covering one starting point
+ // (=quarter-square tringle) are legal.
+ if (is_gembloq && ! m_bd.is_legal(c, mv))
+ continue;
+ if (check_forbidden<MAX_SIZE>(is_forbidden, mv, moves, nu_moves))
+ {
+ LIBBOARDGAME_ASSERT(! marker[mv]);
+ 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 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)
+{
+ 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;
+ gamma *= m_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>
+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>(
+ mv, info, m_gamma_piece[info.get_piece()], moves, nu_moves,
+ playout_features, total_gamma);
+}
+
+#ifdef LIBBOARDGAME_DEBUG
+string State::dump() const
+{
+ ostringstream s;
+ s << "pentobi_mcts::State:\n" << libpentobi_base::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)
+ {
+ 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;
+ res += get_quality_bonus(Color(0), res, s)
+ + get_quality_bonus_attach_multicolor();
+ 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 (m_bd.get_variant() == Variant::classic_3)
+ {
+ result[3] = result[0];
+ result[4] = result[1];
+ result[5] = result[2];
+ }
+}
+
+/** Evaluation function for game variants with 2 colors. */
+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)
+ {
+ 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;
+ res += get_quality_bonus(Color(0), res, s);
+ if (m_is_callisto)
+ res += get_quality_bonus_attach_twocolor();
+ 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;
+ }
+ auto px = static_cast<float>(geo.get_x(p));
+ auto 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())
+ {
+ auto ppx = static_cast<float>(geo.get_x(pp));
+ auto 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)
+ {
+ if (! m_is_callisto)
+ {
+ init_moves_without_gamma<5, false>(to_play);
+ return m_prior_knowledge.gen_children<5, 16, false>(
+ m_bd, m_moves[to_play], m_is_symmetry_broken,
+ expander, root_val);
+ }
+ init_moves_without_gamma<5, true>(to_play);
+ return m_prior_knowledge.gen_children<5, 16, true>(
+ m_bd, m_moves[to_play], m_is_symmetry_broken,
+ expander, root_val);
+ }
+ if (m_max_piece_size == 6)
+ {
+ init_moves_without_gamma<6, false>(to_play);
+ return m_prior_knowledge.gen_children<6, 22, false>(
+ m_bd, m_moves[to_play], m_is_symmetry_broken, expander,
+ root_val);
+ }
+ if (m_max_piece_size == 7)
+ {
+ init_moves_without_gamma<7, false>(to_play);
+ return m_prior_knowledge.gen_children<7, 12, false>(
+ m_bd, m_moves[to_play], m_is_symmetry_broken, expander,
+ root_val);
+ }
+ LIBBOARDGAME_ASSERT(m_max_piece_size == 22);
+ init_moves_without_gamma<22, false>(to_play);
+ return m_prior_knowledge.gen_children<22, 44, false>(
+ 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 if (m_max_piece_size == 7)
+ init_moves_with_gamma<7, 12, false>(to_play);
+ else
+ init_moves_with_gamma<22, 44, 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 if (m_max_piece_size == 7)
+ update_moves<7, 12, false>(to_play);
+ else
+ update_moves<22, 44, 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)])
+ {
+ 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];
+ 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. */
+template<bool IS_CALLISTO>
+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
+ && ! 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
+ auto 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 = static_cast<int>(m_bd.get_attach_points(Color(0)).size())
+ - static_cast<int>(m_bd.get_attach_points(Color(1)).size());
+ for (Point p : m_bd.get_attach_points(Color(0)))
+ n -= static_cast<int>(m_bd.is_forbidden(p, Color(0)));
+ for (Point p : m_bd.get_attach_points(Color(1)))
+ n += static_cast<int>(m_bd.is_forbidden(p, Color(1)));
+ auto 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 = static_cast<int>(m_bd.get_attach_points(Color(0)).size())
+ + static_cast<int>(m_bd.get_attach_points(Color(2)).size())
+ - static_cast<int>(m_bd.get_attach_points(Color(1)).size())
+ - static_cast<int>(m_bd.get_attach_points(Color(3)).size());
+ for (Point p : m_bd.get_attach_points(Color(0)))
+ n -= static_cast<int>(m_bd.is_forbidden(p, Color(0)));
+ for (Point p : m_bd.get_attach_points(Color(2)))
+ n -= static_cast<int>(m_bd.is_forbidden(p, Color(2)));
+ for (Point p : m_bd.get_attach_points(Color(1)))
+ n += static_cast<int>(m_bd.is_forbidden(p, Color(1)));
+ for (Point p : m_bd.get_attach_points(Color(3)))
+ n += static_cast<int>(m_bd.is_forbidden(p, Color(3)));
+ auto 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;
+}
+
+void State::init_gamma()
+{
+ auto& bd = *m_shared_const.board;
+ const auto piece_set = bd.get_piece_set();
+ if (piece_set == PieceSet::gembloq)
+ {
+ static_assert(PlayoutFeatures::max_local + 1 >= 20, "");
+ m_gamma_local[0] = 1;
+ m_gamma_local[1] = 1e6f;
+ m_gamma_local[2] = 1e6f;
+ m_gamma_local[3] = 1e6f;
+ m_gamma_local[4] = 1e6f;
+ m_gamma_local[5] = 1e6f;
+ m_gamma_local[6] = 1e6f;
+ m_gamma_local[7] = 1e6f;
+ m_gamma_local[8] = 1e12f;
+ m_gamma_local[9] = 1e12f;
+ m_gamma_local[10] = 1e12f;
+ m_gamma_local[11] = 1e12f;
+ m_gamma_local[12] = 1e18f;
+ m_gamma_local[13] = 1e18f;
+ m_gamma_local[14] = 1e18f;
+ m_gamma_local[15] = 1e18f;
+ m_gamma_local[16] = 1e24f;
+ m_gamma_local[17] = 1e24f;
+ m_gamma_local[18] = 1e24f;
+ m_gamma_local[19] = 1e24f;
+ for (unsigned i = 20; i < PlayoutFeatures::max_local + 1; ++i)
+ m_gamma_local[i] = 1e25f;
+ }
+ else if (piece_set == PieceSet::trigon)
+ {
+ static_assert(PlayoutFeatures::max_local + 1 >= 5, "");
+ m_gamma_local[0] = 1;
+ m_gamma_local[1] = 1e6f;
+ m_gamma_local[2] = 1e12f;
+ m_gamma_local[3] = 1e18f;
+ m_gamma_local[4] = 1e24f;
+ for (unsigned i = 5; i < PlayoutFeatures::max_local + 1; ++i)
+ m_gamma_local[i] = 1e30f;
+ }
+ else if (piece_set == PieceSet::nexos)
+ {
+ static_assert(PlayoutFeatures::max_local + 1 >= 4, "");
+ m_gamma_local[0] = 1;
+ m_gamma_local[1] = 1e6f;
+ m_gamma_local[2] = 1e12f;
+ m_gamma_local[3] = 1e18f;
+ for (unsigned i = 4; i < PlayoutFeatures::max_local + 1; ++i)
+ m_gamma_local[i] = 1e24f;
+ }
+ else
+ {
+ static_assert(PlayoutFeatures::max_local + 1 >= 5, "");
+ m_gamma_local[0] = 1;
+ m_gamma_local[1] = 1e6f;
+ m_gamma_local[2] = 1e12f;
+ m_gamma_local[3] = 1e18f;
+ m_gamma_local[4] = 1e24f;
+ for (unsigned i = 5; i < PlayoutFeatures::max_local + 1; ++i)
+ m_gamma_local[i] = 1e25f;
+ }
+ 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;
+ case BoardType::gembloq_2:
+ case BoardType::gembloq: // Not tuned
+ case BoardType::gembloq_3: // Not tuned
+ gamma_size_factor = 1.5f;
+ 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);
+ }
+ if (m_is_callisto)
+ // Playing 1-piece in Callisto early in playouts is bad, make sure it
+ // gets a low gamma even if it is on a local point.
+ m_gamma_piece[m_bd.get_one_piece()] = 1e-13f;
+}
+
+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, IS_CALLISTO>(m_bd);
+ auto& marker = m_marker[c];
+ auto& moves = m_moves[c];
+ marker.clear(moves);
+ auto& pieces = get_pieces_considered<IS_CALLISTO>(c);
+ if (m_bd.is_first_piece(c) && ! IS_CALLISTO)
+ add_starting_moves<MAX_SIZE>(c, pieces, true, moves);
+ else
+ {
+ unsigned nu_moves = 0;
+ float total_gamma = 0;
+ if (IS_CALLISTO)
+ add_callisto_one_piece_moves(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>(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, bool IS_CALLISTO>
+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<IS_CALLISTO>(c);
+ auto& is_forbidden = m_bd.is_forbidden(c);
+ if (m_bd.is_first_piece(c) && ! IS_CALLISTO)
+ add_starting_moves<MAX_SIZE>(c, pieces, false, moves);
+ else
+ {
+ unsigned nu_moves = 0;
+ if (IS_CALLISTO)
+ {
+ float total_gamma_dummy;
+ add_callisto_one_piece_moves(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, IS_CALLISTO>(c);
+ }
+}
+
+void State::play_expanded_child(Move mv)
+{
+ 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;
+ }
+}
+
+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.is_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
+ || variant == Variant::gembloq_2);
+ 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();
+}
+
+void State::start_simulation(size_t n)
+{
+ LIBBOARDGAME_UNUSED(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, IS_CALLISTO>(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>(
+ 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>(
+ 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<IS_CALLISTO>(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>(*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>(
+ 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/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 probability
+ that a move is played in the playout phase. */
+class State
+{
+public:
+ using Node =
+ libboardgame_mcts::Node<Move, Float, SearchParamConst::multithread>;
+
+ using Tree = libboardgame_mcts::Tree<Node>;
+
+ using LastGoodReply =
+ libboardgame_mcts::LastGoodReply<Move,
+ SearchParamConst::max_players,
+ SearchParamConst::lgr_hash_table_size,
+ SearchParamConst::multithread>;
+
+
+ /** 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);
+
+ /** 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>& mv);
+
+ void evaluate_playout(array<Float, 6>& result);
+
+ void play_playout(Move mv);
+
+ /** Check if RAVE value for this move should not be updated. */
+ bool skip_rave(Move mv) const;
+
+#ifdef LIBBOARDGAME_DEBUG
+ string dump() const;
+#endif
+
+ string get_info() const;
+
+private:
+ /** 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 PlayoutFeatures::get_nu_local(). */
+ array<float, PlayoutFeatures::max_local + 1> m_gamma_local;
+
+ /** 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>
+ 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);
+
+ LIBBOARDGAME_NOINLINE
+ void add_callisto_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;
+
+ template<bool IS_CALLISTO>
+ const Board::PiecesLeftList& get_pieces_considered(Color c);
+
+ void init_gamma();
+
+ template<unsigned MAX_SIZE, unsigned MAX_ADJ_ATTACH, bool IS_CALLISTO>
+ void init_moves_with_gamma(Color c);
+
+ template<unsigned MAX_SIZE, bool IS_CALLISTO>
+ 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 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 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();
+ int has_attach_point = 0;
+ do
+ {
+ if (m_bd.is_forbidden(*i, c))
+ return false;
+ has_attach_point |= static_cast<int>(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 != 0;
+}
+
+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 (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()
+ return false;
+ PlayerInt player = get_player();
+ Move lgr2 = lgr.get_lgr2(player, last, second_last);
+ if (check_lgr(lgr2))
+ {
+ mv = PlayerMove<Move>(player, lgr2);
+ return true;
+ }
+ Move lgr1 = lgr.get_lgr1(player, last);
+ if (check_lgr(lgr1))
+ {
+ 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_range());
+ 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_range());
+ 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 if (m_max_piece_size == 7)
+ {
+ m_bd.play<7, 12>(to_play, mv);
+ update_playout_features<7, 12>(to_play, mv);
+ }
+ else
+ {
+ m_bd.play<22, 44>(to_play, mv);
+ update_playout_features<22, 44>(to_play, mv);
+ }
+ }
+ else
+ {
+ ++m_nu_passes;
+ m_bd.set_to_play(to_play.get_next(m_nu_colors));
+ }
+}
+
+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 if (m_max_piece_size == 7)
+ {
+ 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
+ LIBBOARDGAME_ASSERT(m_is_symmetry_broken);
+ }
+ else
+ {
+ m_bd.play<22, 44>(to_play, mv);
+ update_playout_features<22, 44>(to_play, mv);
+ if (! m_is_symmetry_broken)
+ update_symmetry_broken<22>(mv);
+ }
+ ++m_nu_new_moves[to_play];
+ m_last_move[to_play] = mv;
+ m_nu_passes = 0;
+}
+
+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);
+ if (MAX_SIZE == 7) // Nexos
+ LIBBOARDGAME_ASSERT(get_move_info_ext<MAX_ADJ_ATTACH>(mv).size_adj_points == 0);
+ else
+ 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 */
+//-----------------------------------------------------------------------------
+
+#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::IntType, Color::range> symmetric_state{ {1, 0, 3, 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));
+#ifdef 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.to_int() != 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.to_int() != 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 */
+//-----------------------------------------------------------------------------
+
+#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 {
+
+using libboardgame_sgf::Writer;
+using libpentobi_base::write_setup;
+using libpentobi_base::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()
+ << "\nPrior: " << node.get_move_prior()
+ << "\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;
+ children.reserve(node.get_nu_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 8 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 8 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 > 8)
+ nu_threads = 8;
+ return nu_threads;
+}
+
+//-----------------------------------------------------------------------------
+
+} // 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 {
+
+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 libpentobi_mcts
+
+#endif // LIBPENTOBI_MCTS_UTIL_H
--- /dev/null
+find_package(Qt5Gui 5.9 REQUIRED)
+
+add_library(pentobi_paint STATIC
+Paint.cpp
+Paint.h
+)
+
+target_compile_definitions(pentobi_paint PRIVATE
+ QT_DEPRECATED_WARNINGS
+ QT_DISABLE_DEPRECATED_BEFORE=0x051100)
+
+target_link_libraries(pentobi_paint pentobi_base Qt5::Gui)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_paint/Paint.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Paint.h"
+
+#include <QPainter>
+#include "libpentobi_base/CallistoGeometry.h"
+#include "libpentobi_base/ColorMap.h"
+
+using namespace std;
+using libboardgame_util::ArrayList;
+using libpentobi_base::CallistoGeometry;
+using libpentobi_base::Color;
+using libpentobi_base::ColorMap;
+using libpentobi_base::Geometry;
+using libpentobi_base::GeometryType;
+using libpentobi_base::Point;
+
+namespace libpentobi_paint {
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void paintQuarterSquareBase(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base)
+{
+ const QPointF polygon[3] =
+ {
+ QPointF(x, y),
+ QPointF(x + width, y),
+ QPointF(x, y + height)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(base);
+ painter.drawConvexPolygon(polygon, 3);
+}
+
+void paintQuarterSquareFrame(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& light)
+{
+ const QPointF polygon[4] =
+ {
+ QPointF(x, y + height),
+ QPointF(x, y + 0.9 * height),
+ QPointF(x + 0.9 * width, y),
+ QPointF(x + width, y)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(light);
+ painter.drawConvexPolygon(polygon, 4);
+}
+
+void paintSquareFrame(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& light,
+ const QColor& dark)
+{
+ painter.save();
+ painter.translate(x, y);
+ qreal border = 0.05 * max(width, height);
+ const QPointF down[4] =
+ {
+ QPointF(border, height - border),
+ QPointF(width - border, height - border),
+ QPointF(width, height),
+ QPointF(0, height)
+ };
+ const QPointF right[4] =
+ {
+ QPointF(width - border, height - border),
+ QPointF(width - border, border),
+ QPointF(width, 0),
+ QPointF(width, height)
+ };
+ const QPointF up[4] =
+ {
+ QPointF(0, 0),
+ QPointF(width, 0),
+ QPointF(width - border, border),
+ QPointF(border, border)
+ };
+ const QPointF left[4] =
+ {
+ QPointF(0, 0),
+ QPointF(border, border),
+ QPointF(border, height - border),
+ QPointF(0, height)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(dark);
+ painter.drawConvexPolygon(down, 4);
+ painter.drawConvexPolygon(right, 4);
+ painter.setBrush(light);
+ painter.drawConvexPolygon(up, 4);
+ painter.drawConvexPolygon(left, 4);
+ painter.restore();
+}
+
+void paintTriangleDownFrame(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& light,
+ const QColor& dark)
+{
+ painter.save();
+ painter.translate(x, y);
+ auto border = 0.05 * height;
+ const QPointF left[4] =
+ {
+ QPointF(0.5 * width, height),
+ QPointF(0.5 * width, height - 2 * border),
+ QPointF(width - 1.732 * border, border),
+ QPointF(width, 0)
+ };
+ const QPointF right[4] =
+ {
+ QPointF(0.5 * width, height),
+ QPointF(0.5 * width, height - 2 * border),
+ QPointF(1.732 * border, border),
+ QPointF(0, 0)
+ };
+ const QPointF up[4] =
+ {
+ QPointF(width, 0),
+ QPointF(width - 1.732 * border, border),
+ QPointF(1.732 * border, border),
+ QPointF(0, 0)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(dark);
+ painter.drawConvexPolygon(left, 4);
+ painter.drawConvexPolygon(right, 4);
+ painter.setBrush(light);
+ painter.drawConvexPolygon(up, 4);
+ painter.restore();
+}
+
+void paintTriangleUpFrame(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& light,
+ const QColor& dark)
+{
+ painter.save();
+ painter.translate(x, y);
+ auto border = 0.05 * height;
+ const QPointF down[4] =
+ {
+ QPointF(0, height),
+ QPointF(width, height),
+ QPointF(width - 1.732 * border, height - border),
+ QPointF(1.732 * border, height - border)
+ };
+ const QPointF left[4] =
+ {
+ QPointF(0.5 * width, 0),
+ QPointF(0.5 * width, 2 * border),
+ QPointF(1.732 * border, height - border),
+ QPointF(0, height)
+ };
+ const QPointF right[4] =
+ {
+ QPointF(0.5 * width, 0),
+ QPointF(0.5 * width, 2 * border),
+ QPointF(width - 1.732 * border, height - border),
+ QPointF(width, height)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(dark);
+ painter.drawConvexPolygon(down, 4);
+ painter.setBrush(light);
+ painter.drawConvexPolygon(left, 4);
+ painter.drawConvexPolygon(right, 4);
+ painter.restore();
+}
+
+void paintBoardCallisto(QPainter& painter, qreal width, qreal height,
+ const Geometry& geo, unsigned nuColors,
+ const QColor& base, const QColor& light,
+ const QColor& dark, const QColor& centerBase,
+ const QColor& centerLight, const QColor& centerDark)
+{
+ auto gridWidth = width / geo.get_width();
+ auto gridHeight = height / geo.get_height();
+ for (auto p : geo)
+ {
+ auto x = geo.get_x(p);
+ auto y = geo.get_y(p);
+ if (CallistoGeometry::is_center_section(x, y, nuColors))
+ {
+ painter.fillRect(QRectF(x * gridWidth, y * gridHeight, gridWidth,
+ gridHeight), base);
+ painter.fillRect(QRectF(x * gridWidth + 0.05 * gridWidth,
+ y * gridHeight + 0.05 * gridHeight,
+ 0.9 * gridWidth, 0.9 * gridHeight),
+ centerBase);
+ paintSquareFrame(painter, x * gridWidth + 0.05 * gridWidth,
+ y * gridHeight + 0.05 * gridHeight,
+ 0.9 * gridWidth, 0.9 * gridHeight, centerDark,
+ centerLight);
+ }
+ else
+ {
+ painter.fillRect(QRectF(x * gridWidth, y * gridHeight, gridWidth,
+ gridHeight), base);
+ paintSquareFrame(painter, x * gridWidth + 0.05 * gridWidth,
+ y * gridHeight + 0.05 * gridHeight,
+ 0.9 * gridWidth, 0.9 * gridHeight, dark, light);
+ }
+ }
+}
+
+void paintBoardClassic(QPainter& painter, qreal width, qreal height,
+ const Geometry& geo, const QColor& base,
+ const QColor& light, const QColor& dark)
+{
+ painter.fillRect(QRectF(0, 0, width, height), base);
+ auto gridWidth = width / geo.get_width();
+ auto gridHeight = height / geo.get_height();
+ for (unsigned x = 0; x < geo.get_width(); ++x)
+ for (unsigned y = 0; y < geo.get_height(); ++y)
+ paintSquareFrame(painter, x * gridWidth, y * gridHeight, gridWidth,
+ gridHeight, dark, light);
+}
+
+void paintBoardNexos(QPainter& painter, qreal width, qreal height,
+ const Geometry& geo, const QColor& base,
+ const QColor& light, const QColor& dark)
+{
+ painter.fillRect(QRectF(0, 0, width, height), base);
+ auto gridWidth = width / (geo.get_width() - 0.5);
+ auto gridHeight = height / (geo.get_height() - 0.5);
+ for (unsigned x = 1; x < geo.get_width(); x += 2)
+ for (unsigned y = 0; y < geo.get_height(); y += 2)
+ paintSquareFrame(painter, x * gridWidth - 0.5 * gridWidth,
+ y * gridHeight, 1.5 * gridWidth, 0.5 * gridHeight,
+ dark, light);
+ for (unsigned x = 0; x < geo.get_width(); x += 2)
+ for (unsigned y = 1; y < geo.get_height(); y += 2)
+ paintSquareFrame(painter, x * gridWidth,
+ y * gridHeight - 0.5 * gridHeight,
+ 0.5 * gridWidth, 1.5 * gridHeight, dark, light);
+}
+
+void paintBoardGembloQ(QPainter& painter, qreal width, qreal height,
+ const Geometry& geo, const QColor& base,
+ const QColor& light, const QColor& dark)
+{
+ auto gridWidth = width / geo.get_width();
+ auto gridHeight = height / geo.get_height();
+ qreal distX, distY;
+ switch (geo.get_height())
+ {
+ case 22:
+ case 26:
+ distX = 14 * gridWidth;
+ distY = 7 * gridHeight;
+ break;
+ default:
+ LIBBOARDGAME_ASSERT(geo.get_height() == 28);
+ distX = 2 * gridWidth;
+ distY = gridHeight;
+ break;
+ }
+ const QPointF board[8] =
+ {
+ QPointF(distX, 0),
+ QPointF(width - distX, 0),
+ QPointF(width, distY),
+ QPointF(width, height - distY),
+ QPointF(width - distX, height),
+ QPointF(distX, height),
+ QPointF(0, height - distY),
+ QPointF(0, distY)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(base);
+ painter.drawConvexPolygon(board, 8);
+ for (auto p : geo)
+ {
+ painter.save();
+ painter.translate(QPointF(geo.get_x(p) * gridWidth,
+ geo.get_y(p) * gridHeight));
+ QColor border;
+ switch (geo.get_point_type(p))
+ {
+ case 0:
+ border = light;
+ break;
+ case 1:
+ border = dark;
+ painter.rotate(180);
+ painter.translate(-gridWidth, -gridHeight);
+ break;
+ case 2:
+ border = dark;
+ painter.rotate(270);
+ painter.translate(-gridHeight, 0);
+ break;
+ case 3:
+ border = light;
+ painter.rotate(90);
+ painter.translate(0, -gridWidth);
+ break;
+ }
+ paintQuarterSquareFrame(painter, 0, 0, 2 * gridWidth, gridHeight,
+ border);
+ painter.restore();
+ }
+}
+
+void paintBoardTrigon(QPainter& painter, qreal width, qreal height,
+ const Geometry& geo, const QColor& base,
+ const QColor& light, const QColor& dark)
+{
+ auto gridWidth = width / (geo.get_width() + 1);
+ auto gridHeight = height / geo.get_height();
+ auto dist = (geo.get_width() + 1 - geo.get_height()) * gridWidth/ 2;
+ const QPointF board[6] =
+ {
+ QPointF(dist, 0),
+ QPointF(width - dist, 0),
+ QPointF(width, height / 2),
+ QPointF(width - dist, height),
+ QPointF(dist, height),
+ QPointF(0, height / 2)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(base);
+ painter.drawConvexPolygon(board, 6);
+ for (auto p : geo)
+ if (geo.get_point_type(p) == 0)
+ paintTriangleUpFrame(painter, geo.get_x(p) * gridWidth - 0.5,
+ geo.get_y(p) * gridHeight, 2 * gridWidth,
+ gridHeight, dark, light);
+ else
+ paintTriangleDownFrame(painter, geo.get_x(p) * gridWidth - 0.5,
+ geo.get_y(p) * gridHeight, 2 * gridWidth,
+ gridHeight, dark, light);
+}
+
+void paintPiecesCallisto(
+ QPainter& painter, qreal width, qreal height, const Geometry& geo,
+ const Grid<PointState>& pointState, const Grid<unsigned>& pieceId,
+ const ColorMap<QColor>& base, const ColorMap<QColor>& light,
+ const ColorMap<QColor>& dark)
+{
+ auto gridWidth = width / geo.get_width();
+ auto gridHeight = height / geo.get_height();
+ for (auto p : geo)
+ {
+ if (pointState[p].is_empty())
+ continue;
+ auto c = pointState[p].to_color();
+ auto x = geo.get_x(p);
+ auto y = geo.get_y(p);
+ bool hasLeft =
+ (geo.is_onboard(x - 1, y)
+ && pieceId[p] == pieceId[geo.get_point(x - 1, y)]);
+ bool hasRight =
+ (geo.is_onboard(x + 1, y)
+ && pieceId[p] == pieceId[geo.get_point(x + 1, y)]);
+ bool hasUp =
+ (geo.is_onboard(x, y - 1)
+ && pieceId[p] == pieceId[geo.get_point(x, y - 1)]);
+ bool hasDown =
+ (geo.is_onboard(x, y + 1)
+ && pieceId[p] == pieceId[geo.get_point(x, y + 1)]);
+ if (! hasLeft && ! hasRight && ! hasUp && ! hasDown)
+ {
+ paintCallistoOnePiece(painter, x * gridWidth + 0.05 * gridWidth,
+ y * gridHeight + 0.05 * gridHeight,
+ 0.9 * gridWidth, 0.9 * gridHeight, base[c],
+ light[c], dark[c]);
+ continue;
+ }
+ if (hasRight)
+ painter.fillRect(
+ QRectF(x * gridWidth + 0.96 * gridWidth,
+ y * gridHeight + 0.07 * gridHeight,
+ 0.08 * gridWidth, 0.86 * gridHeight), base[c]);
+ if (hasDown)
+ painter.fillRect(
+ QRectF(x * gridWidth + 0.07 * gridWidth,
+ y * gridHeight + 0.96 * gridHeight,
+ 0.86 * gridWidth, 0.08 * gridHeight), base[c]);
+ paintSquare(painter, x * gridWidth + 0.05 * gridWidth,
+ y * gridHeight + 0.05 * gridHeight, 0.9 * gridWidth,
+ 0.9 * gridHeight, base[c], light[c], dark[c]);
+ }
+}
+
+void paintPiecesClassic(
+ QPainter& painter, qreal width, qreal height, const Geometry& geo,
+ const Grid<PointState>& pointState, const ColorMap<QColor>& base,
+ const ColorMap<QColor>& light, const ColorMap<QColor>& dark)
+{
+ auto gridWidth = width / geo.get_width();
+ auto gridHeight = height / geo.get_height();
+ for (auto p : geo)
+ {
+ if (pointState[p].is_empty())
+ continue;
+ auto c = pointState[p].to_color();
+ paintSquare(painter, geo.get_x(p) * gridWidth,
+ geo.get_y(p) * gridHeight, gridWidth, gridHeight, base[c],
+ light[c], dark[c]);
+ }
+}
+
+void paintPiecesGembloQ(
+ QPainter& painter, qreal width, qreal height, const Geometry& geo,
+ const Grid<PointState>& pointState, const ColorMap<QColor>& base,
+ const ColorMap<QColor>& light, const ColorMap<QColor>& dark)
+{
+ auto gridWidth = width / geo.get_width();
+ auto gridHeight = height / geo.get_height();
+ for (auto p : geo)
+ {
+ if (pointState[p].is_empty())
+ continue;
+ auto c = pointState[p].to_color();
+ painter.save();
+ painter.translate(QPointF(geo.get_x(p) * gridWidth,
+ geo.get_y(p) * gridHeight));
+ QColor border;
+ switch (geo.get_point_type(p))
+ {
+ case 0:
+ border = light[c];
+ break;
+ case 1:
+ border = dark[c];
+ painter.rotate(180);
+ painter.translate(-gridWidth, -gridHeight);
+ break;
+ case 2:
+ border = dark[c];
+ painter.rotate(270);
+ painter.translate(-gridHeight, 0);
+ break;
+ case 3:
+ border = light[c];
+ painter.rotate(90);
+ painter.translate(0, -gridWidth);
+ break;
+ }
+ // Antialiasing cause unwanted seams between quarter squares
+ painter.setRenderHint(QPainter::Antialiasing, false);
+ paintQuarterSquareBase(painter, 0, 0, 2 * gridWidth, gridHeight,
+ base[c]);
+ painter.setRenderHint(QPainter::Antialiasing);
+ paintQuarterSquareFrame(painter, 0, 0, 2 * gridWidth, gridHeight,
+ border);
+ painter.restore();
+ }
+}
+
+void paintJunction(QPainter& painter, const Geometry& geo,
+ const Grid<PointState>& pointState,
+ const Grid<unsigned>& pieceId, Point p, qreal gridWidth,
+ qreal gridHeight, const ColorMap<QColor>& base)
+{
+ auto x = geo.get_x(p);
+ auto y = geo.get_y(p);
+ ArrayList<unsigned, 4> pieces;
+ if (x > 0)
+ {
+ auto piece = pieceId[geo.get_point(x - 1, y)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ if (x < geo.get_width() - 1)
+ {
+ auto piece = pieceId[geo.get_point(x + 1, y)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ if (y > 0)
+ {
+ auto piece = pieceId[geo.get_point(x, y - 1)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ if (y < geo.get_height() - 1)
+ {
+ auto piece = pieceId[geo.get_point(x, y + 1)];
+ if (piece != 0)
+ pieces.include(piece);
+ }
+ for (auto piece : pieces)
+ {
+ Color c;
+ bool hasLeft = false;
+ if (x > 0)
+ {
+ auto p = geo.get_point(x - 1, y);
+ if (pieceId[p] == piece)
+ {
+ hasLeft = true;
+ c = pointState[p].to_color();
+ }
+ }
+ bool hasRight = false;
+ if (x < geo.get_width() - 1)
+ {
+ auto p = geo.get_point(x + 1, y);
+ if (pieceId[p] == piece)
+ {
+ hasRight = true;
+ c = pointState[p].to_color();
+ }
+ }
+ bool hasUp = false;
+ if (y > 0)
+ {
+ auto p = geo.get_point(x, y - 1);
+ if (pieceId[p] == piece)
+ {
+ hasUp = true;
+ c = pointState[p].to_color();
+ }
+ }
+ bool hasDown = false;
+ if (y < geo.get_height() - 1)
+ {
+ auto p = geo.get_point(x, y + 1);
+ if (pieceId[p] == piece)
+ {
+ hasDown = true;
+ c = pointState[p].to_color();
+ }
+ }
+ auto w = 0.5 * gridWidth;
+ auto h = 0.5 * gridHeight;
+ painter.save();
+ painter.translate(x * gridWidth, y * gridHeight);
+ if (hasLeft && hasRight && hasUp && hasDown)
+ paintJunctionAll(painter, 0, 0, w, h, base[c]);
+ else if (hasLeft && hasRight && ! hasUp && ! hasDown)
+ paintJunctionStraight(painter, 0, 0, w, h, base[c]);
+ else if (! hasLeft && ! hasRight && hasUp && hasDown)
+ {
+ painter.save();
+ painter.rotate(90);
+ painter.translate(0, -w);
+ paintJunctionStraight(painter, 0, 0, w, h, base[c]);
+ painter.restore();
+ }
+ else if (! hasLeft && hasRight && ! hasUp && hasDown)
+ paintJunctionRight(painter, 0, 0, w, h, base[c]);
+ else if (hasLeft && ! hasRight && ! hasUp && hasDown)
+ {
+ painter.save();
+ painter.rotate(90);
+ painter.translate(0, -w);
+ paintJunctionRight(painter, 0, 0, w, h, base[c]);
+ painter.restore();
+ }
+ else if (hasLeft && ! hasRight && hasUp && ! hasDown)
+ {
+ painter.save();
+ painter.rotate(180);
+ painter.translate(-w, -h);
+ paintJunctionRight(painter, 0, 0, w, h, base[c]);
+ painter.restore();
+ }
+ else if (! hasLeft && hasRight && hasUp && ! hasDown)
+ {
+ painter.save();
+ painter.rotate(270);
+ painter.translate(-h, 0);
+ paintJunctionRight(painter, 0, 0, w, h, base[c]);
+ painter.restore();
+ }
+ else if (hasLeft && hasRight && ! hasUp && hasDown)
+ paintJunctionT(painter, 0, 0, w, h, base[c]);
+ else if (hasLeft && ! hasRight && hasUp && hasDown)
+ {
+ painter.save();
+ painter.rotate(90);
+ painter.translate(0, -w);
+ paintJunctionT(painter, 0, 0, w, h, base[c]);
+ painter.restore();
+ }
+ else if (hasLeft && hasRight && hasUp && ! hasDown)
+ {
+ painter.save();
+ painter.rotate(180);
+ painter.translate(-w, -h);
+ paintJunctionT(painter, 0, 0, w, h, base[c]);
+ painter.restore();
+ }
+ else if (! hasLeft && hasRight && hasUp && hasDown)
+ {
+ painter.save();
+ painter.rotate(270);
+ painter.translate(-h, 0);
+ paintJunctionT(painter, 0, 0, w, h, base[c]);
+ painter.restore();
+ }
+ painter.restore();
+ }
+}
+
+void paintPiecesNexos(
+ QPainter& painter, qreal width, qreal height, const Geometry& geo,
+ const Grid<PointState>& pointState, const Grid<unsigned>& pieceId,
+ const ColorMap<QColor>& base, const ColorMap<QColor>& light,
+ const ColorMap<QColor>& dark)
+{
+ auto gridWidth = width / (geo.get_width() - 0.5);
+ auto gridHeight = height / (geo.get_height() - 0.5);
+ for (auto p : geo)
+ {
+ switch (geo.get_point_type(p))
+ {
+ case 0:
+ paintJunction(painter, geo, pointState, pieceId, p, gridWidth,
+ gridHeight, base);
+ break;
+ case 1:
+ {
+ if (pointState[p].is_empty())
+ continue;
+ auto c = pointState[p].to_color();
+ paintSquare(painter, geo.get_x(p) * gridWidth - 0.5 * gridWidth,
+ geo.get_y(p) * gridHeight, 1.5 * gridWidth,
+ 0.5 * gridHeight, base[c], light[c], dark[c]);
+ break;
+ }
+ case 2:
+ {
+ if (pointState[p].is_empty())
+ continue;
+ auto c = pointState[p].to_color();
+ paintSquare(painter, geo.get_x(p) * gridWidth,
+ geo.get_y(p) * gridHeight - 0.5 * gridHeight,
+ 0.5 * gridWidth, 1.5 * gridHeight, base[c], light[c],
+ dark[c]);
+ break;
+ }
+ }
+ }
+}
+
+void paintPiecesTrigon(
+ QPainter& painter, qreal width, qreal height, const Geometry& geo,
+ const Grid<PointState>& pointState, const ColorMap<QColor>& base,
+ const ColorMap<QColor>& light, const ColorMap<QColor>& dark)
+{
+ auto gridWidth = width / (geo.get_width() + 1);
+ auto gridHeight = height / geo.get_height();
+ for (auto p : geo)
+ {
+ if (pointState[p].is_empty())
+ continue;
+ auto c = pointState[p].to_color();
+ if (geo.get_point_type(p) == 0)
+ paintTriangleUp(painter, geo.get_x(p) * gridWidth - 0.5,
+ geo.get_y(p) * gridHeight, 2 * gridWidth,
+ gridHeight, base[c], light[c], dark[c]);
+ else
+ paintTriangleDown(painter, geo.get_x(p) * gridWidth - 0.5,
+ geo.get_y(p) * gridHeight, 2 * gridWidth,
+ gridHeight, base[c], light[c], dark[c]);
+ }
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+void paint(QPainter& painter, qreal width, qreal height, Variant variant,
+ const Geometry& geo, const Grid<PointState>& pointState,
+ const Grid<unsigned>& pieceId)
+{
+ const QColor boardBase(174, 167, 172);
+ const QColor boardLight(199, 191, 197);
+ const QColor boardDark(134, 128, 132);
+ const QColor centerBase(145, 139, 143);
+ const QColor centerLight(160, 154, 159);
+ const QColor centerDark(124, 119, 123);
+ painter.setRenderHint(QPainter::Antialiasing);
+ paintBoard(painter, width, height, variant, boardBase, boardLight,
+ boardDark, centerBase, centerLight, centerDark);
+ array<QColor, 3> blue{ {
+ QColor(0, 115, 207), QColor(20, 153, 255), QColor(0, 72, 129)} };
+ array<QColor, 3> green{ {
+ QColor(0, 192, 0), QColor(0, 250, 0), QColor(0, 120, 0)} };
+ array<QColor, 3> orange{ {
+ QColor(240, 146, 23), QColor(255, 187, 103), QColor(157, 94, 11)} };
+ array<QColor, 3> purple{ {
+ QColor(161, 44, 207), QColor(190, 112, 220), QColor(109, 39, 135)} };
+ array<QColor, 3> red{ {
+ QColor(230, 62, 44), QColor(255, 101, 90), QColor(144, 38, 27)} };
+ array<QColor, 3> yellow{ {
+ QColor(245, 195, 32), QColor(255, 219, 88), QColor(170, 133, 22)} };
+ ColorMap<QColor> piecesBase;
+ ColorMap<QColor> piecesLight;
+ ColorMap<QColor> piecesDark;
+ if (variant == Variant::duo)
+ {
+ piecesBase[Color(0)] = purple[0];
+ piecesLight[Color(0)] = purple[1];
+ piecesDark[Color(0)] = purple[2];
+ }
+ else if (variant == Variant::junior)
+ {
+ piecesBase[Color(0)] = green[0];
+ piecesLight[Color(0)] = green[1];
+ piecesDark[Color(0)] = green[2];
+ }
+ else
+ {
+ piecesBase[Color(0)] = blue[0];
+ piecesLight[Color(0)] = blue[1];
+ piecesDark[Color(0)] = blue[2];
+ }
+ if (variant == Variant::duo || variant == Variant::junior)
+ {
+ piecesBase[Color(1)] = orange[0];
+ piecesLight[Color(1)] = orange[1];
+ piecesDark[Color(1)] = orange[2];
+ }
+ else if (get_nu_colors(variant) == 2)
+ {
+ piecesBase[Color(1)] = green[0];
+ piecesLight[Color(1)] = green[1];
+ piecesDark[Color(1)] = green[2];
+ }
+ else
+ {
+ piecesBase[Color(1)] = yellow[0];
+ piecesLight[Color(1)] = yellow[1];
+ piecesDark[Color(1)] = yellow[2];
+ }
+ piecesBase[Color(2)] = red[0];
+ piecesLight[Color(2)] = red[1];
+ piecesDark[Color(2)] = red[2];
+ piecesBase[Color(3)] = green[0];
+ piecesLight[Color(3)] = green[1];
+ piecesDark[Color(3)] = green[2];
+ switch (get_geometry_type(variant))
+ {
+ case GeometryType::classic:
+ paintPiecesClassic(painter, width, height, geo, pointState, piecesBase,
+ piecesLight, piecesDark);
+ break;
+ case GeometryType::trigon:
+ paintPiecesTrigon(painter, width, height, geo, pointState, piecesBase,
+ piecesLight, piecesDark);
+ break;
+ case GeometryType::nexos:
+ paintPiecesNexos(painter, width, height, geo, pointState, pieceId,
+ piecesBase, piecesLight, piecesDark);
+ break;
+ case GeometryType::callisto:
+ paintPiecesCallisto(painter, width, height, geo, pointState, pieceId,
+ piecesBase, piecesLight, piecesDark);
+ break;
+ case GeometryType::gembloq:
+ paintPiecesGembloQ(painter, width, height, geo, pointState, piecesBase,
+ piecesLight, piecesDark);
+ break;
+ }
+}
+
+void paintBoard(QPainter& painter, qreal width, qreal height, Variant variant,
+ const QColor& base, const QColor& light, const QColor& dark,
+ const QColor& centerBase, const QColor& centerLight,
+ const QColor& centerDark)
+{
+ auto& geo = get_geometry(variant);
+ switch (get_geometry_type(variant))
+ {
+ case GeometryType::classic:
+ paintBoardClassic(painter, width, height, geo, base, light, dark);
+ break;
+ case GeometryType::trigon:
+ paintBoardTrigon(painter, width, height, geo, base, light, dark);
+ break;
+ case GeometryType::nexos:
+ paintBoardNexos(painter, width, height, geo, base, light, dark);
+ break;
+ case GeometryType::callisto:
+ paintBoardCallisto(painter, width, height, geo, get_nu_colors(variant),
+ base, light, dark, centerBase, centerLight,
+ centerDark);
+ break;
+ case GeometryType::gembloq:
+ paintBoardGembloQ(painter, width, height, geo, base, light, dark);
+ break;
+ }
+}
+
+void paintCallistoOnePiece(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base,
+ const QColor& light, const QColor& dark)
+{
+ auto dx = 0.175 * width;
+ auto dy = 0.175 * height;
+ painter.fillRect(QRectF(x, y, width, dy), base);
+ painter.fillRect(QRectF(x, y + height - dy, width, dy), base);
+ painter.fillRect(QRectF(x, y, dx, height), base);
+ painter.fillRect(QRectF(x + width - dx, y, dx, height), base);
+ paintSquareFrame(painter, x, y, width, height, light, dark);
+}
+
+void paintJunctionAll(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base)
+{
+ auto dx = 0.22 * width;
+ auto dy = 0.22 * height;
+ painter.fillRect(QRectF(x + dx, y, width - 2 * dx, height), base);
+ painter.fillRect(QRectF(x, y + dy, width, height - 2 * dy), base);
+}
+
+void paintJunctionRight(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base)
+{
+ auto dx = 0.3 * width;
+ auto dy = 0.3 * height;
+ const QPointF polygon[3] =
+ {
+ QPointF(x + dx, y + height),
+ QPointF(x + width, y + height),
+ QPointF(x + width, y + dy)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(base);
+ painter.drawConvexPolygon(polygon, 3);
+}
+
+void paintJunctionStraight(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base)
+{
+ auto dy = 0.22 * height;
+ painter.fillRect(QRectF(x, y + dy, width, height - 2 * dy), base);
+}
+
+void paintJunctionT(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base)
+{
+ auto dx = 0.22 * width;
+ auto dy = 0.22 * height;
+ painter.fillRect(QRectF(x + dx, y + dy, width - 2 * dx, height - dy),
+ base);
+ painter.fillRect(QRectF(x, y + dy, width, height - 2 * dy), base);
+}
+
+void paintQuarterSquare(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light)
+{
+ paintQuarterSquareBase(painter, x, y, width, height, base);
+ paintQuarterSquareFrame(painter, x, y, width, height, light);
+}
+
+void paintSquare(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light,
+ const QColor& dark)
+{
+ painter.fillRect(QRectF(x, y, width, height), base);
+ paintSquareFrame(painter, x, y, width, height, light, dark);
+}
+
+void paintTriangleDown(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light,
+ const QColor& dark)
+{
+ const QPointF polygon[3] =
+ {
+ QPointF(x, y),
+ QPointF(x + width, y),
+ QPointF(x + 0.5 * width, y + height)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(base);
+ painter.drawConvexPolygon(polygon, 3);
+ paintTriangleDownFrame(painter, x, y, width, height, light, dark);
+}
+
+void paintTriangleUp(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light,
+ const QColor& dark)
+{
+ const QPointF polygon[3] =
+ {
+ QPointF(x, y + height),
+ QPointF(x + width, y + height),
+ QPointF(x + 0.5 * width, y)
+ };
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(base);
+ painter.drawConvexPolygon(polygon, 3);
+ paintTriangleUpFrame(painter, x, y, width, height, light, dark);
+}
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_paint
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_paint/Paint.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef LIBPENTOBI_PAINT_H
+#define LIBPENTOBI_PAINT_H
+
+#include <QtGlobal>
+#include "libpentobi_base/Grid.h"
+#include "libpentobi_base/PointState.h"
+#include "libpentobi_base/Variant.h"
+
+class QColor;
+class QPainter;
+
+namespace libpentobi_paint {
+
+using libpentobi_base::Grid;
+using libpentobi_base::Geometry;
+using libpentobi_base::PointState;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+/** Paint the board and pieces.
+ This function takes a Grid<PointState> for the board positions instead of
+ an instance of libpentobi_base::Board, because creating a Board is too
+ expensive for lightweight use cases like a thumbnailer.
+
+ The pieceId parameter only needs to be initialized in game variants Nexos
+ and Callisto. It is needed to paint the junctions between piece elements.
+ They must be 0 for empty points and contain a unique value for points
+ of the same piece. */
+void paint(QPainter& painter, qreal width, qreal height, Variant variant,
+ const Geometry& geo, const Grid<PointState>& pointState,
+ const Grid<unsigned>& pieceId);
+
+/** Paint empty board. */
+void paintBoard(QPainter& painter, qreal width, qreal height, Variant variant,
+ const QColor& base, const QColor& light, const QColor& dark,
+ const QColor& centerBase, const QColor& centerLight,
+ const QColor& centerDark);
+
+void paintCallistoOnePiece(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base,
+ const QColor& light, const QColor& dark);
+
+void paintJunctionAll(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base);
+
+void paintJunctionRight(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base);
+
+void paintJunctionStraight(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base);
+
+void paintJunctionT(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base);
+
+void paintQuarterSquare(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light);
+
+void paintSquare(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light,
+ const QColor& dark);
+
+void paintTriangleDown(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light,
+ const QColor& dark);
+
+void paintTriangleUp(QPainter& painter, qreal x, qreal y, qreal width,
+ qreal height, const QColor& base, const QColor& light,
+ const QColor& dark);
+
+//-----------------------------------------------------------------------------
+
+} // namespace libpentobi_paint
+
+#endif // LIBPENTOBI_PAINT_H
--- /dev/null
+find_package(Qt5Gui 5.9 REQUIRED)
+
+add_library(pentobi_thumbnail STATIC
+ CreateThumbnail.h
+ CreateThumbnail.cpp
+ )
+
+target_compile_definitions(pentobi_thumbnail PRIVATE
+ QT_DEPRECATED_WARNINGS
+ QT_DISABLE_DEPRECATED_BEFORE=0x051100)
+
+target_link_libraries(pentobi_thumbnail pentobi_paint)
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file libpentobi_thumbnail/CreateThumbnail.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "CreateThumbnail.h"
+
+#include <QPainter>
+#include "libboardgame_sgf/TreeReader.h"
+#include "libpentobi_base/NodeUtil.h"
+#include "libpentobi_paint/Paint.h"
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libboardgame_sgf::TreeReader;
+using libpentobi_base::Color;
+using libpentobi_base::Geometry;
+using libpentobi_base::Grid;
+using libpentobi_base::MovePoints;
+using libpentobi_base::PieceSet;
+using libpentobi_base::Point;
+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)
+{
+ if (! node.has_property(id))
+ return;
+ for (auto& s : node.get_multi_property(id))
+ {
+ ++currentPieceId;
+ auto begin = s.begin();
+ auto end = begin;
+ while (true)
+ {
+ while (end != s.end() && *end != ',')
+ ++end;
+ Point p;
+ if (geo.from_string(begin, end, p))
+ {
+ pointState[p] = PointState(c);
+ pieceId[p] = currentPieceId;
+ }
+ if (end == s.end())
+ break;
+ ++end;
+ begin = end;
+ }
+ }
+}
+
+/** Helper function for getFinalPosition() */
+void handleSetupEmpty(const SgfNode& node, const Geometry& geo,
+ Grid<PointState>& pointState, Grid<unsigned>& pieceId)
+{
+ if (! node.has_property("AE"))
+ return;
+ for (auto& s : node.get_multi_property("AE"))
+ {
+ auto begin = s.begin();
+ auto end = begin;
+ while (true)
+ {
+ while (end != s.end() && *end != ',')
+ ++end;
+ Point p;
+ if (geo.from_string(begin, end, p))
+ {
+ pointState[p] = PointState::empty();
+ pieceId[p] = 0;
+ }
+ if (end == s.end())
+ break;
+ ++end;
+ begin = end;
+ }
+ }
+}
+
+/** 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;
+ do
+ {
+ if (libpentobi_base::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::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();
+ }
+ while (node != nullptr);
+ 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; // Init to avoid compiler warning
+ const Geometry* geo;
+ Grid<PointState> pointState;
+ Grid<unsigned> pieceId;
+ if (! getFinalPosition(reader.get_tree(), variant, geo, pointState,
+ pieceId))
+ return false;
+ qreal ratio;
+ if (get_piece_set(variant) == PieceSet::trigon)
+ ratio = geo->get_height() * 1.732 / geo->get_width();
+ else
+ ratio = 1;
+ qreal paintWidth = min(static_cast<qreal>(width), height / ratio);
+ qreal paintHeight = ratio * paintWidth;
+ QPainter painter(&image);
+ painter.translate(QPointF((width - paintWidth) / 2,
+ (height - paintHeight) / 2));
+ libpentobi_paint::paint(painter, paintWidth, paintHeight, variant, *geo,
+ pointState, pieceId);
+ 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/AnalyzeGameModel.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "AnalyzeGameModel.h"
+
+#include <QSettings>
+#include <QtConcurrentRun>
+#include "GameModel.h"
+#include "PlayerModel.h"
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_util/Abort.h"
+
+using libboardgame_sgf::is_main_variation;
+using libboardgame_sgf::find_root;
+using libboardgame_util::clear_abort;
+using libboardgame_util::set_abort;
+using libboardgame_util::ArrayList;
+using libpentobi_base::ColorMove;
+
+//-----------------------------------------------------------------------------
+
+AnalyzeGameElement::AnalyzeGameElement(QObject* parent, int moveColor,
+ double value)
+ : QObject(parent),
+ m_moveColor(moveColor),
+ m_value(value)
+{
+}
+
+//-----------------------------------------------------------------------------
+
+AnalyzeGameModel::AnalyzeGameModel(QObject* parent)
+ : QObject(parent)
+{
+ connect(&m_watcher, &QFutureWatcher<void>::finished, this, [this]
+ {
+ updateElements();
+ // Set isRunning after updating elements because in GameDisplayDesktop
+ // either isRunning must be true or elements.length > 0 to show the
+ // analysis and we don't want it to disappear if a game with one move
+ // was analyzed.
+ setIsRunning(false);
+ });
+}
+
+AnalyzeGameModel::~AnalyzeGameModel()
+{
+ cancel();
+}
+
+void AnalyzeGameModel::asyncRun(const Game* game, Search* search)
+{
+ auto progressCallback =
+ [&](unsigned movesAnalyzed, unsigned totalMoves)
+ {
+ Q_UNUSED(movesAnalyzed);
+ Q_UNUSED(totalMoves);
+ // Use invokeMethod() because callback runs in different thread
+ QMetaObject::invokeMethod(this, "updateElements",
+ Qt::BlockingQueuedConnection);
+ };
+ m_analyzeGame.run(*game, *search, m_nuSimulations, progressCallback);
+}
+
+void AnalyzeGameModel::autoSave(GameModel* gameModel)
+{
+ auto& bd = gameModel->getGame().get_board();
+ QVariantList list;
+ auto variant = bd.get_variant();
+ auto nuMoves = m_analyzeGame.get_nu_moves();
+ QSettings settings;
+ if (nuMoves == 0 || m_analyzeGame.get_variant() != variant)
+ settings.remove(QStringLiteral("analyzeGame"));
+ else
+ {
+ list.append(to_string_id(variant));
+ list.append(nuMoves);
+ for (unsigned i = 0; i < nuMoves; ++i)
+ {
+ auto mv = m_analyzeGame.get_move(i);
+ list.append(mv.color.to_int());
+ list.append(bd.to_string(mv.move).c_str());
+ list.append(m_analyzeGame.get_value(i));
+ }
+ settings.setValue(QStringLiteral("analyzeGame"),
+ QVariant::fromValue(list));
+ }
+}
+
+void AnalyzeGameModel::cancel()
+{
+ if (! m_isRunning)
+ return;
+ set_abort();
+ m_watcher.waitForFinished();
+ setIsRunning(false);
+}
+
+void AnalyzeGameModel::clear()
+{
+ cancel();
+ if (m_elements.empty())
+ return;
+ m_analyzeGame.clear();
+ m_markMoveNumber = -1;
+ m_elements.clear();
+ emit elementsChanged();
+}
+
+QQmlListProperty<AnalyzeGameElement> AnalyzeGameModel::elements()
+{
+ return {this, m_elements};
+}
+
+void AnalyzeGameModel::gotoMove(GameModel* gameModel, int moveNumber)
+{
+ if (moveNumber < 0)
+ return;
+ auto n = static_cast<unsigned>(moveNumber);
+ if (n >= m_analyzeGame.get_nu_moves())
+ return;
+ auto& game = gameModel->getGame();
+ if (game.get_variant() != m_analyzeGame.get_variant())
+ return;
+ auto& tree = game.get_tree();
+ auto node = &tree.get_root();
+ if (tree.has_move(*node))
+ {
+ // Move in root node not supported.
+ setMarkMoveNumber(-1);
+ return;
+ }
+ for (unsigned i = 0; i < n; ++i)
+ {
+ auto mv = m_analyzeGame.get_move(i);
+ bool found = false;
+ for (auto& child : node->get_children())
+ if (tree.get_move(child) == mv)
+ {
+ found = true;
+ node = &child;
+ break;
+ }
+ if (! found)
+ {
+ setMarkMoveNumber(-1);
+ return;
+ }
+ }
+ gameModel->gotoNode(*node);
+ setMarkMoveNumber(moveNumber);
+}
+
+void AnalyzeGameModel::loadAutoSave(GameModel* gameModel)
+{
+ QSettings settings;
+ auto list =
+ settings.value(
+ QStringLiteral("analyzeGame")).value<QVariantList>();
+ int size = list.size();
+ int index = 0;
+ if (index >= size)
+ return;
+ auto variant = list[index++].toString();
+ auto& bd = gameModel->getGame().get_board();
+ if (variant != to_string_id(bd.get_variant()))
+ return;
+ if (index >= size)
+ return;
+ auto nuMoves = list[index++].toUInt();
+ vector<ColorMove> moves;
+ vector<double> values;
+ for (unsigned i = 0; i < nuMoves; ++i)
+ {
+ if (index >= size)
+ return;
+ auto color = list[index++].toUInt();
+ if (color >= bd.get_nu_colors())
+ return;
+ if (index >= size)
+ return;
+ auto moveString = list[index++].toString();
+ Move mv;
+ if (! bd.from_string(mv, moveString.toLatin1().constData()))
+ return;
+ if (index >= size)
+ return;
+ auto value = list[index++].toDouble();
+ moves.emplace_back(Color(static_cast<Color::IntType>(color)), mv);
+ values.push_back(value);
+ }
+ m_analyzeGame.set(bd.get_variant(), moves, values);
+ updateElements();
+}
+
+void AnalyzeGameModel::markCurrentMove(GameModel* gameModel)
+{
+ auto& game = gameModel->getGame();
+ auto& node = game.get_current();
+ int moveNumber = -1;
+ if (is_main_variation(node))
+ {
+ ArrayList<ColorMove, Board::max_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_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;
+ moveNumber = static_cast<int>(moves.size());
+ }
+ }
+ setMarkMoveNumber(moveNumber);
+}
+
+void AnalyzeGameModel::setIsRunning(bool isRunning)
+{
+ if (m_isRunning == isRunning)
+ return;
+ m_isRunning = isRunning;
+ emit isRunningChanged();
+}
+
+void AnalyzeGameModel::setMarkMoveNumber(int markMoveNumber)
+{
+ if (m_markMoveNumber == markMoveNumber)
+ return;
+ m_markMoveNumber = markMoveNumber;
+ emit markMoveNumberChanged();
+}
+
+void AnalyzeGameModel::start(GameModel* gameModel, PlayerModel* playerModel,
+ int nuSimulations)
+{
+ if (nuSimulations <= 0)
+ return;
+ m_markMoveNumber = -1;
+ m_nuSimulations = static_cast<size_t>(nuSimulations);
+ cancel();
+ clear_abort();
+ auto future = QtConcurrent::run(this, &AnalyzeGameModel::asyncRun,
+ &gameModel->getGame(),
+ &playerModel->getSearch());
+ m_watcher.setFuture(future);
+ setIsRunning(true);
+}
+
+void AnalyzeGameModel::updateElements()
+{
+ m_elements.clear();
+ for (unsigned i = 0; i < m_analyzeGame.get_nu_moves(); ++i)
+ {
+ auto moveColor = m_analyzeGame.get_move(i).color.to_int();
+ // Values of search are supposed to be win/loss probabilities but can
+ // be slightly outside [0..1] (see libpentobi_mcts::State).
+ auto value = max(0., min(1., m_analyzeGame.get_value(i)));
+ m_elements.append(new AnalyzeGameElement(this, moveColor, value));
+ }
+ emit elementsChanged();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AnalyzeGameModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_ANALYZE_GAME_MODEL_H
+#define PENTOBI_ANALYZE_GAME_MODEL_H
+
+#include <QFutureWatcher>
+#include <QQmlListProperty>
+#include "libpentobi_mcts/AnalyzeGame.h"
+
+class GameModel;
+class PlayerModel;
+namespace libpentobi_base { class Game; }
+namespace libpentobi_mcts { class Search; }
+
+using libpentobi_base::Game;
+using libpentobi_mcts::AnalyzeGame;
+using libpentobi_mcts::Search;
+
+//-----------------------------------------------------------------------------
+
+class AnalyzeGameElement
+ : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int moveColor MEMBER m_moveColor CONSTANT)
+ Q_PROPERTY(double value MEMBER m_value CONSTANT)
+
+public:
+ explicit AnalyzeGameElement(QObject* parent, int moveColor, double value);
+
+private:
+ int m_moveColor;
+
+ double m_value;
+};
+
+//-----------------------------------------------------------------------------
+
+class AnalyzeGameModel
+ : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QQmlListProperty<AnalyzeGameElement> elements READ elements NOTIFY elementsChanged)
+ Q_PROPERTY(bool isRunning READ isRunning NOTIFY isRunningChanged)
+ Q_PROPERTY(int markMoveNumber READ markMoveNumber NOTIFY markMoveNumberChanged)
+
+public:
+ explicit AnalyzeGameModel(QObject* parent = nullptr);
+
+ ~AnalyzeGameModel() override;
+
+
+ Q_INVOKABLE void autoSave(GameModel* gameModel);
+
+ Q_INVOKABLE void cancel();
+
+ Q_INVOKABLE void clear();
+
+ Q_INVOKABLE void gotoMove(GameModel* gameModel, int moveNumber);
+
+ Q_INVOKABLE void loadAutoSave(GameModel* gameModel);
+
+ Q_INVOKABLE void markCurrentMove(GameModel* gameModel);
+
+ Q_INVOKABLE void start(GameModel* gameModel, PlayerModel* playerModel,
+ int nuSimulations);
+
+
+ bool isRunning() const { return m_isRunning; }
+
+ int markMoveNumber() const { return m_markMoveNumber; }
+
+ QQmlListProperty<AnalyzeGameElement> elements();
+
+signals:
+ void isRunningChanged();
+
+ void markMoveNumberChanged();
+
+ void progressChanged();
+
+ void elementsChanged();
+
+private:
+ bool m_isRunning = false;
+
+ int m_markMoveNumber = -1;
+
+ size_t m_nuSimulations;
+
+ QList<AnalyzeGameElement*> m_elements;
+
+ QFutureWatcher<void> m_watcher;
+
+ AnalyzeGame m_analyzeGame;
+
+
+ Q_INVOKABLE void updateElements();
+
+
+ void asyncRun(const Game* game, Search* search);
+
+ void setIsRunning(bool isRunning);
+
+ void setMarkMoveNumber(int markMoveNumber);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANALYZE_GAME_MODEL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AndroidUtils.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "AndroidUtils.h"
+
+#include <QCoreApplication>
+#include <QStandardPaths>
+
+#ifdef Q_OS_ANDROID
+#include <QDir>
+#include <QDirIterator>
+#include <QtAndroidExtras/QtAndroid>
+#include <QtAndroidExtras/QAndroidJniObject>
+#endif
+
+//-----------------------------------------------------------------------------
+
+bool AndroidUtils::checkPermission(const QString& permission)
+{
+#ifdef Q_OS_ANDROID
+ return QtAndroid::checkPermission(permission) ==
+ QtAndroid::PermissionResult::Granted;
+#else
+ Q_UNUSED(permission);
+ return true;
+#endif
+}
+
+QUrl AndroidUtils::extractHelp(const QString& language)
+{
+#ifdef Q_OS_ANDROID
+ if (language != QStringLiteral("C"))
+ // Other languages use pictures from C
+ extractHelp(QStringLiteral("C"));
+ auto activity = QtAndroid::androidActivity();
+ auto filesDir =
+ activity.callObjectMethod("getFilesDir", "()Ljava/io/File;");
+ if (! filesDir.isValid())
+ return {};
+ auto filesDirString = filesDir.callObjectMethod("toString",
+ "()Ljava/lang/String;");
+ if (! filesDirString.isValid())
+ return {};
+ QDir dir(filesDirString.toString() + "/help/"
+ + QCoreApplication::applicationVersion() + "/" + language
+ + "/pentobi");
+ auto dirPath = dir.path();
+ if (QFileInfo::exists(dirPath + "/index.html"))
+ return QUrl::fromLocalFile(dirPath + "/index.html");
+ if (! QFileInfo::exists(filesDirString.toString() + "/help/"
+ + QCoreApplication::applicationVersion()
+ + "/C/pentobi/index.html"))
+ // No need to keep files from older versions around
+ QDir(filesDirString.toString() + "/help").removeRecursively();
+ QDirIterator it(":qml/help/" + language + "/pentobi");
+ while (it.hasNext())
+ {
+ it.next();
+ if (! it.fileInfo().isFile())
+ continue;
+ QFile dest(dirPath + "/" + it.fileName());
+ QFileInfo(dest).dir().mkpath(QStringLiteral("."));
+ dest.remove();
+ QFile::copy(it.filePath(), dest.fileName());
+ }
+ auto file = QFileInfo(dirPath + "/index.html").absoluteFilePath();
+ return QUrl::fromLocalFile(file);
+#else
+ Q_UNUSED(language);
+ return {};
+#endif
+}
+
+QUrl AndroidUtils::getDefaultFolder()
+{
+#ifdef Q_OS_ANDROID
+ QUrl fallback(QStringLiteral("file:///sdcard"));
+ auto file = QAndroidJniObject::callStaticObjectMethod(
+ "android/os/Environment", "getExternalStorageDirectory",
+ "()Ljava/io/File;");
+ if (! file.isValid())
+ return fallback;
+ auto fileString = file.callObjectMethod("toString",
+ "()Ljava/lang/String;");
+ if (! fileString.isValid())
+ return fallback;
+ return QUrl::fromLocalFile(fileString.toString());
+#else
+ return QUrl::fromLocalFile(
+ QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
+#endif
+}
+
+#ifdef Q_OS_ANDROID
+float AndroidUtils::getDensity()
+{
+ auto resources = QtAndroid::androidActivity().callObjectMethod(
+ "getResources", "()Landroid/content/res/Resources;");
+ if (! resources.isValid())
+ return 0;
+ auto metrics = resources.callObjectMethod(
+ "getDisplayMetrics", "()Landroid/util/DisplayMetrics;");
+ if (! metrics.isValid())
+ return 0;
+ return metrics.getField<jfloat>("density");
+}
+#endif
+
+void AndroidUtils::scanFile(const QString& pathname)
+{
+#ifdef Q_OS_ANDROID
+ // Corresponding Java code:
+ // sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
+ // Uri.fromFile(File(pathname).getCanonicalFile())));
+ auto action = QAndroidJniObject::getStaticObjectField<jstring>(
+ "android/content/Intent", "ACTION_MEDIA_SCANNER_SCAN_FILE");
+ if (! action.isValid())
+ return;
+ auto pathnameString = QAndroidJniObject::fromString(pathname);
+ QAndroidJniObject file("java/io/File", "(Ljava/lang/String;)V",
+ pathnameString.object<jstring>());
+ if (! file.isValid())
+ return;
+ auto absoluteFile = file.callObjectMethod(
+ "getAbsoluteFile", "()Ljava/io/File;");
+ if (! absoluteFile.isValid())
+ return;
+ auto uri = QAndroidJniObject::callStaticObjectMethod(
+ "android/net/Uri", "fromFile",
+ "(Ljava/io/File;)Landroid/net/Uri;", absoluteFile.object());
+ if (! uri.isValid())
+ return;
+ QAndroidJniObject intent("android/content/Intent",
+ "(Ljava/lang/String;Landroid/net/Uri;)V",
+ action.object<jstring>(), uri.object());
+ if (! intent.isValid())
+ return;
+ QtAndroid::androidActivity().callMethod<void>(
+ "sendBroadcast", "(Landroid/content/Intent;)V",
+ intent.object());
+#else
+ Q_UNUSED(pathname);
+#endif
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/AndroidUtils.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_ANDROID_UTILS_H
+#define PENTOBI_ANDROID_UTILS_H
+
+#include <QObject>
+#include <QUrl>
+
+//-----------------------------------------------------------------------------
+
+class AndroidUtils
+ : public QObject
+{
+ Q_OBJECT
+
+public:
+ using QObject::QObject;
+
+ /** Calls QtAndroid::checkPermission().
+ On platforms other than Android, always returns true. */
+ Q_INVOKABLE static bool checkPermission(const QString& permission);
+
+ Q_INVOKABLE static QUrl extractHelp(const QString& language);
+
+ /** Return a directory for storing files.
+ Avoids a dependency on qt.labs.platform only for StandardPaths and
+ handles Android better. On Android, it returns
+ android.os.Environment.getExternalStorageDirectory(). On other
+ platforms, it returns QStandardPaths::HomeLocation */
+ Q_INVOKABLE static QUrl getDefaultFolder();
+
+ /** Request the Android media scanner to scan a file.
+ Ensures that the file will be visible via MTP. On platforms other
+ than Android, this function does nothing. */
+ Q_INVOKABLE static void scanFile(const QString& pathname);
+
+#ifdef Q_OS_ANDROID
+ /** Return the logical density of the display.
+ Returns android.util.DisplayMetrics.density. This should be the same as
+ Screen.devicePixelRatio, but can be used before QGuiApplication is
+ constructed.
+ @return The density or 0 on error. */
+ static float getDensity();
+#endif
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_ANDROID_UTILS_H
--- /dev/null
+set(CMAKE_AUTOMOC TRUE)
+
+find_package(Threads)
+find_package(Qt5Concurrent 5.11 REQUIRED)
+find_package(Qt5QuickControls2 5.11 REQUIRED)
+find_package(Qt5LinguistTools 5.11 REQUIRED)
+find_package(Qt5Svg 5.11 REQUIRED)
+find_package(Qt5QuickCompiler REQUIRED)
+find_package(Qt5WebView 5.11)
+
+qt5_add_translation(pentobi_QM
+ qml/i18n/qml_de.ts
+ qml/i18n/qml_fr.ts
+ qml/i18n/qml_nb_NO.ts
+ OPTIONS -removeidentical -nounfinished
+ )
+add_custom_command(
+ OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc"
+ COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/qml/i18n/translations.qrc"
+ "${CMAKE_CURRENT_BINARY_DIR}"
+ DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/qml/i18n/translations.qrc" ${pentobi_QM}
+ )
+
+qt5_add_resources(pentobi_RC_SRCS
+ "${CMAKE_CURRENT_BINARY_DIR}/translations.qrc"
+ ../books/pentobi_books.qrc
+ ../icon/pentobi_icon.qrc
+ ../icon/pentobi_icon_desktop.qrc
+ )
+
+qtquick_compiler_add_resources(pentobi_RC_SRCS_QML
+ resources.qrc
+ resources_desktop.qrc
+ qml/themes/themes.qrc
+ )
+
+add_executable(pentobi WIN32
+ ${pentobi_RC_SRCS}
+ ${pentobi_RC_SRCS_QML}
+ AnalyzeGameModel.h
+ AnalyzeGameModel.cpp
+ AndroidUtils.h
+ AndroidUtils.cpp
+ GameModel.h
+ GameModel.cpp
+ ImageProvider.h
+ ImageProvider.cpp
+ Main.cpp
+ PieceModel.h
+ PieceModel.cpp
+ PlayerModel.h
+ PlayerModel.cpp
+ RatingModel.h
+ RatingModel.cpp
+ SyncSettings.h
+ )
+
+file(GLOB qml_SRC "qml/*.qml" "qml/*.js" "qml/i18n/*.ts" "qml/themes/*/*.qml")
+target_sources(pentobi PRIVATE ${qml_SRC})
+
+target_compile_definitions(pentobi PRIVATE
+ QT_DEPRECATED_WARNINGS
+ QT_DISABLE_DEPRECATED_BEFORE=0x051100
+ QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
+ PENTOBI_HELP_DIR="${CMAKE_INSTALL_FULL_DATAROOTDIR}/help"
+ VERSION="${PENTOBI_VERSION}"
+ )
+
+target_link_libraries(pentobi
+ pentobi_paint
+ pentobi_mcts
+ Qt5::Concurrent
+ Qt5::Qml
+ Qt5::QuickControls2
+ Qt5::Svg
+ Threads::Threads
+ )
+
+if (Qt5WebView_FOUND AND NOT PENTOBI_OPEN_HELP_EXTERNALLY)
+ target_link_libraries(pentobi Qt5::WebView)
+else()
+ target_compile_definitions(pentobi PRIVATE PENTOBI_OPEN_HELP_EXTERNALLY)
+endif()
+
+install(TARGETS pentobi DESTINATION ${CMAKE_INSTALL_BINDIR})
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/GameModel.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "GameModel.h"
+
+#include <cerrno>
+#include <cmath>
+#include <cstring>
+#include <fstream>
+#include <QClipboard>
+#include <QGuiApplication>
+#include <QDebug>
+#include <QDir>
+#include <QFileInfo>
+#include <QSettings>
+#include <QTextCodec>
+#include "AndroidUtils.h"
+#include "libboardgame_sgf/SgfUtil.h"
+#include "libboardgame_sgf/TreeReader.h"
+#include "libpentobi_base/MoveMarker.h"
+#include "libpentobi_base/NodeUtil.h"
+#include "libpentobi_base/PentobiTreeWriter.h"
+#include "libpentobi_base/TreeUtil.h"
+
+using namespace std;
+using libboardgame_sgf::SgfError;
+using libboardgame_sgf::TreeReader;
+using libboardgame_sgf::back_to_main_variation;
+using libboardgame_sgf::beginning_of_branch;
+using libboardgame_sgf::find_next_comment;
+using libboardgame_sgf::get_last_node;
+using libboardgame_sgf::has_comment;
+using libboardgame_sgf::has_earlier_variation;
+using libboardgame_sgf::is_main_variation;
+using libboardgame_util::ArrayList;
+using libboardgame_util::get_letter_coord;
+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::has_setup;
+using libpentobi_base::get_move_number;
+using libpentobi_base::get_moves_left;
+using libpentobi_base::get_move_node;
+
+//-----------------------------------------------------------------------------
+
+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() < qreal(0.01);
+}
+
+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);
+}
+
+/** Board uses 4 starting points per Color in GembloQ for technical reasons,
+ GameModel only needs one for displaying the colored dot, shifted to the
+ center of a square. */
+QPointF getGembloQStartingPoint(const Board& bd, Color c)
+{
+ auto p = bd.get_starting_points(c)[0];
+ auto& geo = bd.get_geometry();
+ qreal x = geo.get_x(p);
+ qreal y = geo.get_y(p);
+ if (geo.get_x(p) % 2 == 0)
+ x -= 0.5;
+ else
+ x += 0.5;
+ if (geo.get_y(p) % 2 == 0)
+ y += 0.5;
+ else
+ y -= 0.5;
+ return {x, y};
+}
+
+/** Simple heuristic used for sorting the list used in GameModel::findMove().
+ Prefers larger pieces, and moves of the same piece (in that order). */
+float getHeuristic(const Board& bd, Move mv)
+{
+ auto piece = bd.get_move_piece(mv);
+ auto points = bd.get_piece_info(piece).get_score_points();
+ return Piece::max_pieces * points - piece.to_int();
+}
+
+/** 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;
+ }
+ return nuSiblingMoves != 1;
+}
+
+} //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()),
+ m_nuPlayers(getBoard().get_nu_players())
+{
+ loadRecentFiles();
+ initGame(m_game.get_variant());
+ createPieceModels();
+ updateProperties();
+}
+
+GameModel::~GameModel() = default;
+
+PieceModel* GameModel::addEmpty(const QPoint& pos)
+{
+ if (! checkSetupAllowed())
+ return nullptr;
+ auto move = getMoveAt(pos);
+ if (move.is_null())
+ return nullptr;
+ auto c = move.color;
+ auto mv = move.move;
+ auto& bd = getBoard();
+ LIBBOARDGAME_ASSERT(bd.get_setup().placements[c].contains(mv));
+ auto gameCoord = getGameCoord(bd, mv);
+ PieceModel* result = nullptr;
+ for (auto& variant : qAsConst(m_pieceModels[c]))
+ {
+ auto pieceModel = qvariant_cast<PieceModel*>(variant);
+ if (compareGameCoord(pieceModel->gameCoord(), gameCoord))
+ {
+ result = pieceModel;
+ break;
+ }
+ }
+ preparePositionChange();
+ m_game.remove_setup(c, mv);
+ setSetupPlayer();
+ updateProperties();
+ return result;
+}
+
+void GameModel::addRecentFile(const QString& file)
+{
+ m_recentFiles.removeAll(file);
+ m_recentFiles.prepend(file);
+ while (m_recentFiles.length() > maxRecentFiles)
+ m_recentFiles.removeLast();
+ QSettings settings;
+ settings.setValue(QStringLiteral("recentFiles"), m_recentFiles);
+ emit recentFilesChanged();
+}
+
+void GameModel::addSetup(PieceModel* pieceModel, QPointF coord)
+{
+ if (! checkSetupAllowed())
+ return;
+ Color c(static_cast<Color::IntType>(pieceModel->color()));
+ Move mv;
+ if (! findMove(*pieceModel, pieceModel->state(), coord, mv))
+ return;
+ preparePositionChange();
+ preparePieceGameCoord(pieceModel, mv);
+ pieceModel->setIsPlayed(true);
+ preparePieceTransform(pieceModel, mv);
+ try
+ {
+ m_game.add_setup(c, mv);
+ }
+ catch (const SgfError& error)
+ {
+ m_error = error.what();
+ emit invalidSgfFile();
+ }
+ setSetupPlayer();
+ updateProperties();
+}
+
+void GameModel::autoSave()
+{
+ auto& tree = m_game.get_tree();
+ QSettings settings;
+ settings.setValue(QStringLiteral("variant"),
+ to_string_id(m_game.get_variant()));
+ if (! m_file.isEmpty() && ! m_isModified)
+ settings.remove(QStringLiteral("autosave"));
+ else
+ settings.setValue(QStringLiteral("autosave"), getSgf());
+ settings.setValue(QStringLiteral("file"), m_file);
+ settings.setValue(QStringLiteral("fileDate"), m_fileDate);
+ settings.setValue(QStringLiteral("isModified"), m_isModified);
+ m_autosaveDate = QDateTime::currentDateTime();
+ settings.setValue(QStringLiteral("autosaveDate"), m_autosaveDate);
+ QVariantList location;
+ uint depth = 0;
+ auto node = &m_game.get_current();
+ while (node != &tree.get_root())
+ {
+ auto& parent = node->get_parent();
+ if (parent.get_nu_children() > 1)
+ location.prepend(parent.get_child_index(*node));
+ node = &parent;
+ ++depth;
+ }
+ location.prepend(depth);
+ settings.setValue(QStringLiteral("autosaveLocation"), location);
+}
+
+void GameModel::backToMainVar()
+{
+ gotoNode(back_to_main_variation(m_game.get_current()));
+}
+
+void GameModel::changeGameVariant(const QString& gameVariant)
+{
+ Variant variant;
+ if (! parse_variant_id(gameVariant.toLocal8Bit().constData(), variant))
+ {
+ qWarning("GameModel: invalid game variant");
+ return;
+ }
+ initGameVariant(variant);
+ setIsModified(false);
+ clearFile();
+}
+
+bool GameModel::checkAutosaveModifiedOutside()
+{
+ QSettings settings;
+ auto autosaveDate =
+ settings.value(QStringLiteral("autosaveDate")).toDateTime();
+ return m_autosaveDate.isValid() && autosaveDate.isValid()
+ && m_autosaveDate != autosaveDate
+ && settings.value(QStringLiteral("isModified")).toBool()
+ && settings.value(QStringLiteral("autosave")).toByteArray() != getSgf();
+}
+
+bool GameModel::checkFileExists(const QString& file)
+{
+ return QFileInfo::exists(file);
+}
+
+bool GameModel::checkFileModifiedOutside()
+{
+ if (m_file.isEmpty() || ! m_fileDate.isValid())
+ return false;
+ QFileInfo fileInfo(m_file);
+ if (! fileInfo.exists())
+ return false;
+ return fileInfo.lastModified() != m_fileDate;
+}
+
+/** Check if setup is allowed in the current position.
+ Currently, we support 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).*/
+bool GameModel::checkSetupAllowed() const
+{
+ return ! m_canGoBackward && ! m_canGoForward && m_moveNumber == 0;
+}
+
+void GameModel::clearFile()
+{
+ if (m_file.isEmpty())
+ return;
+ m_file.clear();
+ emit fileChanged();
+}
+
+void GameModel::clearRecentFiles()
+{
+ m_recentFiles.clear();
+ QSettings settings;
+ settings.setValue(QStringLiteral("recentFiles"), m_recentFiles);
+ emit recentFilesChanged();
+}
+
+bool GameModel::createFolder(const QUrl& folder)
+{
+ auto localFolder = folder.toLocalFile();
+ if (! QDir().mkdir(localFolder)) {
+ m_error = QString::fromLocal8Bit(strerror(errno));
+ return false;
+ }
+ AndroidUtils::scanFile(localFolder);
+ return true;
+}
+
+void GameModel::createPieceModels()
+{
+ createPieceModels(Color(0));
+ createPieceModels(Color(1));
+ if (m_nuColors > 2)
+ createPieceModels(Color(2));
+ else
+ m_pieceModels[Color(2)].clear();
+ if (m_nuColors > 3)
+ createPieceModels(Color(3));
+ else
+ m_pieceModels[Color(3)].clear();
+}
+
+void GameModel::createPieceModels(Color c)
+{
+ auto& bd = getBoard();
+ auto nuPieces = bd.get_nu_uniq_pieces();
+ m_pieceModels[c].clear();
+ m_pieceModels[c].reserve(nuPieces);
+ for (Piece::IntType i = 0; i < nuPieces; ++i)
+ {
+ Piece piece(i);
+ auto nuInstances = bd.get_piece_info(piece).get_nu_instances();
+ for (unsigned j = 0; j < nuInstances; ++j)
+ {
+ auto variant =
+ QVariant::fromValue(new PieceModel(this, bd, piece, c));
+ m_pieceModels[c].append(variant);
+ }
+ }
+}
+
+QString GameModel::decode(const string& s) const
+{
+ return m_textCodec->toUnicode(s.c_str());
+}
+
+void GameModel::deleteAllVar()
+{
+ if (! is_main_variation(m_game.get_current()))
+ preparePositionChange();
+ m_game.delete_all_variations();
+ updateProperties();
+}
+
+
+QByteArray GameModel::encode(const QString& s) const
+{
+ return m_textCodec->fromUnicode(s);
+}
+
+GameMove* GameModel::findMoveNext()
+{
+ prepareFindMove();
+ if (m_legalMoves->empty())
+ return nullptr;
+ auto i = m_legalMoveIndex >= m_legalMoves->size() ? 0 : m_legalMoveIndex;
+ auto mv = (*m_legalMoves)[i];
+ m_legalMoveIndex = i + 1;
+ return new GameMove(this, ColorMove(getBoard().get_to_play(), mv));
+}
+
+GameMove* GameModel::findMovePrevious()
+{
+ prepareFindMove();
+ if (m_legalMoves->empty())
+ return nullptr;
+ auto i = m_legalMoveIndex > 1 ? m_legalMoveIndex - 2
+ : m_legalMoves->size() - 1;
+ auto mv = (*m_legalMoves)[i];
+ m_legalMoveIndex = i + 1;
+ return new GameMove(this, ColorMove(getBoard().get_to_play(), mv));
+}
+
+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());
+ 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);
+ auto pointType = transform->get_point_type();
+ auto dx = coord.x() - center.x();
+ auto dy = coord.y() - center.y();
+ int offX;
+ if (bd.get_piece_set() == PieceSet::gembloq)
+ {
+ // In GembloQ, every piece has at least one full square, so we can use
+ // half the x resolution, which makes positioning easier for the user.
+ if (pointType == 0 || pointType == 2)
+ offX = static_cast<int>(round(dx * qreal(0.5))) * 2;
+ else
+ offX = static_cast<int>(round((dx - 1) * qreal(0.5))) * 2 + 1;
+ }
+ else
+ offX = static_cast<int>(round(dx));
+ auto offY = static_cast<int>(round(dy));
+ auto& geo = bd.get_geometry();
+ if (geo.get_point_type(offX, offY) != pointType)
+ return false;
+ MovePoints points;
+ for (auto& p : piecePoints)
+ {
+ int x = p.x + offX;
+ int y = p.y + offY;
+ if (! geo.is_onboard(x, y))
+ return false;
+ points.push_back(geo.get_point(x, y));
+ }
+ return bd.find_move(points, piece, mv);
+}
+
+bool GameModel::findNextComment()
+{
+ auto node = find_next_comment(m_game.get_current());
+ if (! node)
+ return false;
+ gotoNode(*node);
+ return true;
+}
+
+bool GameModel::findNextCommentContinueFromRoot()
+{
+ auto node = &m_game.get_root();
+ if (! has_comment(*node))
+ node = find_next_comment(*node);
+ if (! node)
+ return false;
+ gotoNode(*node);
+ return true;
+}
+
+PieceModel* GameModel::findUnplayedPieceModel(Color c, Piece piece)
+{
+ for (auto& variant : qAsConst(m_pieceModels[c]))
+ {
+ auto pieceModel = qvariant_cast<PieceModel*>(variant);
+ if (pieceModel->getPiece() == piece && ! pieceModel->isPlayed())
+ return pieceModel;
+ }
+ return nullptr;
+}
+
+QVariantList GameModel::getPieceModels(int color)
+{
+ if (color >= 0 && color <= static_cast<int>(Color::range))
+ return m_pieceModels[Color(static_cast<Color::IntType>(color))];
+ return m_pieceModels[Color(0)];
+}
+
+QString GameModel::getPlayerString(int player)
+{
+ auto variant = m_game.get_variant();
+ bool isMulticolor = (m_nuColors > m_nuPlayers && variant != Variant::classic_3);
+ switch (player) {
+ case 0:
+ if (isMulticolor)
+ return tr("Blue/Red");
+ else if (variant == Variant::duo)
+ return tr("Purple");
+ else if (variant == Variant::junior)
+ return tr("Green");
+ else
+ return tr("Blue");
+ case 1:
+ if (isMulticolor)
+ return tr("Yellow/Green");
+ else if (variant == Variant::duo || variant == Variant::junior)
+ return tr("Orange");
+ else if (m_nuColors == 2)
+ return tr("Green");
+ else
+ return tr("Yellow");
+ case 2:
+ return tr("Red");
+ case 3:
+ return tr("Green");
+ }
+ return {};
+}
+
+Variant GameModel::getInitialGameVariant()
+{
+ QSettings settings;
+ auto variantString = settings.value(QStringLiteral("variant")).toString();
+ Variant variant;
+ if (! parse_variant_id(variantString.toLocal8Bit().constData(), variant))
+ variant = Variant::duo;
+ return variant;
+}
+
+QString GameModel::getMoveAnnotation(int moveNumber)
+{
+ if (moveNumber <= 0)
+ return {};
+ auto node = get_move_node(m_game.get_tree(), m_game.get_current(),
+ static_cast<unsigned>(moveNumber));
+ if (node == nullptr)
+ return {};
+ return getMoveAnnotationAtNode(*node);
+}
+
+QString GameModel::getMoveAnnotationAtNode(const SgfNode& node) const
+{
+ try
+ {
+ if (m_game.get_good_move(node) == 2)
+ return QStringLiteral("‼");
+ if (m_game.get_good_move(node) == 1)
+ return QStringLiteral("!");
+ if (m_game.is_interesting_move(node))
+ return QStringLiteral("⁉");
+ if (m_game.is_doubtful_move(node))
+ return QStringLiteral("⁈");
+ if (m_game.get_bad_move(node) == 1)
+ return QStringLiteral("?");
+ if (m_game.get_bad_move(node) == 2)
+ return QStringLiteral("⁇");
+ }
+ catch (const SgfError&)
+ {
+ // Silently ignore GM, BM properties with invalid value
+ }
+ return {};
+}
+
+ColorMove GameModel::getMoveAt(const QPoint& pos) const
+{
+ auto& bd = getBoard();
+ auto& geo = bd.get_geometry();
+ if (pos.x() < 0 || pos.y() < 0)
+ return ColorMove::null();
+ if (! geo.is_onboard(pos.x(), pos.y()))
+ return ColorMove::null();
+ auto p = geo.get_point(static_cast<unsigned>(pos.x()),
+ static_cast<unsigned>(pos.y()));
+ auto s = bd.get_point_state(p);
+ if (s.is_empty())
+ return ColorMove::null();
+ auto c = s.to_color();
+ auto mv = bd.get_move_at(p);
+ return {c, mv};
+}
+
+int GameModel::getMoveNumberAt(const QPoint& pos)
+{
+ auto move = getMoveAt(pos);
+ if (move.is_null())
+ return -1;
+ auto n = m_moveNumber;
+ auto& tree = m_game.get_tree();
+ auto node = &m_game.get_current();
+ do
+ {
+ if (tree.has_move(*node))
+ {
+ if (tree.get_move(*node) == move)
+ return n;
+ --n;
+ }
+ node = node->get_parent_or_null();
+ }
+ while (node != nullptr);
+ return -1;
+}
+
+QString GameModel::getResultMessage()
+{
+ auto& bd = getBoard();
+ auto variant = bd.get_variant();
+ if (variant == Variant::duo)
+ {
+ auto score = static_cast<double>(m_points0 - m_points1);
+ if (score == 1)
+ return tr("Purple wins with 1 point.");
+ if (score > 0)
+ return tr("Purple wins with %L1 points.").arg(score);
+ if (score == -1)
+ return tr("Orange wins with 1 point.");
+ if (score < 0)
+ return tr("Orange wins with %L1 points.").arg(-score);
+ return tr("Game ends in a tie.");
+ }
+ if (variant == Variant::junior)
+ {
+ auto score = static_cast<double>(m_points0 - m_points1);
+ if (score == 1)
+ return tr("Green wins with 1 point.");
+ if (score > 0)
+ return tr("Green wins with %L1 points.").arg(score);
+ if (score == -1)
+ return tr("Orange wins with 1 point.");
+ if (score < 0)
+ return tr("Orange wins with %L1 points.").arg(-score);
+ return tr("Game ends in a tie.");
+ }
+ bool breakTies = (bd.get_piece_set() == PieceSet::callisto);
+ if (m_nuColors == 2)
+ {
+ auto score = static_cast<double>(m_points0 - m_points1);
+ if (score == 1)
+ return tr("Blue wins with 1 point.");
+ if (score > 0)
+ return tr("Blue wins with %L1 points.").arg(score);
+ if (score == -1)
+ return tr("Green wins with 1 point.");
+ if (score < 0)
+ return tr("Green wins with %L1 points.").arg(-score);
+ if (breakTies)
+ //: Game variant with tie-breaker rule made later player win.
+ return tr("Green wins (tie resolved).");
+ return tr("Game ends in a tie.");
+ }
+ if (m_nuColors == 4 && m_nuPlayers == 2)
+ {
+ auto score = static_cast<double>(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 %L1 points.").arg(score);
+ if (score == -1)
+ return tr("Yellow/Green wins with 1 point.");
+ if (score < 0)
+ return tr("Yellow/Green wins with %L1 points.").arg(-score);
+ if (breakTies)
+ //: Game variant with tie-breaker rule made later player win.
+ return tr("Yellow/Green wins (tie resolved).");
+ return tr("Game ends in a tie.");
+ }
+ if (m_nuPlayers == 3)
+ {
+ auto maxPoints = 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)
+ //: Game variant with tie-breaker rule made later player win.
+ return tr("Red wins (tie resolved).");
+ if (m_points1 == maxPoints && breakTies)
+ //: Game variant with tie-breaker rule made later player win.
+ 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({m_points0, m_points1, 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)
+ //: Game variant with tie-breaker rule made later player win.
+ return tr("Green wins (tie resolved).");
+ if (m_points2 == maxPoints && breakTies)
+ //: Game variant with tie-breaker rule made later player win.
+ return tr("Red wins (tie resolved).");
+ if (m_points1 == maxPoints && breakTies)
+ //: Game variant with tie-breaker rule made later player win.
+ 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.");
+}
+
+QByteArray GameModel::getSgf() const
+{
+ auto& tree = m_game.get_tree();
+ ostringstream s;
+ PentobiTreeWriter writer(s, tree);
+ writer.set_indent(-1);
+ writer.write();
+ return QByteArray(s.str().c_str());
+}
+
+QString GameModel::getVariationInfo() const
+{
+ auto moveNumber = getBoard().get_nu_moves();
+ QString s = QString::number(moveNumber);
+ unsigned moveIndex;
+ if (getVariationIndex(m_game.get_tree(), m_game.get_current(), moveIndex))
+ s.append(get_letter_coord(moveIndex).c_str());
+ return s;
+}
+
+void GameModel::goBackward()
+{
+ gotoNode(m_game.get_current().get_parent_or_null());
+}
+
+void GameModel::goBackward10()
+{
+ auto node = &m_game.get_current();
+ for (unsigned i = 0; i < 10; ++i)
+ {
+ auto parent = node->get_parent_or_null();
+ if (parent == nullptr)
+ break;
+ node = parent;
+ }
+ gotoNode(node);
+}
+
+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::goForward10()
+{
+ auto node = &m_game.get_current();
+ for (unsigned i = 0; i < 10; ++i)
+ {
+ auto child = node->get_first_child_or_null();
+ if (child == nullptr)
+ break;
+ node = child;
+ }
+ gotoNode(node);
+}
+
+void GameModel::goNextVar()
+{
+ gotoNode(m_game.get_current().get_sibling());
+}
+
+void GameModel::goPrevVar()
+{
+ gotoNode(m_game.get_current().get_previous_sibling());
+}
+
+void GameModel::gotoBeginningOfBranch()
+{
+ gotoNode(beginning_of_branch(m_game.get_current()));
+}
+
+void GameModel::gotoMove(int n)
+{
+ if (n == 0)
+ goBeginning();
+ else if (n > 0)
+ gotoNode(get_move_node(m_game.get_tree(), m_game.get_current(),
+ static_cast<unsigned>(n)));
+}
+
+void GameModel::gotoNode(const SgfNode& node)
+{
+ if (&node == &m_game.get_current())
+ return;
+ preparePositionChange();
+ try
+ {
+ m_game.goto_node(node);
+ }
+ catch (const SgfError& error)
+ {
+ m_error = error.what();
+ emit invalidSgfFile();
+ }
+ updateProperties();
+}
+
+void GameModel::gotoNode(const SgfNode* node)
+{
+ if (node)
+ gotoNode(*node);
+}
+
+void GameModel::initGame(Variant variant)
+{
+ m_game.init(variant);
+#ifdef VERSION
+ m_game.set_application("Pentobi", VERSION);
+#else
+ m_game.set_application("Pentobi");
+#endif
+ m_game.set_date_today();
+ setUtf8();
+ updateGameInfo();
+}
+
+void GameModel::initGameVariant(Variant variant)
+{
+ if (m_game.get_variant() != variant)
+ initGame(variant);
+ auto& bd = getBoard();
+ set(m_nuColors, static_cast<unsigned>(bd.get_nu_colors()),
+ &GameModel::nuColorsChanged);
+ set(m_nuPlayers, bd.get_nu_players(), &GameModel::nuPlayersChanged);
+ m_lastMovePieceModel = nullptr;
+ createPieceModels();
+ m_gameVariant = to_string_id(variant);
+ emit gameVariantChanged();
+ 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()));
+ return getBoard().is_legal(c, mv);
+}
+
+bool GameModel::isLegalSetupPos(PieceModel* pieceModel, const QString& state,
+ QPointF coord) const
+{
+ Move mv;
+ if (! findMove(*pieceModel, state, coord, mv))
+ return false;
+ auto& bd = getBoard();
+ for (auto p : bd.get_move_points(mv))
+ if (! bd.get_point_state(p).is_empty())
+ return false;
+ return true;
+}
+
+void GameModel::keepOnlyPosition()
+{
+ m_game.keep_only_position();
+ updateProperties();
+}
+
+void GameModel::keepOnlySubtree()
+{
+ m_game.keep_only_subtree();
+ updateProperties();
+}
+
+bool GameModel::loadAutoSave()
+{
+ QSettings settings;
+ auto file = settings.value(QStringLiteral("file")).toString();
+ auto isModified = settings.value(QStringLiteral("isModified")).toBool();
+ if (! file.isEmpty() && ! isModified)
+ {
+ if (! checkFileExists(file) || ! openFile(file))
+ return false;
+ updateFileInfo(file);
+ m_autosaveDate = m_fileDate;
+ settings.setValue(QStringLiteral("autosaveDate"), m_autosaveDate);
+ }
+ else
+ {
+ if (! openByteArray(
+ settings.value(QStringLiteral("autosave")).toByteArray()))
+ return false;
+ m_fileDate = settings.value(QStringLiteral("fileDate")).toDateTime();
+ m_autosaveDate =
+ settings.value(QStringLiteral("autosaveDate")).toDateTime();
+ setFile(file);
+ }
+ setIsModified(isModified);
+ restoreAutoSaveLocation();
+ updateProperties();
+ return true;
+}
+
+void GameModel::loadRecentFiles()
+{
+ QSettings settings;
+ m_recentFiles =
+ settings.value(QStringLiteral("recentFiles")).toStringList();
+ QMutableListIterator<QString> i(m_recentFiles);
+ while (i.hasNext()) {
+ auto file = i.next();
+ if (file.isEmpty() || ! QFileInfo::exists(file))
+ i.remove();
+ }
+ while (m_recentFiles.length() > maxRecentFiles)
+ m_recentFiles.removeLast();
+ emit recentFilesChanged();
+}
+
+bool GameModel::openByteArray(const QByteArray& byteArray)
+{
+ istringstream in(byteArray.constData());
+ clearFile();
+ if (! openStream(in))
+ return false;
+ goEnd();
+ 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()
+{
+ preparePositionChange();
+ auto& bd = getBoard();
+ m_game.set_to_play(bd.get_next(bd.get_to_play()));
+ setSetupPlayer();
+ updateProperties();
+}
+
+PieceModel* GameModel::nextPiece(PieceModel* currentPickedPiece)
+{
+ auto& bd = getBoard();
+ auto c = bd.get_to_play();
+ if (bd.get_pieces_left(c).empty())
+ return nullptr;
+ auto nuUniqPieces = bd.get_nu_uniq_pieces();
+ Piece::IntType i;
+ if (currentPickedPiece != nullptr)
+ i = static_cast<Piece::IntType>(
+ currentPickedPiece->getPiece().to_int() + 1);
+ else
+ i = 0;
+ while (true)
+ {
+ if (i >= nuUniqPieces)
+ i = 0;
+ if (bd.is_piece_left(c, Piece(i)))
+ break;
+ ++i;
+ }
+ return findUnplayedPieceModel(c, Piece(i));
+}
+
+void GameModel::newGame()
+{
+ preparePositionChange();
+ initGame(m_game.get_variant());
+ setIsModified(false);
+ clearFile();
+ for (Color c : Color::Range(Color::range))
+ for (auto& variant : qAsConst(m_pieceModels[c]))
+ {
+ auto pieceModel = qvariant_cast<PieceModel*>(variant);
+ pieceModel->setDefaultState();
+ }
+ updateProperties();
+}
+
+bool GameModel::openStream(istream& in)
+{
+ bool result = true;
+ try
+ {
+ preparePositionChange();
+ TreeReader reader;
+ reader.read(in);
+ auto root = reader.get_tree_transfer_ownership();
+ m_game.init(root);
+ }
+ catch (const runtime_error& e)
+ {
+ m_error =
+ tr("Invalid Blokus SGF file. (%1)")
+ .arg(QString::fromLocal8Bit(e.what()));
+ result = false;
+ }
+ auto charSet = m_game.get_charset();
+ if (charSet.empty())
+ m_textCodec = QTextCodec::codecForName("ISO 8859-1");
+ else
+ m_textCodec = QTextCodec::codecForName(m_game.get_charset().c_str());
+ if (! m_textCodec)
+ {
+ m_textCodec = QTextCodec::codecForName("ISO 8859-1");
+ m_error = tr("Unsupported character set");
+ result = false;
+ }
+ if (! result)
+ m_game.init();
+ auto variant = to_string_id(m_game.get_variant());
+ if (variant != m_gameVariant)
+ initGameVariant(m_game.get_variant());
+ setIsModified(false);
+ updateGameInfo();
+ updateProperties();
+ return result;
+}
+
+bool GameModel::openFile(const QString& file)
+{
+ auto absoluteFile = QFileInfo(file).absoluteFilePath();
+ ifstream in(absoluteFile.toLocal8Bit().constData());
+ if (! in)
+ {
+ m_error = QString::fromLocal8Bit(strerror(errno));
+ return false;
+ }
+ if (openStream(in))
+ {
+ updateFileInfo(absoluteFile);
+ addRecentFile(absoluteFile);
+ auto& root = m_game.get_root();
+ // Show end of game position by default unless the root node has
+ // setup stones or comments, because then it might be a puzzle and
+ // we don't want to show the solution.
+ if (! has_setup(root) && ! has_comment(root) && root.has_children())
+ goEnd();
+ return true;
+ }
+ clearFile();
+ return false;
+}
+
+bool GameModel::openClipboard()
+{
+ auto text = QGuiApplication::clipboard()->text();
+ if (text.isEmpty())
+ {
+ m_error = tr("Clipboard is empty.");
+ return false;
+ }
+ istringstream in(text.toLocal8Bit().constData());
+ if (openStream(in))
+ {
+ auto& root = m_game.get_root();
+ if (! has_setup(root) && root.has_children())
+ goEnd();
+ return true;
+ }
+ clearFile();
+ return false;
+}
+
+PieceModel* GameModel::pickNamedPiece(const QString& name,
+ PieceModel* currentPickedPiece)
+{
+ string nameStr(name.toLocal8Bit().constData());
+ auto& bd = getBoard();
+ auto c = bd.get_to_play();
+ Board::PiecesLeftList pieces;
+ for (Piece::IntType i = 0; i < bd.get_nu_uniq_pieces(); ++i)
+ {
+ Piece piece(i);
+ if (bd.is_piece_left(c, piece)
+ && bd.get_piece_info(piece).get_name().find(nameStr) == 0)
+ pieces.push_back(piece);
+ }
+ if (pieces.empty())
+ return nullptr;
+ Piece piece;
+ if (currentPickedPiece == nullptr)
+ piece = pieces[0];
+ else
+ {
+ piece = currentPickedPiece->getPiece();
+ 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;
+ }
+ }
+ return findUnplayedPieceModel(c, piece);
+}
+
+void GameModel::playMove(GameMove* move)
+{
+ auto mv = move->get();
+ if (mv.is_null())
+ return;
+ preparePositionChange();
+ m_game.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;
+ }
+ preparePositionChange();
+ preparePieceGameCoord(pieceModel, mv);
+ pieceModel->setIsPlayed(true);
+ preparePieceTransform(pieceModel, mv);
+ m_game.play(c, mv, false);
+ updateProperties();
+}
+
+void GameModel::prepareFindMove()
+{
+ auto& bd = getBoard();
+ auto c = bd.get_to_play();
+ if (! m_legalMoves)
+ m_legalMoves = make_unique<MoveList>();
+ if (m_legalMoves->empty())
+ {
+ if (! m_marker)
+ m_marker = make_unique<MoveMarker>();
+ bd.gen_moves(c, *m_marker, *m_legalMoves);
+ m_marker->clear(*m_legalMoves);
+ sort(m_legalMoves->begin(), m_legalMoves->end(),
+ [&](Move mv1, Move mv2) {
+ return getHeuristic(bd, mv1) > getHeuristic(bd, mv2);
+ });
+ m_legalMoveIndex = 0;
+ }
+}
+
+PieceModel* GameModel::preparePiece(GameMove* move)
+{
+ if (move == nullptr || move->get().is_null())
+ return nullptr;
+ auto c = move->get().color;
+ auto mv = move->get().move;
+ auto piece = getBoard().get_move_piece(mv);
+ for (auto& variant : qAsConst(m_pieceModels[c]))
+ {
+ auto pieceModel = qvariant_cast<PieceModel*>(variant);
+ 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);
+}
+
+void GameModel::preparePositionChange()
+{
+ if (m_legalMoves)
+ {
+ m_legalMoves->clear();
+ m_legalMoveIndex = 0;
+ }
+ emit positionAboutToChange();
+}
+
+PieceModel* GameModel::previousPiece(PieceModel* currentPickedPiece)
+{
+ auto& bd = getBoard();
+ auto c = bd.get_to_play();
+ if (bd.get_pieces_left(c).empty())
+ return nullptr;
+ auto nuUniqPieces = bd.get_nu_uniq_pieces();
+ Piece::IntType i;
+ if (currentPickedPiece != nullptr)
+ i = static_cast<Piece::IntType>(currentPickedPiece->getPiece().to_int());
+ else
+ i = 0;
+ while (true)
+ {
+ if (i == 0)
+ i = static_cast<Piece::IntType>(nuUniqPieces - 1);
+ else
+ --i;
+ if (bd.is_piece_left(c, Piece(i)))
+ break;
+ }
+ return findUnplayedPieceModel(c, Piece(i));
+}
+
+void GameModel::restoreAutoSaveLocation()
+{
+ QSettings settings;
+ auto location =
+ settings.value(QStringLiteral("autosaveLocation")).value<QVariantList>();
+ if (location.empty())
+ return;
+ int index = 0;
+ bool ok;
+ auto depth = location[index++].toUInt(&ok);
+ if (! ok)
+ return;
+ auto node = &m_game.get_root();
+ while (depth > 0)
+ {
+ auto nuChildren = node->get_nu_children();
+ if (nuChildren == 0)
+ break;
+ if (nuChildren == 1)
+ node = &node->get_first_child();
+ else
+ {
+ if (index >= location.size())
+ break;
+ auto child = location[index++].toUInt(&ok);
+ if (! ok || child >= nuChildren)
+ break;
+ node = &node->get_child(child);
+ }
+ --depth;
+ }
+ gotoNode(*node);
+}
+
+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_error = QString::fromLocal8Bit(strerror(errno));
+ return false;
+ }
+ }
+ AndroidUtils::scanFile(file);
+ updateFileInfo(file);
+ setIsModified(false);
+ addRecentFile(file);
+ return true;
+}
+
+bool GameModel::saveAsciiArt(const QString& file)
+{
+ ofstream out(file.toLocal8Bit().constData());
+ getBoard().write(out, false);
+ if (! out)
+ {
+ m_error = QString::fromLocal8Bit(strerror(errno));
+ return false;
+ }
+ AndroidUtils::scanFile(file);
+ return true;
+}
+
+template<typename T>
+bool GameModel::set(T& target, const T& value,
+ void (GameModel::*changedSignal)())
+{
+ if (target != value)
+ {
+ target = value;
+ emit (this->*changedSignal)();
+ return true;
+ }
+ return false;
+}
+
+void GameModel::setComment(const QString& comment)
+{
+ if (comment == m_comment)
+ return;
+ m_game.set_comment(encode(comment).constData());
+ m_comment = comment;
+ emit commentChanged();
+ updateIsModified();
+}
+
+void GameModel::setDate(const QString& date)
+{
+ if (date == m_date)
+ return;
+ m_date = date;
+ m_game.set_date(encode(date).constData());
+ emit dateChanged();
+ updateIsModified();
+}
+
+void GameModel::setEvent(const QString& event)
+{
+ if (event == m_event)
+ return;
+ m_event = event;
+ m_game.set_event(encode(event).constData());
+ emit eventChanged();
+ updateIsModified();
+}
+
+void GameModel::setFile(const QString& file)
+{
+ if (file == m_file)
+ return;
+ m_file = file;
+ emit fileChanged();
+}
+
+void GameModel::setIsModified(bool isModified)
+{
+ m_game.set_modified(isModified);
+ updateIsModified();
+}
+
+void GameModel::setMoveAnnotationAtNode(const SgfNode& node,
+ const QString& annotation)
+{
+ m_game.remove_move_annotation(node);
+ if (annotation == QStringLiteral("!"))
+ m_game.set_good_move(node);
+ else if (annotation == QStringLiteral("‼"))
+ m_game.set_good_move(node, 2);
+ else if (annotation == QStringLiteral("?"))
+ m_game.set_bad_move(node);
+ else if (annotation == QStringLiteral("⁇"))
+ m_game.set_bad_move(node, 2);
+ else if (annotation == QStringLiteral("⁉"))
+ m_game.set_interesting_move(node);
+ else if (annotation == QStringLiteral("⁈"))
+ m_game.set_doubtful_move(node);
+ updateIsModified();
+ updatePositionInfo();
+ updatePieces();
+}
+
+void GameModel::setMoveAnnotation(int moveNumber, const QString& annotation)
+{
+ if (moveNumber <= 0)
+ return;
+ auto node = get_move_node(m_game.get_tree(), m_game.get_current(),
+ static_cast<unsigned>(moveNumber));
+ if (node == nullptr)
+ return;
+ setMoveAnnotationAtNode(*node, annotation);
+}
+
+void GameModel::setPlayerName0(const QString& name)
+{
+ if (name == m_playerName0)
+ return;
+ m_playerName0 = name;
+ m_game.set_player_name(Color(0), encode(name).constData());
+ emit playerName0Changed();
+ updateIsModified();
+}
+
+void GameModel::setPlayerName1(const QString& name)
+{
+ if (name == m_playerName1)
+ return;
+ m_playerName1 = name;
+ m_game.set_player_name(Color(1), encode(name).constData());
+ emit playerName1Changed();
+ updateIsModified();
+}
+
+void GameModel::setPlayerName2(const QString& name)
+{
+ if (name == m_playerName2)
+ return;
+ m_playerName2 = name;
+ m_game.set_player_name(Color(2), encode(name).constData());
+ emit playerName2Changed();
+ updateIsModified();
+}
+
+void GameModel::setPlayerName3(const QString& name)
+{
+ if (name == m_playerName3)
+ return;
+ m_playerName3 = name;
+ m_game.set_player_name(Color(3), encode(name).constData());
+ emit playerName3Changed();
+ updateIsModified();
+}
+
+void GameModel::setRound(const QString& round)
+{
+ if (round == m_round)
+ return;
+ m_round = round;
+ m_game.set_round(encode(round).constData());
+ emit roundChanged();
+ updateIsModified();
+}
+
+void GameModel::setShowVariations(bool showVariations)
+{
+ if (set(m_showVariations, showVariations, &GameModel::showVariationsChanged))
+ updatePieces();
+}
+
+void GameModel::setSetupPlayer()
+{
+ if (! m_game.has_setup())
+ m_game.remove_player();
+ else
+ m_game.set_player(getBoard().get_to_play());
+}
+
+void GameModel::setTime(const QString& time)
+{
+ if (time == m_time)
+ return;
+ m_time = time;
+ m_game.set_time(encode(time).constData());
+ emit playerName3Changed();
+ updateIsModified();
+}
+
+void GameModel::setUtf8()
+{
+ m_game.set_charset("UTF-8");
+ m_textCodec = QTextCodec::codecForName("UTF-8");
+}
+
+QString GameModel::suggestFileName(const QUrl& folder,
+ const QString& fileEnding)
+{
+ QString suffix =
+ ! fileEnding.isEmpty()
+ && ! fileEnding.startsWith(QStringLiteral(".")) ?
+ QStringLiteral(".") + fileEnding : fileEnding;
+ auto localFolder = folder.toLocalFile();
+ QString file = localFolder + '/' + tr("Untitled") + suffix;
+ if (QFileInfo::exists(file))
+ for (unsigned i = 1; ; ++i)
+ {
+ //: The argument is a number, which will be increased if a
+ //: file with the same name already exists
+ file = localFolder + '/' + tr("Untitled %1").arg(i)
+ + suffix;
+ if (! QFileInfo::exists(file))
+ break;
+ }
+ return QUrl::fromLocalFile(file).fileName();
+}
+
+QString GameModel::suggestGameFileName(const QUrl& folder)
+{
+ if (! m_file.isEmpty())
+ return QUrl::fromLocalFile(m_file).fileName();
+ return suggestFileName(folder, QStringLiteral("blksgf"));
+}
+
+QString GameModel::suggestNewFolderName(const QUrl& folder)
+{
+ auto localFolder = folder.toLocalFile();
+ QString file = localFolder;
+ if (! file.endsWith('/'))
+ file.append('/');
+ file.append(tr("New Folder"));
+ if (QFileInfo::exists(file))
+ for (unsigned i = 1; ; ++i)
+ {
+ //: The argument is a number, which will be increased if a
+ //: folder with the same name already exists
+ file = localFolder + '/' + tr("New Folder %1").arg(i);
+ if (! QFileInfo::exists(file))
+ break;
+ }
+ return QUrl::fromLocalFile(file).fileName();
+}
+
+void GameModel::truncate()
+{
+ if (! m_game.get_current().has_parent())
+ return;
+ preparePositionChange();
+ m_game.truncate();
+ updateProperties();
+}
+
+void GameModel::truncateChildren()
+{
+ m_game.truncate_children();
+ updateProperties();
+}
+
+void GameModel::undo()
+{
+ if (! m_canUndo)
+ return;
+ preparePositionChange();
+ m_game.undo();
+ updateProperties();
+}
+
+void GameModel::updateFileInfo(const QString& file)
+{
+ setFile(file);
+ m_fileDate = QFileInfo(file).lastModified();
+}
+
+void GameModel::updateGameInfo()
+{
+ static_assert(Color::range == 4, "");
+ setPlayerName0(decode(m_game.get_player_name(Color(0))));
+ setPlayerName1(decode(m_game.get_player_name(Color(1))));
+ if (m_nuPlayers > 2)
+ setPlayerName2(decode(m_game.get_player_name(Color(2))));
+ if (m_nuPlayers > 3)
+ setPlayerName3(decode(m_game.get_player_name(Color(3))));
+ setDate(decode(m_game.get_date()));
+ setTime(decode(m_game.get_time()));
+ setEvent(decode(m_game.get_event()));
+ setRound(decode(m_game.get_round()));
+}
+
+void GameModel::updateIsModified()
+{
+ // Don't consider modified game tree as modified if it is empty and no
+ // file is associated.
+ bool isModified =
+ m_game.is_modified()
+ && (! libboardgame_sgf::is_empty(m_game.get_tree())
+ || ! m_file.isEmpty());
+ set(m_isModified, isModified, &GameModel::isModifiedChanged);
+}
+
+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 = m_pieceModels[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)
+ {
+ auto pieceModel = qvariant_cast<PieceModel*>(pieceModels[i]);
+ if (pieceModel->getPiece() == piece
+ && pieceModel->isPlayed()
+ && compareGameCoord(pieceModel->gameCoord(), gameCoord)
+ && compareTransform(pieceInfo, pieceModel->getTransform(),
+ transform))
+ {
+ isPlayed[i] = true;
+ return pieceModel;
+ }
+ }
+ for (int i = 0; i < pieceModels.length(); ++i)
+ {
+ auto pieceModel = qvariant_cast<PieceModel*>(pieceModels[i]);
+ if (pieceModel->getPiece() == piece && ! isPlayed[i])
+ {
+ isPlayed[i] = true;
+ // Set PieceModel.isPlayed temporarily to false, such that there is
+ // always a state transition animation (e.g. if the piece stays
+ // on the board but changes its coordinates when navigating through
+ // move variations).
+ pieceModel->setIsPlayed(false);
+ // Set gameCoord before isPlayed because the animation needs it.
+ pieceModel->setGameCoord(gameCoord);
+ pieceModel->setIsPlayed(true);
+ pieceModel->setTransform(transform);
+ return pieceModel;
+ }
+ }
+ LIBBOARDGAME_ASSERT(false);
+ return nullptr;
+}
+
+void GameModel::updatePieces()
+{
+ auto& bd = getBoard();
+ ColorMap<array<bool, Board::max_pieces>> isPlayed;
+
+ // Update pieces of setup
+ for (Color c : bd.get_colors())
+ {
+ isPlayed[c].fill(false);
+ for (Move mv : bd.get_setup().placements[c])
+ {
+ auto pieceModel = updatePiece(c, mv, isPlayed[c]);
+ pieceModel->setMoveLabel(QString());
+ }
+ }
+
+ // Update pieces of moves played after last setup or root
+ auto& tree = m_game.get_tree();
+ // We need to loop forward through the moves to ensure the persistence of
+ // the GUI pieces, see comment in updatePiece()
+ ArrayList<const SgfNode*, Board::max_moves> nodes;
+ auto node = &m_game.get_current();
+ do
+ {
+ if (tree.has_move(*node))
+ nodes.push_back(node);
+ if (has_setup(*node) || nodes.size() == decltype(nodes)::max_size)
+ break;
+ node = node->get_parent_or_null();
+ }
+ while (node);
+ PieceModel* pieceModel = nullptr;
+ int moveNumber = 1;
+ for (auto i = nodes.size(); i > 0; --i)
+ {
+ node = nodes[i - 1];
+ auto mv = tree.get_move(*node);
+ auto c = mv.color;
+ pieceModel = updatePiece(c, mv.move, isPlayed[c]);
+ QString label = QString::number(moveNumber);
+ ++moveNumber;
+ unsigned moveIndex;
+ if (m_showVariations && getVariationIndex(tree, *node, moveIndex))
+ label.append(get_letter_coord(moveIndex).c_str());
+ label.append(getMoveAnnotationAtNode(*node));
+ pieceModel->setMoveLabel(label);
+ }
+ if (pieceModel != m_lastMovePieceModel)
+ {
+ if (m_lastMovePieceModel != nullptr)
+ m_lastMovePieceModel->setIsLastMove(false);
+ if (pieceModel != nullptr)
+ pieceModel->setIsLastMove(true);
+ m_lastMovePieceModel = pieceModel;
+ }
+
+ // Update pieces not on board
+ for (Color c : bd.get_colors())
+ for (int i = 0; i < m_pieceModels[c].length(); ++i)
+ {
+ auto pieceModel = qvariant_cast<PieceModel*>(m_pieceModels[c][i]);
+ if (! isPlayed[c][i] && pieceModel->isPlayed())
+ {
+ pieceModel->setDefaultState();
+ pieceModel->setIsPlayed(false);
+ pieceModel->setMoveLabel(QString());
+ }
+ }
+}
+
+void GameModel::updatePositionInfo()
+{
+ auto& tree = m_game.get_tree();
+ auto& current = m_game.get_current();
+ auto& bd = m_game.get_board();
+ auto move = get_move_number(tree, current);
+ auto left = get_moves_left(tree, current);
+ auto total = move + left;
+ auto variation = get_variation_string(current);
+ QString positionInfo = QString::number(move);
+ if (left > 0 || move > 0)
+ positionInfo.append(getMoveAnnotationAtNode(current));
+ if (left > 0)
+ {
+ positionInfo.append('/');
+ positionInfo.append(QString::number(total));
+ }
+ if (! variation.empty())
+ {
+ positionInfo.append(" (");
+ positionInfo.append(QString::fromLocal8Bit(variation.c_str()));
+ positionInfo.append(')');
+ }
+ auto positionInfoShort = positionInfo;
+ if (positionInfo.isEmpty())
+ {
+ positionInfo = bd.has_setup() ? tr("(Setup)") : tr("(No moves)");
+ positionInfoShort = bd.has_setup() ? tr("(Setup)") : QString();
+ }
+ else
+ {
+ //: The argument is the current move number.
+ positionInfo = tr("Move %1").arg(positionInfo);
+ if (bd.get_nu_moves() == 0 && bd.has_setup())
+ {
+ positionInfo.append(' ');
+ positionInfo.append(tr("(Setup)"));
+ positionInfoShort.append(' ');
+ positionInfoShort.append(tr("(Setup)"));
+ }
+ }
+ set(m_positionInfo, positionInfo, &GameModel::positionInfoChanged);
+ set(m_positionInfoShort, positionInfoShort,
+ &GameModel::positionInfoShortChanged);
+}
+
+/** Update all properties that might change when changing the current
+ position in the game tree. */
+void GameModel::updateProperties()
+{
+ auto& bd = getBoard();
+ auto& geo = bd.get_geometry();
+ auto& tree = m_game.get_tree();
+ bool isGembloQ = (bd.get_piece_set() == PieceSet::gembloq);
+ bool isTrigon = (bd.get_piece_set() == PieceSet::trigon);
+ bool isNexos = (bd.get_board_type() == BoardType::nexos);
+ 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 (isNexos)
+ m_tmpPoints.append(QPointF(4, 4));
+ else if (isGembloQ)
+ m_tmpPoints.append(getGembloQStartingPoint(bd, Color(0)));
+ else 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 (isNexos)
+ m_tmpPoints.append(QPointF(20, 4));
+ else if (isGembloQ)
+ m_tmpPoints.append(getGembloQStartingPoint(bd, Color(1)));
+ else 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 (isNexos)
+ m_tmpPoints.append(QPointF(20, 20));
+ else if (isGembloQ)
+ m_tmpPoints.append(getGembloQStartingPoint(bd, Color(2)));
+ else 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 (isNexos)
+ m_tmpPoints.append(QPointF(4, 20));
+ else if (isGembloQ)
+ m_tmpPoints.append(getGembloQStartingPoint(bd, Color(3)));
+ else 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_startingPointsAny, m_tmpPoints,
+ &GameModel::startingPointsAnyChanged);
+ auto& current = m_game.get_current();
+ set(m_canUndo,
+ ! current.has_children() && tree.has_move(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_hasEarlierVar, has_earlier_variation(current),
+ &GameModel::hasEarlierVarChanged);
+ set(m_isMainVar, is_main_variation(current), &GameModel::isMainVarChanged);
+ set(m_moveNumber, static_cast<int>(get_move_number(tree, current)),
+ &GameModel::moveNumberChanged);
+ set(m_movesLeft, static_cast<int>(get_moves_left(tree, current)),
+ &GameModel::movesLeftChanged);
+ updatePositionInfo();
+ bool isGameOver = true;
+ for (Color c : bd.get_colors())
+ if (bd.has_moves(c))
+ {
+ isGameOver = false;
+ break;
+ }
+ set(m_isBoardEmpty, bd.get_nu_onboard_pieces() == 0,
+ &GameModel::isBoardEmptyChanged);
+ set(m_isGameOver, isGameOver, &GameModel::isGameOverChanged);
+ updateIsModified();
+ updatePieces();
+ set(m_comment, decode(m_game.get_comment()), &GameModel::commentChanged);
+ set(m_toPlay, m_isGameOver ? 0u : bd.get_effective_to_play().to_int(),
+ &GameModel::toPlayChanged);
+ set(m_altPlayer,
+ bd.get_variant() == Variant::classic_3 ? bd.get_alt_player() : 0u,
+ &GameModel::altPlayerChanged);
+
+ emit positionChanged();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/GameModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_GAME_MODEL_H
+#define PENTOBI_GAME_MODEL_H
+
+#include <QDateTime>
+#include <QUrl>
+#include "PieceModel.h"
+#include "libpentobi_base/Game.h"
+
+class QTextCodec;
+
+using namespace std;
+using libboardgame_sgf::SgfNode;
+using libpentobi_base::ColorMap;
+using libpentobi_base::ColorMove;
+using libpentobi_base::Board;
+using libpentobi_base::Game;
+using libpentobi_base::Move;
+using libpentobi_base::MoveList;
+using libpentobi_base::MoveMarker;
+using libpentobi_base::ScoreType;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+class GameMove
+ : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int color READ color CONSTANT)
+
+public:
+ GameMove(QObject* parent, ColorMove mv)
+ : QObject(parent),
+ m_move(mv)
+ { }
+
+
+ Q_INVOKABLE bool isNull() const { return m_move.is_null(); }
+
+
+ int color() const { return static_cast<int>(m_move.color.to_int()); }
+
+ ColorMove get() const { return m_move; }
+
+private:
+ ColorMove m_move;
+};
+
+//-----------------------------------------------------------------------------
+
+class GameModel
+ : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString gameVariant READ gameVariant NOTIFY gameVariantChanged)
+ Q_PROPERTY(QString positionInfo READ positionInfo NOTIFY positionInfoChanged)
+ Q_PROPERTY(QString positionInfoShort READ positionInfoShort NOTIFY positionInfoShortChanged)
+ Q_PROPERTY(QString comment READ comment WRITE setComment NOTIFY commentChanged)
+ Q_PROPERTY(QString file READ file NOTIFY fileChanged)
+ Q_PROPERTY(QStringList recentFiles READ recentFiles NOTIFY recentFilesChanged)
+ Q_PROPERTY(unsigned nuColors READ nuColors NOTIFY nuColorsChanged)
+ Q_PROPERTY(unsigned nuPlayers READ nuPlayers NOTIFY nuPlayersChanged)
+ Q_PROPERTY(unsigned toPlay READ toPlay NOTIFY toPlayChanged)
+ Q_PROPERTY(unsigned altPlayer READ altPlayer NOTIFY altPlayerChanged)
+ Q_PROPERTY(int moveNumber READ moveNumber NOTIFY moveNumberChanged)
+ Q_PROPERTY(int movesLeft READ movesLeft NOTIFY movesLeftChanged)
+ Q_PROPERTY(float points0 READ points0 NOTIFY points0Changed)
+ Q_PROPERTY(float points1 READ points1 NOTIFY points1Changed)
+ Q_PROPERTY(float points2 READ points2 NOTIFY points2Changed)
+ Q_PROPERTY(float points3 READ points3 NOTIFY points3Changed)
+ Q_PROPERTY(float bonus0 READ bonus0 NOTIFY bonus0Changed)
+ Q_PROPERTY(float bonus1 READ bonus1 NOTIFY bonus1Changed)
+ Q_PROPERTY(float bonus2 READ bonus2 NOTIFY bonus2Changed)
+ Q_PROPERTY(float bonus3 READ bonus3 NOTIFY bonus3Changed)
+ Q_PROPERTY(bool hasMoves0 READ hasMoves0 NOTIFY hasMoves0Changed)
+ Q_PROPERTY(bool hasMoves1 READ hasMoves1 NOTIFY hasMoves1Changed)
+ Q_PROPERTY(bool hasMoves2 READ hasMoves2 NOTIFY hasMoves2Changed)
+ Q_PROPERTY(bool hasMoves3 READ hasMoves3 NOTIFY hasMoves3Changed)
+ Q_PROPERTY(bool isBoardEmpty READ isBoardEmpty NOTIFY isBoardEmptyChanged)
+ Q_PROPERTY(bool isGameOver READ isGameOver NOTIFY isGameOverChanged)
+ Q_PROPERTY(bool isModified READ isModified WRITE setIsModified NOTIFY isModifiedChanged)
+ Q_PROPERTY(bool canUndo READ canUndo NOTIFY canUndoChanged)
+ Q_PROPERTY(bool canGoBackward READ canGoBackward NOTIFY canGoBackwardChanged)
+ Q_PROPERTY(bool canGoForward READ canGoForward NOTIFY canGoForwardChanged)
+ Q_PROPERTY(bool hasPrevVar READ hasPrevVar NOTIFY hasPrevVarChanged)
+ Q_PROPERTY(bool hasNextVar READ hasNextVar NOTIFY hasNextVarChanged)
+ Q_PROPERTY(bool hasVariations READ hasVariations NOTIFY hasVariationsChanged)
+ Q_PROPERTY(bool hasEarlierVar READ hasEarlierVar NOTIFY hasEarlierVarChanged)
+ Q_PROPERTY(bool isMainVar READ isMainVar NOTIFY isMainVarChanged)
+ Q_PROPERTY(bool showVariations MEMBER m_showVariations WRITE setShowVariations NOTIFY showVariationsChanged)
+ Q_PROPERTY(QVariantList startingPoints0 READ startingPoints0 NOTIFY startingPoints0Changed)
+ Q_PROPERTY(QVariantList startingPoints1 READ startingPoints1 NOTIFY startingPoints1Changed)
+ Q_PROPERTY(QVariantList startingPoints2 READ startingPoints2 NOTIFY startingPoints2Changed)
+ Q_PROPERTY(QVariantList startingPoints3 READ startingPoints3 NOTIFY startingPoints3Changed)
+ Q_PROPERTY(QVariantList startingPointsAny READ startingPointsAny NOTIFY startingPointsAnyChanged)
+ Q_PROPERTY(QString playerName0 READ playerName0 WRITE setPlayerName0 NOTIFY playerName0Changed)
+ Q_PROPERTY(QString playerName1 READ playerName1 WRITE setPlayerName1 NOTIFY playerName1Changed)
+ Q_PROPERTY(QString playerName2 READ playerName2 WRITE setPlayerName2 NOTIFY playerName2Changed)
+ Q_PROPERTY(QString playerName3 READ playerName3 WRITE setPlayerName3 NOTIFY playerName3Changed)
+ Q_PROPERTY(QString date READ date WRITE setDate NOTIFY dateChanged)
+ Q_PROPERTY(QString time READ time WRITE setTime NOTIFY timeChanged)
+ Q_PROPERTY(QString event READ getEvent WRITE setEvent NOTIFY eventChanged)
+ Q_PROPERTY(QString round READ getRound WRITE setRound NOTIFY roundChanged)
+
+public:
+ static const int maxRecentFiles = 9;
+
+ static Variant getInitialGameVariant();
+
+
+ explicit GameModel(QObject* parent = nullptr);
+
+ ~GameModel() override;
+
+
+ Q_INVOKABLE void addSetup(PieceModel* pieceModel, QPointF coord);
+
+ /** Remove a piece from the board.
+ Updates setup properties in the current node.
+ @param pos The point on the board in game coordinates.
+ @return The PieceModel corresponding to the removed piece or null if
+ there is no piece at this location. */
+ Q_INVOKABLE PieceModel* addEmpty(const QPoint& pos);
+
+ Q_INVOKABLE void clearRecentFiles();
+
+ Q_INVOKABLE bool createFolder(const QUrl& folder);
+
+ Q_INVOKABLE void deleteAllVar();
+
+ Q_INVOKABLE bool findNextComment();
+
+ Q_INVOKABLE bool findNextCommentContinueFromRoot();
+
+ Q_INVOKABLE int getMoveNumberAt(const QPoint& pos);
+
+ Q_INVOKABLE QString getPlayerString(int player);
+
+ Q_INVOKABLE QString getVariationInfo() const;
+
+ Q_INVOKABLE bool isLegalPos(PieceModel* pieceModel, const QString& state,
+ QPointF coord) const;
+
+ Q_INVOKABLE bool isLegalSetupPos(PieceModel* pieceModel,
+ const QString& state,
+ QPointF coord) const;
+
+ Q_INVOKABLE void keepOnlyPosition();
+
+ Q_INVOKABLE void keepOnlySubtree();
+
+ Q_INVOKABLE void nextColor();
+
+ Q_INVOKABLE bool openByteArray(const QByteArray& byteArray);
+
+ Q_INVOKABLE bool openClipboard();
+
+ Q_INVOKABLE bool openFile(const QString& file);
+
+ Q_INVOKABLE PieceModel* preparePiece(GameMove* move);
+
+ Q_INVOKABLE void playPiece(PieceModel* pieceModel, QPointF coord);
+
+ Q_INVOKABLE void playMove(GameMove* move);
+
+ Q_INVOKABLE void newGame();
+
+ Q_INVOKABLE void undo();
+
+ Q_INVOKABLE QString getMoveAnnotation(int moveNumber);
+
+ Q_INVOKABLE void goBeginning();
+
+ Q_INVOKABLE void goBackward();
+
+ Q_INVOKABLE void goBackward10();
+
+ Q_INVOKABLE void goForward();
+
+ Q_INVOKABLE void goForward10();
+
+ Q_INVOKABLE void goEnd();
+
+ Q_INVOKABLE void goNextVar();
+
+ Q_INVOKABLE void goPrevVar();
+
+ Q_INVOKABLE void backToMainVar();
+
+ Q_INVOKABLE void gotoBeginningOfBranch();
+
+ Q_INVOKABLE void gotoMove(int n);
+
+ Q_INVOKABLE void changeGameVariant(const QString& gameVariant);
+
+ Q_INVOKABLE void autoSave();
+
+ Q_INVOKABLE bool loadAutoSave();
+
+ Q_INVOKABLE bool save(const QString& file);
+
+ Q_INVOKABLE bool saveAsciiArt(const QString& file);
+
+ Q_INVOKABLE void setMoveAnnotation(int moveNumber,
+ const QString& annotation);
+
+ Q_INVOKABLE void makeMainVar();
+
+ Q_INVOKABLE void moveDownVar();
+
+ Q_INVOKABLE void moveUpVar();
+
+ Q_INVOKABLE void truncate();
+
+ Q_INVOKABLE void truncateChildren();
+
+ Q_INVOKABLE QString getResultMessage();
+
+ Q_INVOKABLE bool checkFileExists(const QString& file);
+
+ Q_INVOKABLE bool checkFileModifiedOutside();
+
+ Q_INVOKABLE bool checkAutosaveModifiedOutside();
+
+ Q_INVOKABLE GameMove* findMoveNext();
+
+ Q_INVOKABLE GameMove* findMovePrevious();
+
+ Q_INVOKABLE PieceModel* pickNamedPiece(const QString& name,
+ PieceModel* currentPickedPiece);
+
+ Q_INVOKABLE PieceModel* nextPiece(PieceModel* currentPickedPiece);
+
+ Q_INVOKABLE PieceModel* previousPiece(PieceModel* currentPickedPiece);
+
+ Q_INVOKABLE QString suggestFileName(const QUrl& folder,
+ const QString& fileEnding);
+
+ Q_INVOKABLE QString suggestGameFileName(const QUrl& folder);
+
+ Q_INVOKABLE QString suggestNewFolderName(const QUrl& folder);
+
+ Q_INVOKABLE QString getError() const { return m_error; }
+
+ Q_INVOKABLE QVariantList getPieceModels(int color);
+
+
+ QByteArray getSgf() const;
+
+ void setComment(const QString& comment);
+
+ const QString& gameVariant() const { return m_gameVariant; }
+
+ const QString& positionInfo() const { return m_positionInfo; }
+
+ const QString& positionInfoShort() const { return m_positionInfoShort; }
+
+ const QString& file() const { return m_file; }
+
+ const QString& comment() const { return m_comment; }
+
+ unsigned nuColors() const { return m_nuColors; }
+
+ unsigned nuPlayers() const { return m_nuPlayers; }
+
+ unsigned toPlay() const { return m_toPlay; }
+
+ unsigned altPlayer() const { return m_altPlayer; }
+
+ int moveNumber() const { return m_moveNumber; }
+
+ int movesLeft() const { return m_movesLeft; }
+
+ float points0() const { return m_points0; }
+
+ float points1() const { return m_points1; }
+
+ float points2() const { return m_points2; }
+
+ float points3() const { return m_points3; }
+
+ float bonus0() const { return m_bonus0; }
+
+ float bonus1() const { return m_bonus1; }
+
+ float bonus2() const { return m_bonus2; }
+
+ float bonus3() const { return m_bonus3; }
+
+ bool hasMoves0() const { return m_hasMoves0; }
+
+ bool hasMoves1() const { return m_hasMoves1; }
+
+ bool hasMoves2() const { return m_hasMoves2; }
+
+ bool hasMoves3() const { return m_hasMoves3; }
+
+ bool isBoardEmpty() const { return m_isBoardEmpty; }
+
+ bool isGameOver() const { return m_isGameOver; }
+
+ bool isModified() const { return m_isModified; }
+
+ bool canUndo() const { return m_canUndo; }
+
+ bool canGoBackward() const { return m_canGoBackward; }
+
+ bool canGoForward() const { return m_canGoForward; }
+
+ bool hasEarlierVar() const { return m_hasEarlierVar; }
+
+ bool hasPrevVar() const { return m_hasPrevVar; }
+
+ bool hasNextVar() const { return m_hasNextVar; }
+
+ bool hasVariations() const { return m_hasVariations; }
+
+ bool isMainVar() const { return m_isMainVar; }
+
+ const QStringList& recentFiles() const { return m_recentFiles; }
+
+ const QVariantList& startingPoints0() const { return m_startingPoints0; }
+
+ const QVariantList& startingPoints1() const { return m_startingPoints1; }
+
+ const QVariantList& startingPoints2() const { return m_startingPoints2; }
+
+ const QVariantList& startingPoints3() const { return m_startingPoints3; }
+
+ const QVariantList& startingPointsAny() const { return m_startingPointsAny; }
+
+ const QString& playerName0() const { return m_playerName0; }
+
+ const QString& playerName1() const { return m_playerName1; }
+
+ const QString& playerName2() const { return m_playerName2; }
+
+ const QString& playerName3() const { return m_playerName3; }
+
+ const QString& date() const { return m_date; }
+
+ const QString& time() const { return m_time; }
+
+ // Avoid conflict with QObject::event()
+ const QString& getEvent() const { return m_event; }
+
+ // Avoid conflict with round(), which cannot be resolved by using fully
+ // qualified std::round(), because with many GCC versions it's in the
+ // global namespace (http://stackoverflow.com/questions/1882689)
+ const QString& getRound() const { return m_round; }
+
+ void setIsModified(bool isModified);
+
+ void setMoveAnnotationAtNode(const SgfNode& node,
+ const QString& annotation);
+
+ void setPlayerName0(const QString& name);
+
+ void setPlayerName1(const QString& name);
+
+ void setPlayerName2(const QString& name);
+
+ void setPlayerName3(const QString& name);
+
+ void setDate(const QString& date);
+
+ void setShowVariations(bool showVariations);
+
+ void setTime(const QString& time);
+
+ void setEvent(const QString& event);
+
+ void setRound(const QString& round);
+
+ const Game& getGame() const { return m_game; }
+
+ const Board& getBoard() const { return m_game.get_board(); }
+
+ void gotoNode(const SgfNode& node);
+
+ void gotoNode(const SgfNode* node);
+
+signals:
+ /** Loaded Blokus SGF file has invalid syntax.
+ Triggered when a loaded SGF file causes a problem later than at load
+ time (e.g. invalid move property value in a side variation). The
+ reason can be retrieved with in getError().*/
+ void invalidSgfFile();
+
+ /** 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();
+
+ void altPlayerChanged();
+
+ void fileChanged();
+
+ void points0Changed();
+
+ void points1Changed();
+
+ void points2Changed();
+
+ void points3Changed();
+
+ void bonus0Changed();
+
+ void bonus1Changed();
+
+ void bonus2Changed();
+
+ void bonus3Changed();
+
+ void hasEarlierVarChanged();
+
+ void hasMoves0Changed();
+
+ void hasMoves1Changed();
+
+ void hasMoves2Changed();
+
+ void hasMoves3Changed();
+
+ void hasVariationsChanged();
+
+ void isBoardEmptyChanged();
+
+ void isGameOverChanged();
+
+ void isModifiedChanged();
+
+ void isMainVarChanged();
+
+ void canUndoChanged();
+
+ void canGoBackwardChanged();
+
+ void canGoForwardChanged();
+
+ void hasPrevVarChanged();
+
+ void hasNextVarChanged();
+
+ void gameVariantChanged();
+
+ void positionInfoChanged();
+
+ void positionInfoShortChanged();
+
+ void commentChanged();
+
+ void moveNumberChanged();
+
+ void movesLeftChanged();
+
+ void nuColorsChanged();
+
+ void nuPlayersChanged();
+
+ void recentFilesChanged();
+
+ void startingPoints0Changed();
+
+ void startingPoints1Changed();
+
+ void startingPoints2Changed();
+
+ void startingPoints3Changed();
+
+ void startingPointsAnyChanged();
+
+ void playerName0Changed();
+
+ void playerName1Changed();
+
+ void playerName2Changed();
+
+ void playerName3Changed();
+
+ void dateChanged();
+
+ void showVariationsChanged();
+
+ void timeChanged();
+
+ void eventChanged();
+
+ void roundChanged();
+
+private:
+ Game m_game;
+
+ QString m_gameVariant;
+
+ QString m_positionInfo;
+
+ QString m_positionInfoShort;
+
+ QString m_comment;
+
+ QString m_error;
+
+ QString m_file;
+
+ QString m_playerName0;
+
+ QString m_playerName1;
+
+ QString m_playerName2;
+
+ QString m_playerName3;
+
+ QString m_date;
+
+ QString m_time;
+
+ QString m_event;
+
+ QString m_round;
+
+ QStringList m_recentFiles;
+
+ QDateTime m_fileDate;
+
+ QDateTime m_autosaveDate;
+
+ unsigned m_nuColors;
+
+ unsigned m_nuPlayers;
+
+ unsigned m_toPlay = 0;
+
+ unsigned m_altPlayer = 0;
+
+ int m_moveNumber = 0;
+
+ int m_movesLeft = 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_isBoardEmpty = true;
+
+ bool m_isGameOver = false;
+
+ bool m_isModified = false;
+
+ bool m_canUndo = false;
+
+ bool m_canGoForward = false;
+
+ bool m_canGoBackward = false;
+
+ bool m_hasEarlierVar = false;
+
+ bool m_hasPrevVar = false;
+
+ bool m_hasNextVar = false;
+
+ bool m_isMainVar = true;
+
+ bool m_showVariations = true;
+
+ ColorMap<QVariantList> m_pieceModels;
+
+ PieceModel* m_lastMovePieceModel = nullptr;
+
+ QVariantList m_startingPoints0;
+
+ QVariantList m_startingPoints1;
+
+ QVariantList m_startingPoints2;
+
+ QVariantList m_startingPoints3;
+
+ QVariantList m_startingPointsAny;
+
+ QVariantList m_tmpPoints;
+
+ QTextCodec* m_textCodec;
+
+ unique_ptr<MoveList> m_legalMoves;
+
+ unsigned m_legalMoveIndex;
+
+ /** Local variable reused for efficiency. */
+ unique_ptr<MoveMarker> m_marker;
+
+
+ void addRecentFile(const QString& file);
+
+ bool checkSetupAllowed() const;
+
+ void clearFile();
+
+ void createPieceModels();
+
+ void createPieceModels(Color c);
+
+ QString decode(const string& s) const;
+
+ QByteArray encode(const QString& s) const;
+
+ bool findMove(const PieceModel& pieceModel, const QString& state,
+ QPointF coord, Move& mv) const;
+
+ PieceModel* findUnplayedPieceModel(Color c, Piece piece);
+
+ QString getMoveAnnotationAtNode(const SgfNode& node) const;
+
+ ColorMove getMoveAt(const QPoint& pos) const;
+
+ void initGame(Variant variant);
+
+ void initGameVariant(Variant variant);
+
+ void loadRecentFiles();
+
+ bool openStream(istream& in);
+
+ void prepareFindMove();
+
+ void preparePieceGameCoord(PieceModel* pieceModel, Move mv);
+
+ void preparePieceTransform(PieceModel* pieceModel, Move mv);
+
+ void preparePositionChange();
+
+ void restoreAutoSaveLocation();
+
+ template<typename T>
+ bool set(T& target, const T& value, void (GameModel::*changedSignal)());
+
+ void setFile(const QString& file);
+
+ void setSetupPlayer();
+
+ void setUtf8();
+
+ void updateFileInfo(const QString& file);
+
+ void updateGameInfo();
+
+ void updateIsModified();
+
+ PieceModel* updatePiece(Color c, Move mv,
+ array<bool, Board::max_pieces>& isPlayed);
+
+ void updatePieces();
+
+ void updatePositionInfo();
+
+ void updateProperties();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_GAME_MODEL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/ImageProvider.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "ImageProvider.h"
+
+#include <QPainter>
+#include "libpentobi_paint/Paint.h"
+
+using namespace std;
+using namespace libpentobi_paint;
+
+//-----------------------------------------------------------------------------
+
+ImageProvider::ImageProvider()
+ : QQuickImageProvider(QQuickImageProvider::Pixmap)
+{ }
+
+QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size,
+ const QSize& requestedSize)
+{
+ // Piece element images are always created with a user-defined sourceSize,
+ // requestedSize can only become 0 temporarily when changing the game
+ // variant (when scaleUnplayed of a piece becomes 0). In this case, we
+ // return a 1x1 pixmap (0x0 would cause a QQuickImageProvider warning).
+ int width = max(requestedSize.width(), 1);
+ int height = max(requestedSize.height(), 1);
+ *size = QSize(width, height);
+ QPixmap pixmap(width, height);
+ if (requestedSize.width() == 0 || requestedSize.height() == 0)
+ return pixmap;
+ pixmap.fill(Qt::transparent);
+ QPainter painter(&pixmap);
+ painter.setRenderHint(QPainter::Antialiasing);
+ auto splitRef = id.splitRef(QStringLiteral("/"));
+ if (splitRef.empty())
+ return pixmap;
+ auto name = splitRef[0];
+ if (name == "board" && splitRef.size() == 8)
+ {
+ auto gameVariant = splitRef[1].toLocal8Bit();
+ QColor base(splitRef[2]);
+ QColor dark(splitRef[3]);
+ QColor light(splitRef[4]);
+ QColor centerBase(splitRef[5]);
+ QColor centerDark(splitRef[6]);
+ QColor centerLight(splitRef[7]);
+ Variant variant;
+ if (parse_variant_id(gameVariant.constData(), variant))
+ paintBoard(painter, width, height, variant, base, light, dark,
+ centerBase, centerLight, centerDark);
+ }
+ else if (splitRef.size() == 2)
+ {
+ QColor base(splitRef[1]);
+ if (name == "junction-all")
+ paintJunctionAll(painter, 0, 0, width, height, base);
+ else if (name == "junction-right")
+ paintJunctionRight(painter, 0, 0, width, height, base);
+ else if (name == "junction-straight")
+ paintJunctionStraight(painter, 0, 0, width, height, base);
+ else if (name == "junction-t")
+ paintJunctionT(painter, 0, 0, width, height, base);
+ }
+ else if (splitRef.size() == 4)
+ {
+ QColor base(splitRef[1]);
+ QColor dark(splitRef[2]);
+ QColor light(splitRef[3]);
+ if (name == "frame")
+ paintCallistoOnePiece(painter, 0, 0, width, height, base, light,
+ dark);
+ else if (name == "quarter-square")
+ paintQuarterSquare(painter, 0, 0, width, height, base, light);
+ else if (name == "quarter-square-bottom")
+ paintQuarterSquare(painter, 0, 0, width, height, base, dark);
+ else if (name == "square")
+ paintSquare(painter, 0, 0, width, height, base, light, dark);
+ else if (name == "triangle")
+ paintTriangleUp(painter, 0, 0, width, height, base, light, dark);
+ else if (name == "triangle-down")
+ paintTriangleDown(painter, 0, 0, width, height, base, light, dark);
+ }
+ return pixmap;
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/ImageProvider.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_IMAGE_PROVIDER_H
+#define PENTOBI_IMAGE_PROVIDER_H
+
+#include <QQuickImageProvider>
+
+//-----------------------------------------------------------------------------
+
+class ImageProvider
+ : public QQuickImageProvider
+{
+public:
+ ImageProvider();
+
+ QPixmap requestPixmap(const QString& id, QSize* size,
+ const QSize& requestedSize) override;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_IMAGE_PROVIDER_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/Main.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include <QApplication>
+#include <QIcon>
+#include <QQuickStyle>
+#include <QtQml>
+#include <QTranslator>
+#include "AnalyzeGameModel.h"
+#include "AndroidUtils.h"
+#include "GameModel.h"
+#include "ImageProvider.h"
+#include "PlayerModel.h"
+#include "RatingModel.h"
+#include "SyncSettings.h"
+#include "libboardgame_util/Log.h"
+
+#ifndef Q_OS_ANDROID
+#include <QCommandLineParser>
+#endif
+
+#ifndef PENTOBI_OPEN_HELP_EXTERNALLY
+#include <QtWebView>
+#endif
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+#ifdef Q_OS_ANDROID
+
+int mainAndroid()
+{
+ QQmlApplicationEngine engine;
+ engine.addImageProvider(QStringLiteral("pentobi"), new ImageProvider);
+ auto ctx = engine.rootContext();
+ ctx->setContextProperty(QStringLiteral("initialFile"), QString());
+ ctx->setContextProperty(QStringLiteral("isDesktop"), false);
+#ifdef QT_DEBUG
+ ctx->setContextProperty(QStringLiteral("isDebug"), true);
+#else
+ ctx->setContextProperty(QStringLiteral("isDebug"), false);
+#endif
+ ctx->setContextProperty(QStringLiteral("openHelpExternally"), false);
+ engine.load(QStringLiteral("qrc:///qml/Main.qml"));
+ if (engine.rootObjects().empty())
+ return 1;
+ return QGuiApplication::exec();
+}
+
+#else // ! defined(Q_OS_ANDROID)
+
+int mainDesktop()
+{
+ QIcon icon;
+ icon.addFile(QStringLiteral(":/pentobi_icon/pentobi.svg"));
+ icon.addFile(QStringLiteral(":/pentobi_icon/pentobi-16.svg"));
+ icon.addFile(QStringLiteral(":/pentobi_icon/pentobi-32.svg"));
+ icon.addFile(QStringLiteral(":/pentobi_icon/pentobi-64.svg"));
+ QGuiApplication::setWindowIcon(icon);
+ QGuiApplication::setDesktopFileName(
+ QStringLiteral("io.sourceforge.pentobi"));
+ QCommandLineParser parser;
+ auto maxSupportedLevel = Player::max_supported_level;
+ QCommandLineOption optionMaxLevel(
+ QStringLiteral("maxlevel"),
+ QStringLiteral("Set maximum level to <n>."),
+ QStringLiteral("n"),
+ QString::number(PlayerModel::maxLevel));
+ parser.addOption(optionMaxLevel);
+ QCommandLineOption optionNoBook(
+ QStringLiteral("nobook"),
+ QStringLiteral("Do not use opening books."));
+ QCommandLineOption optionMobile(
+ QStringLiteral("mobile"),
+ QStringLiteral("Use layout optimized for smartphones."));
+ parser.addOption(optionMobile);
+ parser.addOption(optionNoBook);
+ QCommandLineOption optionNoDelay(
+ QStringLiteral("nodelay"),
+ QStringLiteral("Do not delay fast computer moves."));
+ parser.addOption(optionNoDelay);
+ QCommandLineOption optionSeed(
+ QStringLiteral("seed"),
+ QStringLiteral("Set random seed to <n>."),
+ QStringLiteral("n"));
+ parser.addOption(optionSeed);
+ QCommandLineOption optionThreads(
+ QStringLiteral("threads"),
+ QStringLiteral("Use <n> threads (0=auto)."),
+ QStringLiteral("n"));
+ parser.addOption(optionThreads);
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ QCommandLineOption optionVerbose(
+ QStringLiteral("verbose"),
+ QStringLiteral("Print logging information to standard error."));
+ parser.addOption(optionVerbose);
+#endif
+ parser.addPositionalArgument(
+ QStringLiteral("file.blksgf"),
+ QStringLiteral("Blokus SGF file to open (optional)."));
+ parser.addHelpOption();
+ parser.process(*QCoreApplication::instance());
+ try
+ {
+#ifndef LIBBOARDGAME_DISABLE_LOG
+ 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;
+ auto maxLevel = parser.value(optionMaxLevel).toUInt(&ok);
+ if (! ok || maxLevel < 1 || maxLevel > maxSupportedLevel)
+ throw runtime_error("--maxlevel must be between 1 and "
+ + libboardgame_util::to_string(maxSupportedLevel));
+ PlayerModel::maxLevel = maxLevel;
+ if (parser.isSet(optionSeed))
+ {
+ auto seed = parser.value(optionSeed).toUInt(&ok);
+ if (! ok)
+ throw runtime_error("--seed must be a positive number");
+ libboardgame_util::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;
+ }
+ bool isDesktop = ! parser.isSet(optionMobile);
+ QString initialFile;
+ auto args = parser.positionalArguments();
+ if (args.size() > 1)
+ throw runtime_error("Too many arguments");
+ if (! args.empty())
+ initialFile = args.at(0);
+ if (QQuickStyle::name().isEmpty() && isDesktop)
+ QQuickStyle::setStyle(QStringLiteral("Fusion"));
+ QQmlApplicationEngine engine;
+ engine.addImageProvider(QStringLiteral("pentobi"), new ImageProvider);
+ auto ctx = engine.rootContext();
+ ctx->setContextProperty(QStringLiteral("initialFile"), initialFile);
+ ctx->setContextProperty(QStringLiteral("isDesktop"), isDesktop);
+#ifdef QT_DEBUG
+ ctx->setContextProperty(QStringLiteral("isDebug"), true);
+#else
+ ctx->setContextProperty(QStringLiteral("isDebug"), false);
+#endif
+#ifdef PENTOBI_HELP_DIR
+ ctx->setContextProperty(QStringLiteral("helpDir"),
+ QString::fromLocal8Bit(PENTOBI_HELP_DIR));
+#else
+ ctx->setContextProperty(QStringLiteral("helpDir"), QString());
+#endif
+#ifdef PENTOBI_OPEN_HELP_EXTERNALLY
+ ctx->setContextProperty(QStringLiteral("openHelpExternally"), true);
+#else
+ ctx->setContextProperty(QStringLiteral("openHelpExternally"), false);
+#endif
+ engine.load(QStringLiteral("qrc:///qml/Main.qml"));
+ if (engine.rootObjects().empty())
+ return 1;
+ return QGuiApplication::exec();
+ }
+ catch (const exception& e)
+ {
+ LIBBOARDGAME_LOG("Error: ", e.what());
+ return 1;
+ }
+}
+
+#endif // Q_OS_ANDROID
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char *argv[])
+{
+ libboardgame_util::LogInitializer log_initializer;
+#ifdef Q_OS_ANDROID
+ // We don't use HighDpiScaling on low-DPI Android devices because of
+ // QTBUG-69102 and other bugs
+ auto density = AndroidUtils::getDensity();
+ if (density == 0 || density > 1)
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#else
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#endif
+ QCoreApplication::setOrganizationName(QStringLiteral("Pentobi"));
+ QCoreApplication::setApplicationName(QStringLiteral("Pentobi"));
+#ifdef VERSION
+ QCoreApplication::setApplicationVersion(QStringLiteral(VERSION));
+#endif
+ QGuiApplication app(argc, argv);
+#ifndef PENTOBI_OPEN_HELP_EXTERNALLY
+ QtWebView::initialize();
+#endif
+ qmlRegisterType<AnalyzeGameModel>("pentobi", 1, 0, "AnalyzeGameModel");
+ qmlRegisterType<AndroidUtils>("pentobi", 1, 0, "AndroidUtils");
+ qmlRegisterType<GameModel>("pentobi", 1, 0, "GameModel");
+ qmlRegisterType<PlayerModel>("pentobi", 1, 0, "PlayerModel");
+ qmlRegisterType<RatingModel>("pentobi", 1, 0, "RatingModel");
+ qmlRegisterType<SyncSettings>("pentobi", 1, 0, "SyncSettings");
+ qmlRegisterInterface<AnalyzeGameElement>("AnalyzeGameElement");
+ qmlRegisterInterface<GameMove>("GameModelMove");
+ qmlRegisterInterface<PieceModel>("PieceModel");
+ QTranslator translator;
+ translator.load(":qml/i18n/qml_" + QLocale::system().name());
+ QCoreApplication::installTranslator(&translator);
+#ifdef Q_OS_ANDROID
+ return mainAndroid();
+#else
+ return mainDesktop();
+#endif
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+#############################################################################
+# The preferred way of building Pentobi is using CMake. This project file
+# exists only because building, deploying and debugging for Android is not
+# yet functional for CMake projects in QtCreator.
+#############################################################################
+
+lessThan(QT_MAJOR_VERSION, 5) {
+ error("Qt >=5.11 required")
+}
+equals(QT_MAJOR_VERSION, 5):lessThan(QT_MINOR_VERSION, 11) {
+ error("Qt >=5.11 required")
+}
+
+TEMPLATE = app
+
+QT += concurrent quickcontrols2 svg webview
+android {
+ QT += androidextras
+}
+
+INCLUDEPATH += ..
+CONFIG += c++14 qtquickcompiler
+DEFINES += QT_DEPRECATED_WARNINGS
+DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x051100
+DEFINES += QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
+DEFINES += VERSION=\"\\\"16.2\\\"\"
+android {
+ DEFINES += PENTOBI_LOW_RESOURCES
+ 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 += \
+ AnalyzeGameModel.cpp \
+ AndroidUtils.cpp \
+ GameModel.cpp \
+ ImageProvider.cpp \
+ Main.cpp \
+ PieceModel.cpp \
+ PlayerModel.cpp \
+ RatingModel.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/Reader.cpp \
+ ../libboardgame_sgf/SgfError.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/Game.cpp \
+ ../libpentobi_base/GembloQGeometry.cpp \
+ ../libpentobi_base/GembloQTransform.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/PieceTransformsGembloQ.cpp \
+ ../libpentobi_base/PieceTransformsTrigon.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/AnalyzeGame.cpp \
+ ../libpentobi_mcts/History.cpp \
+ ../libpentobi_mcts/LocalPoints.cpp \
+ ../libpentobi_mcts/Player.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 \
+ ../libpentobi_paint/Paint.cpp
+
+RESOURCES += \
+ ../books/pentobi_books.qrc \
+ ../icon/pentobi_icon.qrc \
+ ../../doc/help.qrc \
+ qml/themes/themes.qrc \
+ qml/i18n/translations.qrc \
+ resources.qrc
+
+!android {
+ RESOURCES += \
+ ../icon/pentobi_icon_desktop.qrc \
+ resources_desktop.qrc
+}
+
+HEADERS += \
+ AnalyzeGameModel.h \
+ AndroidUtils.h \
+ GameModel.h \
+ ImageProvider.h \
+ PieceModel.h \
+ PlayerModel.h \
+ RatingModel.h \
+ SyncSettings.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/Reader.h \
+ ../libboardgame_sgf/SgfError.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/CallistoGeometry.h \
+ ../libpentobi_base/Color.h \
+ ../libpentobi_base/ColorMap.h \
+ ../libpentobi_base/ColorMove.h \
+ ../libpentobi_base/Game.h \
+ ../libpentobi_base/GembloQGeometry.h \
+ ../libpentobi_base/GembloQTransform.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/PieceTransformsGembloQ.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/AnalyzeGame.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 \
+ ../libpentobi_paint/Paint.h
+
+lupdate_only {
+SOURCES += \
+ qml/*.qml \
+ qml/*.js
+}
+
+TRANSLATIONS = \
+ qml/i18n/qml_de.ts \
+ qml/i18n/qml_fr.ts \
+ qml/i18n/qml_nb_NO.ts
+
+qtPrepareTool(LRELEASE, lrelease)
+updateqm.input = TRANSLATIONS
+updateqm.output = ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.qm
+updateqm.commands = $$LRELEASE -removeidentical -nounfinished ${QMAKE_FILE_IN} -qm ${QMAKE_FILE_OUT}
+updateqm.CONFIG += no_link target_predeps
+QMAKE_EXTRA_COMPILERS += updateqm
+
+OTHER_FILES += \
+ android/AndroidManifest.xml
+
+ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/PieceModel.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "PieceModel.h"
+
+#include "libboardgame_base/RectTransform.h"
+#include "libpentobi_base/Board.h"
+#include "libpentobi_base/GembloQTransform.h"
+#include "libpentobi_base/TrigonTransform.h"
+
+using namespace std;
+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 libboardgame_util::ArrayList;
+using libpentobi_base::BoardType;
+using libpentobi_base::GeometryType;
+using libpentobi_base::PieceInfo;
+using libpentobi_base::PieceSet;
+using libpentobi_base::TransfGembloQIdentity;
+using libpentobi_base::TransfGembloQRot90;
+using libpentobi_base::TransfGembloQRot180;
+using libpentobi_base::TransfGembloQRot270;
+using libpentobi_base::TransfGembloQRefl;
+using libpentobi_base::TransfGembloQRot90Refl;
+using libpentobi_base::TransfGembloQRot180Refl;
+using libpentobi_base::TransfGembloQRot270Refl;
+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;
+
+//-----------------------------------------------------------------------------
+
+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();
+ auto geoType = bd.get_geometry_type();
+ bool isCallisto = (geoType == GeometryType::callisto);
+ bool isGembloQ = (geoType == GeometryType::gembloq);
+ bool isNexos = (geoType == GeometryType::nexos);
+ bool isTrigon = (geoType == GeometryType::trigon);
+ auto& info = bd.get_piece_info(piece);
+ auto& points = info.get_points();
+ m_elements.reserve(static_cast<int>(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, int> 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);
+ }
+ m_center = findCenter(bd, points, true);
+ auto& labelPos = info.get_label_pos();
+ qreal labelX = labelPos.x - m_center.x();
+ qreal labelY = labelPos.y - m_center.y();
+ if (isGembloQ)
+ {
+ if (labelPos.x % 2 != 0)
+ labelX += 1;
+ if (labelPos.y % 2 != 0)
+ labelY += 1;
+ }
+ else if (isTrigon)
+ {
+ labelX += 0.5;
+ if ((labelPos.x % 2 == 0) != (labelPos.y % 2 == 0))
+ // Downward
+ labelY += 1. / 3;
+ else
+ labelY += 2. / 3;
+ }
+ else
+ {
+ labelX += 0.5;
+ labelY += 0.5;
+ }
+ m_labelPos = QPointF(labelX, labelY);
+}
+
+void PieceModel::flipAcrossX()
+{
+ setTransform(m_bd.get_transforms().get_mirrored_vertically(getTransform()));
+}
+
+void PieceModel::flipAcrossY()
+{
+ setTransform(m_bd.get_transforms().get_mirrored_horizontally(getTransform()));
+}
+
+const Transform* PieceModel::getTransform(const QString& state) const
+{
+ // See comment in getTransform() about the mapping between states and
+ // transform classes.
+ auto& transforms = m_bd.get_transforms();
+ auto pieceSet = m_bd.get_piece_set();
+ if (pieceSet == PieceSet::trigon)
+ {
+ if (state == QStringLiteral("rot60"))
+ return transforms.find<TransfTrigonRot60>();
+ if (state == QStringLiteral("rot120"))
+ return transforms.find<TransfTrigonRot120>();
+ if (state == QStringLiteral("rot180"))
+ return transforms.find<TransfTrigonRot180>();
+ if (state == QStringLiteral("rot240"))
+ return transforms.find<TransfTrigonRot240>();
+ if (state == QStringLiteral("rot300"))
+ return transforms.find<TransfTrigonRot300>();
+ if (state == QStringLiteral("flip"))
+ return transforms.find<TransfTrigonReflRot180>();
+ if (state == QStringLiteral("rot60Flip"))
+ return transforms.find<TransfTrigonReflRot120>();
+ if (state == QStringLiteral("rot120Flip"))
+ return transforms.find<TransfTrigonReflRot60>();
+ if (state == QStringLiteral("rot180Flip"))
+ return transforms.find<TransfTrigonRefl>();
+ if (state == QStringLiteral("rot240Flip"))
+ return transforms.find<TransfTrigonReflRot300>();
+ if (state == QStringLiteral("rot300Flip"))
+ return transforms.find<TransfTrigonReflRot240>();
+ return transforms.find<TransfTrigonIdentity>();
+ }
+ if (pieceSet == PieceSet::gembloq)
+ {
+ if (state == QStringLiteral("rot90"))
+ return transforms.find<TransfGembloQRot90>();
+ if (state == QStringLiteral("rot180"))
+ return transforms.find<TransfGembloQRot180>();
+ if (state == QStringLiteral("rot270"))
+ return transforms.find<TransfGembloQRot270>();
+ if (state == QStringLiteral("flip"))
+ return transforms.find<TransfGembloQRot180Refl>();
+ if (state == QStringLiteral("rot90Flip"))
+ return transforms.find<TransfGembloQRot90Refl>();
+ if (state == QStringLiteral("rot180Flip"))
+ return transforms.find<TransfGembloQRefl>();
+ if (state == QStringLiteral("rot270Flip"))
+ return transforms.find<TransfGembloQRot270Refl>();
+ return transforms.find<TransfGembloQIdentity>();
+ }
+ if (state == QStringLiteral("rot90"))
+ return transforms.find<TransfRectRot90>();
+ if (state == QStringLiteral("rot180"))
+ return transforms.find<TransfRectRot180>();
+ if (state == QStringLiteral("rot270"))
+ return transforms.find<TransfRectRot270>();
+ if (state == QStringLiteral("flip"))
+ return transforms.find<TransfRectRot180Refl>();
+ if (state == QStringLiteral("rot90Flip"))
+ return transforms.find<TransfRectRot90Refl>();
+ if (state == QStringLiteral("rot180Flip"))
+ return transforms.find<TransfRectRefl>();
+ if (state == QStringLiteral("rot270Flip"))
+ return transforms.find<TransfRectRot270Refl>();
+ return transforms.find<TransfIdentity>();
+}
+
+QPointF PieceModel::findCenter(const Board& bd, const PiecePoints& points,
+ bool usePieceInfoPointTypes)
+{
+ auto geoType = bd.get_geometry_type();
+ bool isTrigon = (geoType == GeometryType::trigon);
+ bool isGembloQ = (geoType == GeometryType::gembloq);
+ bool isNexos = (geoType == GeometryType::nexos);
+ bool isOriginDownward = (usePieceInfoPointTypes
+ && bd.get_board_type() == BoardType::trigon_3);
+ auto& geo = bd.get_geometry();
+ qreal sumX = 0;
+ qreal sumY = 0;
+ qreal n = 0;
+ for (auto& p : points)
+ {
+ auto pointType = geo.get_point_type(p);
+ if (isNexos && pointType == 0)
+ continue;
+ qreal centerX, centerY;
+ if (isTrigon)
+ {
+ bool isDownward = (pointType == (isOriginDownward ? 0 : 1));
+ centerX = 0.5;
+ centerY = isDownward ?
+ static_cast<qreal>(1) / 3 : static_cast<qreal>(2) / 3;
+ }
+ else if (isGembloQ)
+ {
+ centerX = (pointType == 1 || pointType == 3) ?
+ static_cast<qreal>(1) / 3 : static_cast<qreal>(2) / 3;
+ centerY = (pointType == 0 || pointType == 3) ?
+ static_cast<qreal>(1) / 3 : static_cast<qreal>(2) / 3;
+ }
+ else
+ {
+ centerX = 0.5;
+ centerY = 0.5;
+ }
+ sumX += (p.x + centerX);
+ sumY += (p.y + centerY);
+ ++n;
+ }
+ return {sumX / n, sumY / n};
+}
+
+void PieceModel::nextOrientation()
+{
+ setTransform(m_bd.get_piece_info(m_piece).get_next_transform(getTransform()));
+}
+
+void PieceModel::previousOrientation()
+{
+ setTransform(m_bd.get_piece_info(m_piece).get_previous_transform(getTransform()));
+}
+
+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::setDefaultState()
+{
+ if (m_state.isEmpty())
+ return;
+ m_state.clear();
+ emit stateChanged();
+}
+
+void PieceModel::setGameCoord(QPointF gameCoord)
+{
+ if (m_gameCoord == gameCoord)
+ return;
+ m_gameCoord = gameCoord;
+ emit gameCoordChanged();
+}
+
+void PieceModel::setIsLastMove(bool isLastMove)
+{
+ if (m_isLastMove == isLastMove)
+ return;
+ m_isLastMove = isLastMove;
+ emit isLastMoveChanged();
+}
+
+void PieceModel::setIsPlayed(bool isPlayed)
+{
+ if (m_isPlayed == isPlayed)
+ return;
+ m_isPlayed = isPlayed;
+ emit isPlayedChanged();
+}
+
+void PieceModel::setMoveLabel(const QString& moveLabel)
+{
+ if (m_moveLabel == moveLabel)
+ return;
+ m_moveLabel = moveLabel;
+ emit moveLabelChanged();
+}
+
+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.
+ auto pieceSet = m_bd.get_piece_set();
+ if (pieceSet == PieceSet::trigon)
+ {
+ if (dynamic_cast<const TransfTrigonRot60*>(transform))
+ state = QStringLiteral("rot60");
+ else if (dynamic_cast<const TransfTrigonRot120*>(transform))
+ state = QStringLiteral("rot120");
+ else if (dynamic_cast<const TransfTrigonRot180*>(transform))
+ state = QStringLiteral("rot180");
+ else if (dynamic_cast<const TransfTrigonRot240*>(transform))
+ state = QStringLiteral("rot240");
+ else if (dynamic_cast<const TransfTrigonRot300*>(transform))
+ state = QStringLiteral("rot300");
+ else if (dynamic_cast<const TransfTrigonReflRot180*>(transform))
+ state = QStringLiteral("flip");
+ else if (dynamic_cast<const TransfTrigonReflRot120*>(transform))
+ state = QStringLiteral("rot60Flip");
+ else if (dynamic_cast<const TransfTrigonReflRot60*>(transform))
+ state = QStringLiteral("rot120Flip");
+ else if (dynamic_cast<const TransfTrigonRefl*>(transform))
+ state = QStringLiteral("rot180Flip");
+ else if (dynamic_cast<const TransfTrigonReflRot300*>(transform))
+ state = QStringLiteral("rot240Flip");
+ else if (dynamic_cast<const TransfTrigonReflRot240*>(transform))
+ state = QStringLiteral("rot300Flip");
+ }
+ else if (pieceSet == PieceSet::gembloq)
+ {
+ if (dynamic_cast<const TransfGembloQRot90*>(transform))
+ state = QStringLiteral("rot90");
+ else if (dynamic_cast<const TransfGembloQRot180*>(transform))
+ state = QStringLiteral("rot180");
+ else if (dynamic_cast<const TransfGembloQRot270*>(transform))
+ state = QStringLiteral("rot270");
+ else if (dynamic_cast<const TransfGembloQRot180Refl*>(transform))
+ state = QStringLiteral("flip");
+ else if (dynamic_cast<const TransfGembloQRot90Refl*>(transform))
+ state = QStringLiteral("rot90Flip");
+ else if (dynamic_cast<const TransfGembloQRefl*>(transform))
+ state = QStringLiteral("rot180Flip");
+ else if (dynamic_cast<const TransfGembloQRot270Refl*>(transform))
+ state = QStringLiteral("rot270Flip");
+ }
+ else
+ {
+ if (dynamic_cast<const TransfRectRot90*>(transform))
+ state = QStringLiteral("rot90");
+ else if (dynamic_cast<const TransfRectRot180*>(transform))
+ state = QStringLiteral("rot180");
+ else if (dynamic_cast<const TransfRectRot270*>(transform))
+ state = QStringLiteral("rot270");
+ else if (dynamic_cast<const TransfRectRot180Refl*>(transform))
+ state = QStringLiteral("flip");
+ else if (dynamic_cast<const TransfRectRot90Refl*>(transform))
+ state = QStringLiteral("rot90Flip");
+ else if (dynamic_cast<const TransfRectRefl*>(transform))
+ state = QStringLiteral("rot180Flip");
+ else if (dynamic_cast<const TransfRectRot270Refl*>(transform))
+ state = QStringLiteral("rot270Flip");
+ }
+ if (m_state == state)
+ return;
+ m_state = state;
+ emit stateChanged();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/PieceModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_PIECE_MODEL_H
+#define PENTOBI_PIECE_MODEL_H
+
+#include <QObject>
+#include <QPointF>
+#include <QVariant>
+#include <QVector>
+#include "libpentobi_base/Color.h"
+#include "libpentobi_base/Piece.h"
+#include "libpentobi_base/PieceInfo.h"
+
+namespace libboardgame_base { class Transform; }
+namespace libpentobi_base { class Board; }
+
+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)
+
+ /** List of QPointF instances with coordinates of piece elements. */
+ Q_PROPERTY(QVariantList elements MEMBER m_elements CONSTANT)
+
+ /** List of QPointF instances with coordinates of piece junctions.
+ Only used in Nexos. */
+ Q_PROPERTY(QVariantList junctions MEMBER m_junctions CONSTANT)
+
+ /** 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. */
+ Q_PROPERTY(QVariantList junctionType MEMBER m_junctionType CONSTANT)
+
+ Q_PROPERTY(QPointF center MEMBER m_center CONSTANT)
+
+ /** Position of the label in board coordinates relative to center. */
+ Q_PROPERTY(QPointF labelPos MEMBER m_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(QString moveLabel READ moveLabel NOTIFY moveLabelChanged)
+ Q_PROPERTY(QPointF gameCoord READ gameCoord NOTIFY gameCoordChanged)
+
+public:
+ static QPointF findCenter(const Board& bd, const PiecePoints& points,
+ bool usePieceInfoPointTypes);
+
+ PieceModel(QObject* parent, const Board& bd, Piece piece, Color c);
+
+ int color() { return m_color.to_int(); }
+
+ QString state() const { return m_state; }
+
+ bool isPlayed() const { return m_isPlayed; }
+
+ bool isLastMove() const { return m_isLastMove; }
+
+ QString moveLabel() const { return m_moveLabel; }
+
+ QPointF gameCoord() const { return m_gameCoord; }
+
+ 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 setMoveLabel(const QString& moveLabel);
+
+ void setGameCoord(QPointF gameCoord);
+
+ Q_INVOKABLE void rotateLeft();
+
+ Q_INVOKABLE void rotateRight();
+
+ Q_INVOKABLE void flipAcrossX();
+
+ Q_INVOKABLE void flipAcrossY();
+
+ Q_INVOKABLE void nextOrientation();
+
+ Q_INVOKABLE void previousOrientation();
+
+signals:
+ void stateChanged();
+
+ void isPlayedChanged();
+
+ void isLastMoveChanged();
+
+ void gameCoordChanged();
+
+ void moveLabelChanged();
+
+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;
+
+ QString m_moveLabel;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_PIECE_MODEL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/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>
+#include "GameModel.h"
+
+using namespace std;
+using libboardgame_util::clear_abort;
+using libboardgame_util::set_abort;
+
+//-----------------------------------------------------------------------------
+
+bool PlayerModel::noBook = false;
+
+bool PlayerModel::noDelay = false;
+
+unsigned PlayerModel::nuThreads = 0;
+
+#ifdef Q_OS_ANDROID
+unsigned PlayerModel::maxLevel = 7;
+#else
+unsigned PlayerModel::maxLevel = 9;
+#endif
+
+PlayerModel::PlayerModel(QObject* parent)
+ : QObject(parent)
+{
+ try
+ {
+ m_player = make_unique<Player>(GameModel::getInitialGameVariant(),
+ maxLevel, "", nuThreads);
+ m_notEnoughMemory = false;
+ }
+ catch (const bad_alloc&)
+ {
+ m_notEnoughMemory = true;
+ return;
+ }
+ if (noBook)
+ m_player->set_use_book(false);
+ m_player->get_search().set_callback(
+ [this](double elapsedSeconds, double remainingSeconds) {
+ emit searchCallback(elapsedSeconds, remainingSeconds);
+ });
+ connect(&m_watcher, &QFutureWatcher<GenMoveResult>::finished,
+ this, &PlayerModel::genMoveFinished);
+}
+
+PlayerModel::~PlayerModel()
+{
+ cancelGenMove();
+}
+
+PlayerModel::GenMoveResult PlayerModel::asyncGenMove(
+ GameModel* gm, Color c, unsigned genMoveId)
+{
+ QElapsedTimer timer;
+ timer.start();
+ auto& bd = gm->getBoard();
+ GenMoveResult result;
+ result.color = c;
+ 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(static_cast<unsigned long>(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_watcher.waitForFinished();
+ setIsGenMoveRunning(false);
+}
+
+void PlayerModel::genMoveFinished()
+{
+ auto result = m_watcher.future().result();
+ if (result.genMoveId != m_genMoveId)
+ // Callback from a canceled move generation
+ return;
+ setIsGenMoveRunning(false);
+ auto& bd = result.gameModel->getBoard();
+ ColorMove mv(result.color, result.move);
+ if (mv.is_null())
+ {
+ qWarning("PlayerModel: failed to generate move");
+ return;
+ }
+ if (! bd.is_legal(mv.color, mv.move))
+ {
+ qWarning("PlayerModel: player generated illegal move");
+ return;
+ }
+ emit moveGenerated(new GameMove(this, mv));
+}
+
+void PlayerModel::loadBook(Variant variant)
+{
+ QFile file(QStringLiteral(":/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::setGameVariant(const QString& gameVariant)
+{
+ if (m_gameVariant == gameVariant)
+ return;
+ m_gameVariant = gameVariant;
+ emit gameVariantChanged();
+}
+
+void PlayerModel::setIsGenMoveRunning(bool isGenMoveRunning)
+{
+ if (m_isGenMoveRunning == isGenMoveRunning)
+ return;
+ m_isGenMoveRunning = isGenMoveRunning;
+ emit isGenMoveRunningChanged();
+}
+
+void PlayerModel::setLevel(unsigned level)
+{
+ if (m_level == level)
+ return;
+ m_level = level;
+ emit levelChanged();
+}
+
+void PlayerModel::startGenMove(GameModel* gameModel)
+{
+ auto& bd = gameModel->getBoard();
+ cancelGenMove();
+ auto level = m_level;
+ if (level < 1)
+ {
+ qDebug() << "Invalid level:" << level << "using 1";
+ level = 1;
+ }
+ else if (level > maxLevel)
+ {
+ qDebug() << "Invalid level:" << level << "using" << maxLevel;
+ level = maxLevel;
+ }
+ m_player->set_level(level);
+ auto variant = gameModel->getBoard().get_variant();
+ if (! m_player->is_book_loaded(variant))
+ loadBook(variant);
+ clear_abort();
+ ++m_genMoveId;
+ QFuture<GenMoveResult> future =
+ QtConcurrent::run(this, &PlayerModel::asyncGenMove, gameModel,
+ bd.get_effective_to_play(), m_genMoveId);
+ m_watcher.setFuture(future);
+ setIsGenMoveRunning(true);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/PlayerModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_PLAYER_MODEL_H
+#define PENTOBI_PLAYER_MODEL_H
+
+#include <QFutureWatcher>
+#include "libpentobi_mcts/Player.h"
+
+class GameModel;
+class GameMove;
+
+using namespace std;
+using libpentobi_base::Color;
+using libpentobi_base::Move;
+using libpentobi_base::Variant;
+using libpentobi_mcts::Player;
+using libpentobi_mcts::Search;
+
+//-----------------------------------------------------------------------------
+
+class PlayerModel
+ : public QObject
+{
+ Q_OBJECT
+
+ /** Game variant should be bound to GameModel.gameVariant.
+ This automatically updates the level property to the stored level for
+ the current game variant. The level will also be updated on
+ startGenMove() but the user interface might want to display the current
+ level immediately after changing the game variant. */
+ Q_PROPERTY(QString gameVariant READ gameVariant WRITE setGameVariant NOTIFY gameVariantChanged)
+
+ Q_PROPERTY(unsigned level READ level WRITE setLevel NOTIFY levelChanged)
+ Q_PROPERTY(bool isGenMoveRunning READ isGenMoveRunning NOTIFY isGenMoveRunningChanged)
+ Q_PROPERTY(unsigned maxLevel MEMBER maxLevel CONSTANT)
+
+public:
+ /** Global variable to disable opening books.
+ Must be set before creating any instances of PlayerModel and not be
+ changed afterwards. */
+ static bool noBook;
+
+ /** Global variable to disable the minimum thinking time.
+ Must be set before creating any instances of PlayerModel and not be
+ changed afterwards. */
+ 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. Must be set before creating any instances of PlayerModel and
+ not be changed afterwards. */
+ static unsigned nuThreads;
+
+ /** Global variable to set the maximum level.
+ Must be set before creating any instances of PlayerModel and not be
+ changed afterwards. */
+ static unsigned maxLevel;
+
+
+ explicit PlayerModel(QObject* parent = nullptr);
+
+ ~PlayerModel() override;
+
+
+ /** Check if the player creation failed because of low memory.
+ This is an expected error condition because the player allocates a
+ large amount of memory. This function must be checked after object
+ creation and no functions may be called if it returns true. */
+ Q_INVOKABLE bool notEnoughMemory() const { return m_notEnoughMemory; }
+
+ /** 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);
+
+ /** Cancel the move generation in the background thread if one is
+ running. */
+ Q_INVOKABLE void cancelGenMove();
+
+ const QString& gameVariant() const { return m_gameVariant; }
+
+ void setGameVariant(const QString& gameVariant);
+
+ unsigned level() const { return m_level; }
+
+ void setLevel(unsigned level);
+
+ bool isGenMoveRunning() const { return m_isGenMoveRunning; }
+
+ Search& getSearch() { return m_player->get_search(); }
+
+signals:
+ void gameVariantChanged();
+
+ void levelChanged();
+
+ void isGenMoveRunningChanged();
+
+ void moveGenerated(GameMove* move);
+
+ void searchCallback(double elapsedSeconds, double remainingSeconds);
+
+private:
+ struct GenMoveResult
+ {
+ Color color;
+
+ Move move;
+
+ unsigned genMoveId;
+
+ GameModel* gameModel;
+ };
+
+
+ bool m_notEnoughMemory;
+
+ bool m_isGenMoveRunning = false;
+
+ QString m_gameVariant;
+
+ unsigned m_level = 1;
+
+ unsigned m_genMoveId = 0;
+
+ unique_ptr<Player> m_player;
+
+ QFutureWatcher<GenMoveResult> m_watcher;
+
+
+ GenMoveResult asyncGenMove(GameModel* gm, Color c, unsigned genMoveId);
+
+ void genMoveFinished();
+
+ void loadBook(Variant variant);
+
+ void setIsGenMoveRunning(bool isGenMoveRunning);
+
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_PLAYER_MODEL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingModel.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "RatingModel.h"
+
+#include <random>
+#include <QDebug>
+#include <QDir>
+#include <QFileInfo>
+#include <QSettings>
+#include <QStandardPaths>
+#include "GameModel.h"
+#include "libpentobi_base/Variant.h"
+#include "libpentobi_mcts/Player.h"
+
+using namespace std;
+using libpentobi_base::Variant;
+using libpentobi_mcts::Player;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+const int maxSavedGames = 50;
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+RatedGameInfo::RatedGameInfo(QObject* parent, int number, int color,
+ double result, const QString& date, int level,
+ double rating)
+ : QObject(parent),
+ m_number(number),
+ m_color(color),
+ m_level(level),
+ m_result(result),
+ m_rating(rating),
+ m_date(date)
+{
+}
+
+//-----------------------------------------------------------------------------
+
+void RatingModel::addResult(GameModel* gameModel, int level)
+{
+ Variant variant;
+ if (! parse_variant_id(m_gameVariant.toLocal8Bit().constData(), variant))
+ return;
+ unsigned place;
+ bool isPlaceShared;
+ auto color = getNextHumanPlayer();
+ gameModel->getBoard().get_place(Color(static_cast<Color::IntType>(color)),
+ place, isPlaceShared);
+ double gameResult;
+ if (place == 0 && ! isPlaceShared)
+ gameResult = 1;
+ else if (place == 0 && isPlaceShared)
+ gameResult = 0.5;
+ else
+ gameResult = 0;
+ auto nuOpponents = static_cast<unsigned>(get_nu_players(variant) - 1);
+ Rating opponentRating =
+ Player::get_rating(variant, static_cast<unsigned>(level));
+ double kValue = (m_numberGames < 30 ? 40 : 20);
+ Rating rating = m_rating;
+ rating.update(gameResult, opponentRating, kValue, nuOpponents);
+ setRating(rating.get());
+ auto numberGames = m_numberGames + 1;
+ if (numberGames == 1 || rating.get() > m_bestRating.get())
+ setBestRating(rating.get());
+ auto date = QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd"));
+ m_history.prepend(new RatedGameInfo(this, numberGames, color, gameResult,
+ date, level, m_rating.get()));
+ auto file = getFile(numberGames);
+ QFileInfo(file).dir().mkpath(QStringLiteral("."));
+ gameModel->save(file);
+ emit historyChanged();
+ setNumberGames(numberGames);
+ saveSettings();
+}
+
+void RatingModel::clearRating()
+{
+ if (! m_history.isEmpty())
+ {
+ m_history.clear();
+ emit historyChanged();
+ }
+ QDir(getDir()).removeRecursively();
+ setRating(1000);
+ setBestRating(1000);
+ setNumberGames(0);
+}
+
+QString RatingModel::getDir() const
+{
+ return QStringLiteral("%1/Rated Games/%2").arg(
+ QStandardPaths::writableLocation(QStandardPaths::AppDataLocation),
+ m_gameVariantName);
+}
+
+QString RatingModel::getFile(int gameNumber) const
+{
+ return QStringLiteral("%1/%2 %3.blksgf").arg(
+ getDir(), m_gameVariantName, QString::number(gameNumber));
+}
+
+int RatingModel::getGameNumberOfFile(const QString& file) const
+{
+ QString left = QStringLiteral("%1/%2 ").arg(getDir(), m_gameVariantName);
+ if (! file.startsWith(left))
+ return 0;
+ QString right = QStringLiteral(".blksgf");
+ if (! file.endsWith(right))
+ return 0;
+ auto leftLen = left.length();
+ auto rightLen = right.length();
+ int n;
+ bool ok;
+ n = QStringRef(&file, leftLen,
+ file.length() - leftLen - rightLen).toInt(&ok);
+ return ok && n >= 1 && n <= m_numberGames ? n : 0;
+}
+
+int RatingModel::getNextHumanPlayer() const
+{
+ Variant variant;
+ if (! parse_variant_id(m_gameVariant.toLocal8Bit().constData(), variant))
+ return 0;
+ mt19937 generator;
+ generator.discard(static_cast<unsigned>(m_numberGames));
+ uniform_int_distribution<> distribution(0, get_nu_players(variant) - 1);
+ return distribution(generator);
+}
+
+int RatingModel::getNextLevel(int maxLevel) const
+{
+ Variant variant;
+ if (! parse_variant_id(m_gameVariant.toLocal8Bit().constData(), variant))
+ return 1;
+ int level = 1; // Initialize to avoid compiler warning
+ double minDiff = 0; // Initialize to avoid compiler warning
+ for (int i = 1; i <= maxLevel; ++i)
+ {
+ auto diff =
+ abs(m_rating.get()
+ - Player::get_rating(variant, static_cast<unsigned>(i)).get());
+ if (i == 1 || diff < minDiff)
+ {
+ minDiff = diff;
+ level = i;
+ }
+ }
+ return level;
+}
+
+void RatingModel::saveSettings()
+{
+ QSettings settings(QStringLiteral("%1/%2.ini").arg(getDir(),
+ m_gameVariantName),
+ QSettings::IniFormat);
+ if (m_numberGames == 0)
+ {
+ settings.remove(QStringLiteral("rated_games"));
+ settings.remove(QStringLiteral("rating"));
+ settings.remove(QStringLiteral("best_rating"));
+ }
+ else
+ {
+ settings.setValue(QStringLiteral("rated_games"), m_numberGames);
+ settings.setValue(QStringLiteral("rating"), m_rating.get());
+ settings.setValue(QStringLiteral("best_rating"),
+ round(m_bestRating.get()));
+ }
+ QList<QObject*> newHistory;
+ newHistory.reserve(m_history.size());
+ for (auto& i : m_history)
+ {
+ auto& info = dynamic_cast<RatedGameInfo&>(*i);
+ if (info.number() <= m_numberGames - maxSavedGames)
+ QFile::remove(getFile(info.number()));
+ else
+ newHistory.append(&info);
+ }
+ if (newHistory.size() != m_history.size())
+ {
+ m_history = newHistory;
+ emit historyChanged();
+ }
+ settings.remove(QStringLiteral("rated_game_info"));
+ if (m_numberGames > 0)
+ {
+ settings.beginWriteArray(QStringLiteral("rated_game_info"));
+ int n = 0;
+ for (auto& i : m_history)
+ {
+ auto& info = dynamic_cast<RatedGameInfo&>(*i);
+ if (info.number() <= m_numberGames - maxSavedGames)
+ continue;
+ settings.setArrayIndex(n++);
+ settings.setValue(QStringLiteral("number"), info.number());
+ settings.setValue(QStringLiteral("color"), info.color());
+ settings.setValue(QStringLiteral("result"), info.result());
+ settings.setValue(QStringLiteral("date"), info.date());
+ settings.setValue(QStringLiteral("level"), info.level());
+ settings.setValue(QStringLiteral("rating"), round(info.rating()));
+ }
+ settings.endArray();
+ }
+}
+
+void RatingModel::setBestRating(double rating)
+{
+ m_bestRating = Rating(rating);
+ emit bestRatingChanged();
+}
+
+void RatingModel::setGameVariant(const QString& gameVariant)
+{
+ if (m_gameVariant == gameVariant)
+ return;
+ Variant variant;
+ if (! libpentobi_base::parse_variant_id(
+ gameVariant.toLocal8Bit().constData(), variant))
+ {
+ qDebug() << "Invalid game variant" << gameVariant;
+ return;
+ }
+ m_gameVariant = gameVariant;
+ m_gameVariantName =
+ QString::fromLocal8Bit(libpentobi_base::to_string(variant));
+ QSettings settings(QStringLiteral("%1/%2.ini").arg(getDir(),
+ m_gameVariantName),
+ QSettings::IniFormat);
+ auto currentRating =
+ settings.value(QStringLiteral("rating"), 1000).toDouble();
+ auto bestRating =
+ settings.value(QStringLiteral("best_rating"), 0).toDouble();
+ setRating(currentRating);
+ setBestRating(bestRating);
+ m_history.clear();
+ auto size = settings.beginReadArray(QStringLiteral("rated_game_info"));
+ for (int i = 0; i < size; ++i)
+ {
+ settings.setArrayIndex(i);
+ auto number = settings.value(QStringLiteral("number")).toInt();
+ auto color = settings.value(QStringLiteral("color")).toInt();
+ auto result = settings.value(QStringLiteral("result")).toDouble();
+ auto date = settings.value(QStringLiteral("date")).toString();
+ auto level = settings.value(QStringLiteral("level")).toInt();
+ auto rating = settings.value(QStringLiteral("rating")).toDouble();
+ m_history.append(new RatedGameInfo(this, number, color, result, date,
+ level, rating));
+ }
+ settings.endArray();
+ sort(m_history.begin(), m_history.end(),
+ [](const QObject* o1, const QObject* o2)
+ {
+ return dynamic_cast<const RatedGameInfo&>(*o1).number()
+ > dynamic_cast<const RatedGameInfo&>(*o2).number();
+ });
+ emit historyChanged();
+ setNumberGames(settings.value(QStringLiteral("rated_games"), 0).toInt());
+ emit gameVariantChanged();
+}
+
+void RatingModel::setInitialRating(double rating)
+{
+ setRating(rating);
+ setBestRating(rating);
+ saveSettings();
+}
+
+void RatingModel::setNumberGames(int numberGames)
+{
+ if (numberGames < 0)
+ {
+ qWarning("RatingModel: invalid number of games");
+ return;
+ }
+ if (m_numberGames == numberGames)
+ return;
+ m_numberGames = numberGames;
+ emit numberGamesChanged();
+}
+
+void RatingModel::setRating(double rating)
+{
+ m_rating = Rating(rating);
+ emit ratingChanged();
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/RatingModel.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_RATING_MODEL_H
+#define PENTOBI_RATING_MODEL_H
+
+#include <QObject>
+#include "libboardgame_base/Rating.h"
+
+class GameModel;
+
+using libboardgame_base::Rating;
+
+//-----------------------------------------------------------------------------
+
+class RatedGameInfo
+ : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int number READ number CONSTANT)
+
+ /** 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. */
+ Q_PROPERTY(int color READ color CONSTANT)
+
+ /** Game result.
+ 0=Loss, 0.5=tie, 1=win from the viewpoint of the human. */
+ Q_PROPERTY(double result READ result CONSTANT)
+
+ /** Date of the game in "YYYY-MM-DD" format. */
+ Q_PROPERTY(QString date READ date CONSTANT)
+
+ /** The playing level of the computer opponent. */
+ Q_PROPERTY(int level READ level CONSTANT)
+
+ /** The rating of the human after the game. */
+ Q_PROPERTY(double rating READ rating CONSTANT)
+
+public:
+ RatedGameInfo(QObject* parent, int number, int color, double result,
+ const QString& date, int level, double rating);
+
+ int number() const { return m_number; }
+
+ int color() const { return m_color; }
+
+ double result() const { return m_result; }
+
+ const QString& date() const { return m_date; }
+
+ int level() const { return m_level; }
+
+ double rating() const { return m_rating; }
+
+private:
+ int m_number;
+
+ int m_color;
+
+ int m_level;
+
+ double m_result;
+
+ double m_rating;
+
+ QString m_date;
+};
+
+//-----------------------------------------------------------------------------
+
+class RatingModel
+ : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(double bestRating READ bestRating NOTIFY bestRatingChanged)
+ Q_PROPERTY(QString gameVariant MEMBER m_gameVariant WRITE setGameVariant NOTIFY gameVariantChanged)
+ Q_PROPERTY(QList<QObject*> history READ history NOTIFY historyChanged)
+ Q_PROPERTY(int numberGames READ numberGames NOTIFY numberGamesChanged)
+ Q_PROPERTY(double rating READ rating NOTIFY ratingChanged)
+
+public:
+ using QObject::QObject;
+
+
+ Q_INVOKABLE void addResult(GameModel* gameModel, int level);
+
+ Q_INVOKABLE void clearRating();
+
+ Q_INVOKABLE int getNextHumanPlayer() const;
+
+ Q_INVOKABLE int getNextLevel(int maxLevel) const;
+
+ Q_INVOKABLE void setInitialRating(double rating);
+
+ Q_INVOKABLE QString getFile(int gameNumber) const;
+
+ /** Get the game number corresponding to a file.
+ @return The game number or 0 if file is not a rated game. */
+ Q_INVOKABLE int getGameNumberOfFile(const QString& file) const;
+
+
+ double bestRating() const { return m_bestRating.get(); }
+
+ const QList<QObject*>& history() const { return m_history; }
+
+ int numberGames() const { return m_numberGames; }
+
+ double rating() const { return m_rating.get(); }
+
+ void setGameVariant(const QString& gameVariant);
+
+signals:
+ void bestRatingChanged();
+
+ void gameVariantChanged();
+
+ void historyChanged();
+
+ void numberGamesChanged();
+
+ void ratingChanged();
+
+private:
+ int m_numberGames = 0;
+
+ Rating m_bestRating = Rating(1000.);
+
+ Rating m_rating = Rating(1000.);
+
+ QString m_gameVariant;
+
+ QString m_gameVariantName;
+
+ QList<QObject*> m_history;
+
+
+ QString getDir() const;
+
+ void saveSettings();
+
+ void setBestRating(double rating);
+
+ void setRating(double rating);
+
+ void setNumberGames(int numberGames);
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_RATING_MODEL_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/SyncSettings.h
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#ifndef PENTOBI_SYNC_SETTINGS_H
+#define PENTOBI_SYNC_SETTINGS_H
+
+#include <QSettings>
+
+//-----------------------------------------------------------------------------
+
+/** Settings with sync function.
+ This item does not use property bindings like Qt.labs.settings.Settings,
+ but get/set methods for getting and setting values at defined times. It
+ also provides a sync() method, which is necessary to ensure that pending
+ changes are written in case the application in killed on Android soon after
+ having been suspended (see QTBUG-70291). */
+class SyncSettings
+ : public QObject
+{
+ Q_OBJECT
+
+public:
+ using QObject::QObject;
+
+ Q_INVOKABLE bool valueBool(const QString& key, bool defaultValue) {
+ return m_settings.value(key, defaultValue).toBool(); }
+
+ Q_INVOKABLE int valueInt(const QString& key, int defaultValue) {
+ return m_settings.value(key, defaultValue).toInt(); }
+
+ Q_INVOKABLE void setValueBool(const QString& key, bool value) {
+ m_settings.setValue(key, value); }
+
+ Q_INVOKABLE void setValueInt(const QString& key, int value) {
+ m_settings.setValue(key, value); }
+
+ Q_INVOKABLE void sync() { m_settings.sync(); }
+
+private:
+ QSettings m_settings;
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // PENTOBI_SYNC_SETTINGS_H
--- /dev/null
+<?xml version="1.0"?>
+<manifest package="net.sf.pentobi" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="16.2" android:versionCode="160020" 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" android:windowSoftInputMode="adjustPan">
+ <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"/>
+ <!-- Splash screen -->
+ <meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splash"/>
+ <!-- extract android style -->
+ <!-- available android:values :
+ * default - In most cases this will be the same as "full", but it can also be something else if needed, e.g., for compatibility reasons
+ * full - useful QWidget & Quick Controls 1 apps
+ * minimal - useful for Quick Controls 2 apps, it is much faster than "full"
+ * none - useful for apps that don't use any of the above Qt modules
+ -->
+ <meta-data android:name="android.app_extract_android_style" android:value="minimal"/>
+ <!-- Background running -->
+ <meta-data android:name="android.app.background_running" android:value="false"/>
+ <!-- Auto screen scale factor -->
+ <meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
+ </activity>
+ </application>
+ <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="28"/>
+ <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <!-- %%INSERT_FEATURES -->
+</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="http://www.w3.org/2000/svg" height="48" width="48" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(30.406 -14.813)">
+ <path opacity=".99" fill="#c00" d="m-24.83 16.813c-1.9807 0-3.5762 1.5955-3.5762 3.5762v7.4238h11v-11h-7.4238z" fill-rule="evenodd"/>
+ <path d="m-28.406 27.813 1-1.0005h9v-9l1.0003-1v11z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-24.83 16.813c-0.96479 0-1.8347 0.383-2.4766 1h8.9004l1-1h-7.4238zm-2.5762 1.0996c-0.617 0.6419-1 1.5118-1 2.4766v7.4238l1-1v-8.9004z" fill-opacity=".15686"/>
+ </g>
+ <g id="c" transform="matrix(1.1 0 0 1.1 18.5 -6.7992)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+ <path d="m-15 28 0.9091-0.90942h8.1819v-8.1819l0.90935-0.90926v10z" fill-opacity=".15686"/>
+ <path fill-opacity=".15686" d="m-15 28v-10h10l-0.90902 0.90863h-8.1819v8.1819z" fill="#fff"/>
+ </g>
+ <use xlink:href="#c" transform="translate(1.2222e-8 11)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#c" transform="translate(11 22)" height="72" width="72" y="0" x="0"/>
+ <g id="b" transform="matrix(1.1 0 0 1.1 29.5 4.2007)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+ <path d="m-15 28 0.90918-0.90926h8.1819v-8.1819l0.90926-0.90942v10z" fill-opacity=".15686"/>
+ <path fill-opacity=".15686" d="m-15 28v-10h10l-0.90893 0.90879h-8.1819v8.1819z" fill="#fff"/>
+ </g>
+ <use xlink:href="#b" transform="translate(11 11)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#b" transform="translate(11)" height="72" width="72" y="0" x="0"/>
+ <g id="a" transform="matrix(1.1 0 0 1.1 40.5 -17.799)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+ <path d="m-15 28 0.90926-0.90958h8.1819v-8.1819l0.90918-0.90911v10z" fill-opacity=".15686"/>
+ <path fill-opacity=".15686" d="m-15 28v-10h10l-0.90885 0.90848h-8.1819v8.1819z" fill="#fff"/>
+ </g>
+ <use xlink:href="#a" transform="translate(11 11)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(11 22)" height="72" width="72" y="0" x="0"/>
+ <g id="e" transform="matrix(1.1 0 0 1.0999 29.499 -17.798)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+ <path d="m-15 28 0.9091-0.90958h8.1819v-8.1819l0.90935-0.90911v10z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-15 28v-10h10l-0.90902 0.90848h-8.1819v8.1819z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(39.928 .74318)">
+ <g transform="translate(-9.7734)">
+ <path opacity=".99" fill="#edd400" d="m-28.154 34.257v7.4238c-0.000001 1.9807 1.5955 3.5762 3.5762 3.5762h7.4238v-11h-11z" fill-rule="evenodd"/>
+ <path d="m-17.154 34.257-1 1v9h-8.9004c0.6419 0.617 1.5118 1 2.4766 1h7.4238v-11z" fill-opacity=".15686"/>
+ </g>
+ <path fill="#fff" d="m-37.928 34.257v7.4258c0 0.96478 0.38301 1.8327 1 2.4746v-8.9004h9l1-1h-11z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(74.315 17.385)">
+ <path opacity=".99" fill="#3465a4" d="m-39.314 17.615v11h7.4238c1.974 0 3.5635-1.5833 3.5742-3.5547v-7.4453h-10.998z" fill-rule="evenodd"/>
+ <path d="m-28.315 17.615-1 1v8.9004c0.61705-0.64191 1-1.5117 1-2.4766v-7.4238zm-10 10-1 1h7.4258c0.96473 0 1.8327-0.38306 2.4746-1h-8.9004z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-39.315 17.615v11l1-1v-9h9l1-1h-11z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(60.33 -21.321)">
+ <g transform="translate(25.231 1.9445)">
+ <path opacity=".99" fill="#73d216" d="m-50.561 21.376v11h11v-7.4238c0-1.9807-1.5955-3.5762-3.5762-3.5762h-7.4238z" fill-rule="evenodd"/>
+ <path fill="#fff" d="m-50.561 21.376v11l1-1v-9h8.9004c-0.64189-0.61694-1.5099-1-2.4746-1h-7.4258z" fill-opacity=".15686"/>
+ </g>
+ <path d="m-15.33 24.42v8.9004h-9l-1 1h11v-7.4238c0-0.96479-0.383-1.8347-1-2.4766z" fill-opacity=".15686"/>
+ </g>
+ <use id="d" xlink:href="#e" transform="translate(11.001 11.001)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#d" transform="translate(-11 .00072815)" height="100%" width="100%" y="0" x="0"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="72" width="72" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(47.16 -34.465)">
+ <path opacity=".99" fill="#c00" d="m-38.16 36.465c-3.878 0-7 3.122-7 7v10h17v-17h-10z" fill-rule="evenodd"/>
+ <path d="m-28.16 36.465-2 2v13h-13l-2 2h17v-17z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-38.16 36.465c-1.915 0-3.6449 0.76228-4.9062 2h12.906l2-2h-10zm-5 2.0938c-1.2377 1.2614-2 2.9912-2 4.9062v10l2-2v-12.906z" fill-opacity=".15686"/>
+ </g>
+ <g id="c" transform="matrix(1.7 0 0 1.7 27.5 -11.6)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#edd400"/>
+ <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+ <path fill-opacity=".15686" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill="#fff"/>
+ </g>
+ <use xlink:href="#c" transform="translate(1.8889e-8 17)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#c" transform="translate(17 34)" height="72" width="72" y="0" x="0"/>
+ <g id="b" transform="matrix(1.7 0 0 1.7 44.5 5.4)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#3465a4"/>
+ <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+ <path fill-opacity=".15686" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill="#fff"/>
+ </g>
+ <use xlink:href="#b" transform="translate(17 17)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#b" transform="translate(17 4.0042e-7)" height="72" width="72" y="0" x="0"/>
+ <g id="a" transform="matrix(1.7 0 0 1.7 61.5 -28.6)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#73d216"/>
+ <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+ <path fill-opacity=".15686" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill="#fff"/>
+ </g>
+ <use xlink:href="#a" transform="translate(17 17)" height="72" width="72" y="0" x="0"/>
+ <use xlink:href="#a" transform="translate(17 34)" height="72" width="72" y="0" x="0"/>
+ <g id="e" transform="matrix(1.7 0 0 1.7 44.5 -28.6)">
+ <rect opacity=".99" fill-rule="evenodd" rx="0" ry="0" height="10" width="10" y="18" x="-15" fill="#c00"/>
+ <path d="m-15 28 1.1765-1.1764h7.6471v-7.6471l1.1765-1.1764v10z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-15 28v-10h10l-1.1764 1.1765h-7.6471v7.6471z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(45.776 37.573)">
+ <path opacity=".99" fill="#edd400" d="m-43.776 15.427v10c0 3.878 3.122 7 7 7h10v-17h-17z" fill-rule="evenodd"/>
+ <path d="m-26.776 15.427-2 2v13h-12.904c1.2614 1.2377 2.9893 2 4.9043 2h10v-17z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-43.776 15.427v10c0 1.9141 0.76336 3.6431 2 4.9043v-12.904h13l2-2h-17z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(96.877 35.451)">
+ <path opacity=".99" fill="#3465a4" d="m-43.877 17.549v17h10c3.878 0 7-3.122 7-7v-10h-17z" fill-rule="evenodd"/>
+ <path d="m-26.877 17.549-2 2v12.904c1.2366-1.2612 2-2.9903 2-4.9043v-10zm-15 15-2 2h10c1.915 0 3.6429-0.76229 4.9043-2h-12.904z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-43.877 17.549v17l2-2v-13h13l2-2h-17z" fill-opacity=".15686"/>
+ </g>
+ <g transform="translate(119.89 -16.717)">
+ <path opacity=".99" fill="#73d216" d="m-66.889 18.717v17h17v-10c0-3.878-3.122-7-7-7h-10z" fill-rule="evenodd"/>
+ <path d="m-51.889 20.814v12.902h-13l-2 2h17v-10c0-1.914-0.76342-3.6411-2-4.9023z" fill-opacity=".15686"/>
+ <path fill="#fff" d="m-66.889 18.717v17l2-2v-13h12.904c-1.2614-1.2377-2.9893-2-4.9043-2h-10z" fill-opacity=".15686"/>
+ </g>
+ <use id="d" xlink:href="#e" transform="translate(17 17)" height="100%" width="100%" y="0" x="0"/>
+ <use xlink:href="#d" transform="translate(-17,-7e-5)" height="100%" width="100%" y="0" x="0"/>
+</svg>
--- /dev/null
+import QtQuick 2.11
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ footer: Pentobi.DialogButtonBox { ButtonClose { } }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(column.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: column.implicitHeight
+
+ Column {
+ id: column
+
+ anchors.fill: parent
+ spacing: 0.5 * font.pixelSize
+ leftPadding: spacing
+ rightPadding: leftPadding
+
+ Image {
+ source: "qrc:pentobi_icon/pentobi-64.svg"
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ Label {
+ //: The argument is the application version.
+ text: qsTr("Pentobi %1").arg(Qt.application.version)
+ font {
+ bold: true
+ pixelSize: 1.3 * root.font.pixelSize
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ Label {
+ text: qsTr("Computer opponent for the board game Blokus")
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ width: Math.min(implicitWidth, maxContentWidth)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ Label {
+ text: "<a href=\"https://pentobi.sourceforge.io\" style=\"text-decoration:none\">pentobi.sourceforge.io</a>"
+ textFormat: Text.RichText
+ elide: Qt.ElideRight
+ width: Math.min(implicitWidth, maxContentWidth)
+ anchors.horizontalCenter: parent.horizontalCenter
+ onLinkActivated: Qt.openUrlExternally(link)
+
+ MouseArea {
+ enabled: isDesktop
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.NoButton
+ cursorShape: containsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor
+ }
+ }
+ Label {
+ text: qsTr("Copyright © 2011–%1 Markus Enzenberger").arg(2019)
+ font.pixelSize: 0.9 * root.font.pixelSize
+ opacity: 0.8
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignHCenter
+ width: Math.min(implicitWidth, maxContentWidth)
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AnalyzeDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+ footer: DialogButtonBoxOkCancel { }
+ onAccepted: {
+ var nuSimulations
+ switch (comboBox.currentIndex) {
+ case 2: nuSimulations = 75000; break
+ case 1: nuSimulations = 15000; break
+ default: nuSimulations = 3000
+ }
+ Logic.analyzeGame(nuSimulations)
+ }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: columnLayout.implicitHeight
+
+ ColumnLayout {
+ id: columnLayout
+
+ anchors.fill: parent
+
+ Label {
+ id: label
+
+ Layout.fillWidth: true
+ text: qsTr("Analysis speed:")
+ }
+ ComboBox {
+ id: comboBox
+
+ model: [ qsTr("Fast"), qsTr("Normal"), qsTr("Slow") ]
+ Layout.fillWidth: true
+ Layout.preferredWidth: font.pixelSize * 15
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AnalyzeGame.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+
+Item {
+ id: root
+
+ property var elements: analyzeGameModel.elements
+ property var color: [ color0[0], color1[0], color2[0], color3[0] ]
+ property int markMoveNumber: analyzeGameModel.markMoveNumber
+ property QtObject theme
+
+ property real margin
+ // Distance between moves on the x axis
+ property real dist
+
+ onElementsChanged: {
+ analyzeGameModel.markCurrentMove(gameModel)
+ canvas.requestPaint()
+ }
+ onMarkMoveNumberChanged: canvas.requestPaint()
+ onThemeChanged: canvas.requestPaint()
+
+ Canvas {
+ id: canvas
+
+ visible: elements.length > 0 || analyzeGameModel.isRunning
+ anchors.fill: parent
+ antialiasing: true
+ onPaint: {
+ var elements = analyzeGameModel.elements
+ var nuMoves = elements.length
+ var w = width
+ var h = height
+ // Use the whole width unless few moves have been played
+ var nuBins = Math.ceil(Math.max(nuMoves, 50))
+ var d = w / nuBins
+ var ctx = getContext("2d")
+ ctx.fillStyle = theme.colorBackground
+ ctx.fillRect(0, 0, w, h)
+ ctx.strokeStyle = theme.colorCommentBorder
+ ctx.save()
+ ctx.translate(d / 2, d / 2)
+ w -= d
+ h -= d
+ ctx.beginPath()
+ ctx.moveTo(0, 0)
+ ctx.lineTo(w, 0)
+ ctx.moveTo(0, h)
+ ctx.lineTo(w, h)
+ ctx.stroke()
+ ctx.beginPath()
+ ctx.moveTo(0, h / 2)
+ ctx.lineTo(w, h / 2)
+ ctx.stroke()
+ var n = markMoveNumber
+ if (n >= 0 && n <= nuMoves) {
+ ctx.beginPath()
+ ctx.moveTo(n * d, 0)
+ ctx.lineTo(n * d, h)
+ ctx.stroke()
+ }
+ var radius = d / 2
+ var i
+ for (i = 0; i < nuMoves; ++i) {
+ ctx.beginPath()
+ ctx.fillStyle = color[elements[i].moveColor]
+ ctx.arc(i * d, h - elements[i].value * h,
+ radius, 0, 2 * Math.PI)
+ ctx.fill()
+ }
+ ctx.restore()
+ dist = d
+ margin = d / 2
+ }
+ }
+ Label {
+ visible: elements.length === 0 && ! analyzeGameModel.isRunning
+ anchors.centerIn: parent
+ color: theme.colorText
+ opacity: 0.8
+ text: qsTr("(No analysis)")
+ }
+ MouseArea {
+ anchors.fill: parent
+
+ onClicked: {
+ if (dist === 0 || elements.length === 0
+ || analyzeGameModel.isRunning)
+ return
+ var moveNumber = Math.round((mouseX - margin) / dist)
+ analyzeGameModel.gotoMove(gameModel, moveNumber)
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AppearanceDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ // Mobile layout may not have enough screen space for apply button and the
+ // immovable dialog will cover most of the screen anyway. Note that using
+ // the same ButtonBox for both and setting visible to false for ButtonApply
+ // if not desktop causes a binding loop for implicitWidth in Qt 5.11 and
+ // elided Text on the dialog buttons.
+ property DialogButtonBox footerDesktop: Pentobi.DialogButtonBox {
+ ButtonCancel { }
+ ButtonApply {
+ enabled:
+ checkBoxCoordinates.checked !== gameDisplay.showCoordinates
+ || checkBoxShowVariations.checked !== gameModel.showVariations
+ || checkBoxAnimatePieces.checked !== gameDisplay.enableAnimations
+ || checkBoxMoveNumber.checked !== gameDisplay.showMoveNumber
+ || comboBoxTheme.currentIndex !== currentThemeIndex
+ || comboBoxMoveMarking.currentIndex !== currentMoveMarkingIndex
+ || comboBoxComment.currentIndex !== currentCommentIndex
+ }
+ ButtonOk { }
+ }
+ property DialogButtonBox footerMobile: DialogButtonBoxOkCancel { }
+ property int currentThemeIndex
+ property int currentMoveMarkingIndex
+ property int currentCommentIndex
+
+ footer: isDesktop ? footerDesktop : footerMobile
+ onOpened: {
+ checkBoxCoordinates.checked = gameDisplay.showCoordinates
+ checkBoxShowVariations.checked = gameModel.showVariations
+ checkBoxAnimatePieces.checked = gameDisplay.enableAnimations
+ if (themeName === "dark")
+ currentThemeIndex = 1
+ else if (themeName === "colorblind-light")
+ currentThemeIndex = 2
+ else if (themeName === "colorblind-dark")
+ currentThemeIndex = 3
+ else if (themeName === "system")
+ currentThemeIndex = isAndroid ? 1 : 4
+ else
+ currentThemeIndex = 0
+ comboBoxTheme.currentIndex = currentThemeIndex
+ if (gameDisplay.moveMarking === "last_dot")
+ currentMoveMarkingIndex = 0
+ else if (gameDisplay.moveMarking === "last_number")
+ currentMoveMarkingIndex = 1
+ else if (gameDisplay.moveMarking === "all_number")
+ currentMoveMarkingIndex = 2
+ else if (gameDisplay.moveMarking === "none")
+ currentMoveMarkingIndex = 3
+ else
+ currentMoveMarkingIndex = 0
+ comboBoxMoveMarking.currentIndex = currentMoveMarkingIndex
+ if (isDesktop) {
+ checkBoxMoveNumber.checked = gameDisplay.showMoveNumber
+ if (gameDisplay.commentMode === "always")
+ currentCommentIndex = 0
+ else if (gameDisplay.commentMode === "never")
+ currentCommentIndex = 2
+ else
+ currentCommentIndex = 1
+ comboBoxComment.currentIndex = currentCommentIndex
+ }
+ }
+ onAccepted: {
+ gameDisplay.showCoordinates = checkBoxCoordinates.checked
+ gameModel.showVariations = checkBoxShowVariations.checked
+ gameDisplay.enableAnimations = checkBoxAnimatePieces.checked
+ switch (comboBoxTheme.currentIndex) {
+ case 0: themeName = "light"; break
+ case 1: themeName = "dark"; break
+ case 2: themeName = "colorblind-light"; break
+ case 3: themeName = "colorblind-dark"; break
+ case 4: themeName = "system"; break
+ }
+ switch (comboBoxMoveMarking.currentIndex) {
+ case 0: gameDisplay.moveMarking = "last_dot"; break
+ case 1: gameDisplay.moveMarking = "last_number"; break
+ case 2: gameDisplay.moveMarking = "all_number"; break
+ case 3: gameDisplay.moveMarking = "none"; break
+ }
+ if (isDesktop)
+ gameDisplay.showMoveNumber = checkBoxMoveNumber.checked
+ switch (comboBoxComment.currentIndex) {
+ case 0: gameDisplay.commentMode = "always"; break
+ case 1: gameDisplay.commentMode = "as_needed"; break
+ case 2: gameDisplay.commentMode = "never"; break
+ }
+ }
+ onApplied: {
+ onAccepted()
+ onOpened()
+ }
+
+ Flickable {
+ implicitWidth:
+ Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: Math.min(columnLayout.implicitHeight, maxContentHeight)
+ contentHeight: columnLayout.implicitHeight
+ clip: true
+
+ ColumnLayout {
+ id: columnLayout
+
+ anchors.fill: parent
+
+ CheckBox {
+ id: checkBoxCoordinates
+
+ text: qsTr("Coordinates")
+ }
+ CheckBox {
+ id: checkBoxShowVariations
+
+ text: qsTr("Show variations")
+ }
+ CheckBox {
+ id: checkBoxMoveNumber
+
+ visible: isDesktop
+ //: Check box in appearance dialog whether to show the
+ //: move number in the status bar.
+ text: qsTr("Move number")
+ }
+ CheckBox {
+ id: checkBoxAnimatePieces
+
+ text: qsTr("Animations")
+ }
+ Label {
+ text: qsTr("Color theme:")
+ Layout.topMargin: 0.5 * font.pixelSize
+
+
+ }
+ ComboBox {
+ id: comboBoxTheme
+
+ model: isAndroid ?
+ [
+ qsTr("Light"),
+ qsTr("Dark"),
+ qsTr("Colorblind light"),
+ qsTr("Colorblind dark")
+ ] :
+ [
+ qsTr("Light"),
+ qsTr("Dark"),
+ qsTr("Colorblind light"),
+ qsTr("Colorblind dark"),
+ //: Name of theme using default system colors
+ qsTr("System")
+ ]
+ Layout.preferredWidth: font.pixelSize * 20
+ Layout.fillWidth: true
+ }
+ Label {
+ text: qsTr("Move marking:")
+ Layout.topMargin: 0.5 * font.pixelSize
+
+
+ }
+ ComboBox {
+ id: comboBoxMoveMarking
+
+ model: [
+ qsTr("Last with dot"),
+ qsTr("Last with number"),
+ qsTr("All with number"),
+ //: Move marking/None
+ qsTr("None")
+ ]
+ Layout.preferredWidth: font.pixelSize * 20
+ Layout.fillWidth: true
+ }
+ Label {
+ visible: isDesktop
+ text: qsTr("Show comment:")
+ Layout.topMargin: 0.5 * font.pixelSize
+
+
+ }
+ ComboBox {
+ id: comboBoxComment
+
+ visible: isDesktop
+ model: [
+ //: Show-comment mode
+ qsTr("Always"),
+ //: Show-comment mode
+ qsTr("As needed"),
+ //: Show-comment mode
+ qsTr("Never")
+ ]
+ Layout.preferredWidth: font.pixelSize * 20
+ Layout.fillWidth: true
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/AsciiArtSaveDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+ title: qsTr("Export ASCII Art")
+ selectExisting: false
+ nameFilterLabels: [ qsTr("Text files") ]
+ nameFilters: [ [ "*.txt", "*.TXT" ] ]
+ folder: rootWindow.folder
+ onAccepted: {
+ rootWindow.folder = folder
+ Logic.exportAsciiArt(fileUrl)
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Board.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Window 2.2
+
+Item {
+ id: root
+
+ property string gameVariant: gameModel.gameVariant
+ property bool showCoordinates
+ property bool isTrigon: gameVariant.startsWith("trigon")
+ property bool isNexos: gameVariant.startsWith("nexos")
+ property bool isCallisto: gameVariant.startsWith("callisto")
+ property bool isGembloQ: gameVariant.startsWith("gembloq")
+ 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
+ case "gembloq":
+ case "gembloq_2_4":
+ return 56
+ case "gembloq_2":
+ return 44
+ case "gembloq_3":
+ return 52
+ default:
+ return 20
+ }
+ }
+ property int rows:
+ isTrigon ? (columns + 1) / 2 : isGembloQ ? columns / 2 : columns
+
+ property real gridWidth: {
+ // Avoid fractional piece element sizes if the piece elements are squares
+ var sideLength
+ if (isTrigon) sideLength = Math.min(width, Math.sqrt(3) * height)
+ else sideLength = Math.min(width, height)
+ var n = columns
+ if (showCoordinates) n += (isTrigon ? 3 : 2)
+ if (isTrigon) return sideLength / (n + 1)
+ if (isNexos) return Math.floor(sideLength * Screen.devicePixelRatio / (n - 0.5)) / Screen.devicePixelRatio
+ if (isGembloQ) return Math.floor(2 * sideLength * Screen.devicePixelRatio / n) / 2 / Screen.devicePixelRatio
+ return Math.floor(sideLength * Screen.devicePixelRatio / n) / Screen.devicePixelRatio
+ }
+ property real gridHeight: {
+ if (isTrigon) return Math.sqrt(3) * gridWidth
+ if (isGembloQ) return 2 * gridWidth
+ return gridWidth
+ }
+ property real startingPointSize: {
+ if (isTrigon) return 0.27 * gridHeight
+ if (isGembloQ) return 0.45 * gridHeight
+ return 0.35 * gridHeight
+ }
+ property int coordinateFontSize: {
+ if (isTrigon) return 0.4 * gridHeight
+ if (isGembloQ) return 0.35 * gridHeight
+ return 0.6 * gridHeight
+ }
+ property Item grabImageTarget: grabImageTarget
+
+ signal clicked(point pos)
+ signal rightClicked(point pos)
+
+ function mapFromGameX(x) {
+ if (isTrigon) return image.x + grabImageTarget.x + (x + 0.5) * gridWidth
+ if (isNexos) return image.x + grabImageTarget.x + (x - 0.25) * gridWidth
+ return image.x + grabImageTarget.x + x * gridWidth
+ }
+ function mapFromGameY(y) {
+ if (isNexos) return image.y + grabImageTarget.y + (y - 0.25) * gridHeight
+ return image.y + grabImageTarget.y + y * gridHeight
+ }
+ function mapToGame(pos) {
+ if (isTrigon)
+ return Qt.point((pos.x - grabImageTarget.x - image.x - 0.5 * gridWidth) / gridWidth,
+ (pos.y - grabImageTarget.y - image.y) / gridHeight)
+ if (isNexos)
+ return Qt.point((pos.x - grabImageTarget.x - image.x + 0.25 * gridWidth) / gridWidth,
+ (pos.y - grabImageTarget.y - image.y + 0.25 * gridHeight) / gridHeight)
+ return Qt.point((pos.x - grabImageTarget.x - image.x) / gridWidth,
+ (pos.y - grabImageTarget.y - image.y) / gridHeight)
+ }
+ // Needs all arguments for dependencies
+ function getStartingPointX(x, gridWidth, pointSize) {
+ return mapFromGameX(x) - grabImageTarget.x + (gridWidth - pointSize) / 2
+ }
+ // Needs all arguments for dependencies
+ function getStartingPointY(y, gridHeight, pointSize) {
+ return mapFromGameY(y) - grabImageTarget.y + (gridHeight - pointSize) / 2
+ }
+ 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
+ }
+ function getColumnCoord(x) {
+ if (x > 25)
+ return String.fromCharCode("A".charCodeAt(0) + x / 26 - 1)
+ + String.fromCharCode("A".charCodeAt(0) + (x % 26))
+ return String.fromCharCode("A".charCodeAt(0) + x)
+ }
+
+ Item {
+ id: grabImageTarget
+
+ anchors.centerIn: parent
+ width: {
+ if (! showCoordinates)
+ return image.width
+ if (isTrigon)
+ return image.width + 3 * gridWidth
+ return image.width + 2 * gridWidth
+ }
+ height: {
+ if (! showCoordinates)
+ return image.height
+ return image.height + 2 * gridHeight
+ }
+
+ Image {
+ id: image
+
+ width: {
+ if (isTrigon) return gridWidth * (columns + 1)
+ if (isNexos) return gridWidth * (columns - 0.5)
+ return gridWidth * columns
+ }
+ height: {
+ if (isNexos) return gridHeight * (rows - 0.5)
+ return gridHeight * rows
+ }
+ anchors.centerIn: parent
+ source: width > 0 && height > 0 ?
+ "image://pentobi/board/" + gameVariant + "/"
+ + theme.colorBoard[0] + "/" + theme.colorBoard[1] + "/"
+ + theme.colorBoard[2] + "/" + theme.colorBoard[3] + "/"
+ + theme.colorBoard[4] + "/" + theme.colorBoard[5] :
+ ""
+ sourceSize { width: width; height: height }
+ cache: false
+ }
+ Repeater {
+ model: gameModel.startingPoints0
+
+ Rectangle {
+ color: color0[0]
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: getStartingPointX(modelData.x, gridWidth, width)
+ y: getStartingPointY(modelData.y, gridHeight, height)
+ }
+ }
+ Repeater {
+ model: gameModel.startingPoints1
+
+ Rectangle {
+ color: color1[0]
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: getStartingPointX(modelData.x, gridWidth, width)
+ y: getStartingPointY(modelData.y, gridHeight, height)
+ }
+ }
+ Repeater {
+ model: gameModel.startingPoints2
+
+ Rectangle {
+ color: color2[0]
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: getStartingPointX(modelData.x, gridWidth, width)
+ y: getStartingPointY(modelData.y, gridHeight, height)
+ }
+ }
+ Repeater {
+ model: gameModel.startingPoints3
+
+ Rectangle {
+ color: color3[0]
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: getStartingPointX(modelData.x, gridWidth, width)
+ y: getStartingPointY(modelData.y, gridHeight, height)
+ }
+ }
+ Repeater {
+ model: gameModel.startingPointsAny
+
+ Rectangle {
+ color: theme.colorStartingPoint
+ width: startingPointSize; height: width
+ radius: width / 2
+ x: mapFromGameX(modelData.x) - grabImageTarget.x
+ + (gridWidth - width) / 2
+ y: mapFromGameY(modelData.y) - grabImageTarget.y
+ + getCenterYTrigon(modelData) - height / 2
+ }
+ }
+ Repeater {
+ model: showCoordinates ? columns : 0
+
+ Text {
+ text: getColumnCoord(index)
+ color: theme.colorText
+ opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+ font { pixelSize: coordinateFontSize; preferShaping: false }
+ x: mapFromGameX(index) - grabImageTarget.x
+ + (gridWidth - width) / 2
+ y: mapFromGameY(-1) - grabImageTarget.y
+ + (gridHeight - height) / 2
+ }
+ }
+ Repeater {
+ model: showCoordinates ? columns : 0
+
+ Text {
+ text: getColumnCoord(index)
+ color: theme.colorText
+ opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+ font { pixelSize: coordinateFontSize; preferShaping: false }
+ x: mapFromGameX(index) - grabImageTarget.x
+ + (gridWidth - width) / 2
+ y: mapFromGameY(rows) - grabImageTarget.y
+ + (gridHeight - height) / 2
+ }
+ }
+ Repeater {
+ model: showCoordinates ? rows : 0
+
+ Text {
+ text: index + 1
+ color: theme.colorText
+ opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+ font { pixelSize: coordinateFontSize; preferShaping: false }
+ x: mapFromGameX(isTrigon ? -1.5 : -1) - grabImageTarget.x
+ + (gridWidth - width) / 2
+ y: mapFromGameY(rows - index - 1) - grabImageTarget.y
+ + (gridHeight - height) / 2
+ }
+ }
+ Repeater {
+ model: showCoordinates ? rows : 0
+
+ Text {
+ text: index + 1
+ color: theme.colorText
+ opacity: 0.55 - 0.1 * theme.colorBackground.hslLightness
+ font { pixelSize: coordinateFontSize; preferShaping: false }
+ x: mapFromGameX(isTrigon ? columns + 0.5 : columns)
+ - grabImageTarget.x + (gridWidth - width) / 2
+ y: mapFromGameY(rows - index - 1) - grabImageTarget.y
+ + (gridHeight - height) / 2
+ }
+ }
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onPressAndHold: {
+ var pos = mapToGame(Qt.point(mouseX + grabImageTarget.x,
+ mouseY + grabImageTarget.y))
+ pos.x = Math.floor(pos.x)
+ pos.y = Math.floor(pos.y)
+ root.rightClicked(pos)
+ }
+ onClicked: {
+ var pos = mapToGame(Qt.point(mouseX + grabImageTarget.x,
+ mouseY + grabImageTarget.y))
+ pos.x = Math.floor(pos.x)
+ pos.y = Math.floor(pos.y)
+ if (mouse.button & Qt.RightButton)
+ root.rightClicked(pos)
+ else
+ root.clicked(pos)
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/BoardContextMenu.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+ property int moveNumber
+
+ property string _annotation
+
+ dynamicWidth: true
+ onOpened: _annotation = gameModel.getMoveAnnotation(moveNumber)
+
+ Pentobi.MenuItem {
+ enabled: moveNumber !== gameModel.moveNumber && ! isRated
+ text: qsTr("Go to Move %1").arg(moveNumber)
+ onTriggered: gameModel.gotoMove(moveNumber)
+ }
+ Pentobi.MenuItem {
+ text: _annotation === "" ?
+ qsTr("Move Annotation") :
+ //: The argument is the annotation symbol for the current move
+ qsTr("Move Annotation (%1)").arg(_annotation)
+ onTriggered: {
+ var dialog = moveAnnotationDialog.get()
+ dialog.moveNumber = moveNumber
+ moveAnnotationDialog.open()
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/BusyIndicator.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.6
+import QtQuick.Controls 2.1
+
+BusyIndicator {
+ id: root
+
+ contentItem: Item {
+ implicitWidth: 64
+ implicitHeight: 64
+
+ Item {
+ id: item
+
+ anchors.fill: parent
+ opacity: root.running ? 1 : 0
+
+ Behavior on opacity {
+ OpacityAnimator {
+ duration: enableAnimations ? animationDurationFast : 0
+ }
+ }
+
+ Repeater {
+ id: repeater
+
+ model: 8
+
+ Rectangle {
+ x: item.width / 2 - width / 2
+ y: item.height / 2 - height / 2
+ width: 0.15 * item.width; height: width
+ radius: width / 2
+ color: theme.colorText
+ opacity: 0.5
+ transform: [
+ Translate {
+ y: -item.width / 2 + width
+ },
+ Rotation {
+ angle: index / repeater.count * 360
+ origin { x: width / 2; y: height / 2 }
+ }
+ ]
+ }
+ }
+ RotationAnimator {
+ target: item
+ running: root.visible && root.running
+ from: 0; to: 360
+ loops: Animation.Infinite
+ duration: 1700
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Button.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Window 2.2
+import QtQuick.Controls 2.3
+
+ToolButton {
+ id: root
+
+ // See ButtonToolTip
+ property bool buttonToolTipHovered
+ property bool effectiveHovered:
+ isDesktop && buttonToolTipHovered && enabled
+
+ implicitWidth: Math.min(getIconSize() + (isDesktop ? 14 : 30),
+ 0.11 * rootWindow.contentItem.height,
+ 0.13 * rootWindow.contentItem.width)
+ implicitHeight: implicitWidth
+
+ // We use SVG icon sources of size 16x16 and want the icon about the same
+ // size as the font, but use multipliers in quarter-size steps (4) for
+ // better pixel alignment. Minimum size is 8. Note that on some Android 4.2
+ // devices, Qt 5.11 reports a much too low pixelDensity (e.g.
+ // pixelDensity=4.2, devicePixelRatio=1.5 on a 4.0" 480x800 device) but
+ // uses a reasonable font size, so deriving the size directly from
+ // pixelDensity and/or devicePixelRatio is not a good idea.
+ function getIconSize() {
+ return Math.max(
+ Math.round(
+ 1.2 * font.pixelSize * Screen.devicePixelRatio / 4)
+ / Screen.devicePixelRatio * 4,
+ Screen.devicePixelRatio * 8)
+ }
+
+ Behavior on opacity {
+ NumberAnimation { duration: gameDisplay.animationDurationFast }
+ }
+
+ opacity: root.enabled ? 0.5 : 0.25
+ hoverEnabled: false
+ display: AbstractButton.IconOnly
+ icon {
+ color: theme.colorText
+ width: getIconSize()
+ height: getIconSize()
+ }
+ focusPolicy: Qt.NoFocus
+ flat: true
+ background: Rectangle {
+ radius: 0.05 * width
+ color: down ? theme.colorButtonPressed :
+ effectiveHovered ? theme.colorButtonHovered
+ : "transparent"
+ border.color: down || effectiveHovered ? theme.colorButtonBorder
+ : "transparent"
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonApply.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+ text: qsTr("Apply")
+ DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonCancel.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+ text: qsTr("Cancel")
+ DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonClose.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+ text: qsTr("Close")
+ DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonOk.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.2
+
+Button {
+ text: qsTr("OK")
+ DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ButtonToolTip.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.4
+import "." as Pentobi
+
+// Used instead of attached tooltip because of QTBUG-30801 (tooltip not shown
+// when the button is disabled). Must be declared such that x and y have the
+// same meaning as in the button (e.g. same parent).
+MouseArea {
+ property Pentobi.Button button
+
+ property bool _inhibitAfterPress
+
+ visible: button.visible && isDesktop
+ x: button.x
+ y: button.y
+ width: button.width
+ height: button.height
+ acceptedButtons: Qt.NoButton
+ hoverEnabled: true
+ onExited: _inhibitAfterPress = false
+ ToolTip.visible: containsMouse && ToolTip.text && ! _inhibitAfterPress
+ ToolTip.delay: 1000
+ ToolTip.timeout: 7000
+ Component.onCompleted:
+ button.buttonToolTipHovered = Qt.binding(function() {
+ return containsMouse })
+
+ Connections {
+ target: button
+ onPressed: _inhibitAfterPress = true
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Comment.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+
+ScrollView {
+ function dropFocus() { if (textArea.activeFocus) textArea.focus = false }
+
+ clip: true
+ ScrollBar.vertical.minimumSize: 0.2
+
+ TextArea {
+ id: textArea
+
+ text: gameModel.comment
+ color: theme.colorCommentText
+ selectionColor: theme.colorSelection
+ selectedTextColor: theme.colorSelectedText
+ selectByMouse: isDesktop
+ wrapMode: TextEdit.Wrap
+ focus: true
+ onTextChanged: gameModel.comment = text
+ background: Rectangle {
+ // Qt 5.12.0 alpha doesn't size the background if it is in a
+ // SwipeView like in GameDisplayMobile
+ anchors.fill: parent
+ color: theme.colorCommentBase
+ radius: 2
+ border.color:
+ textArea.activeFocus ? theme.colorCommentFocus
+ : theme.colorCommentBorder
+ }
+ Keys.onPressed:
+ if (event.key === Qt.Key_Tab)
+ {
+ focus = false
+ event.accepted = true
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ComputerDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ footer: DialogButtonBoxOkCancel { }
+ onOpened: {
+ checkBox0.checked = computerPlays0
+ checkBox1.checked = computerPlays1
+ checkBox2.checked = computerPlays2
+ checkBox3.checked = computerPlays3
+ slider.value = playerModel.level
+ }
+ onAccepted: {
+ computerPlays0 = checkBox0.checked
+ computerPlays1 = checkBox1.checked
+ computerPlays2 = checkBox2.checked
+ computerPlays3 = checkBox3.checked
+ if (! Logic.isComputerToPlay() || playerModel.level !== slider.value)
+ Logic.cancelRunning()
+ playerModel.level = slider.value
+ if (! gameModel.isGameOver)
+ Logic.checkComputerMove()
+ }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(font.pixelSize * 16, maxContentWidth),
+ columnLayout.implicitWidth, minContentWidth)
+ implicitHeight: columnLayout.implicitHeight
+
+ ColumnLayout {
+ id: columnLayout
+
+ anchors.fill: parent
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ Label { text: qsTr("Computer plays:") }
+ GridLayout {
+ columns: gameModel.nuPlayers === 2 ? 1 : 2
+ Layout.fillWidth: true
+
+ Row {
+ Layout.fillWidth: true
+
+ Rectangle {
+ width: font.pixelSize; height: width
+ radius: width / 2
+ anchors.verticalCenter: parent.verticalCenter
+ color: gameDisplay.color0[0]
+ }
+ Rectangle {
+ visible: gameModel.nuColors === 4
+ && gameModel.nuPlayers === 2
+ width: font.pixelSize; height: width
+ radius: width / 2
+ anchors.verticalCenter: parent.verticalCenter
+ color: gameDisplay.color2[0]
+ }
+ CheckBox {
+ id: checkBox0
+
+ enabled: ! isRated
+ text: {
+ if (gameModel.nuColors === 4
+ && gameModel.nuPlayers === 2)
+ return qsTr("Blue/Red")
+ if (gameModel.gameVariant === "duo")
+ return qsTr("Purple")
+ if (gameModel.gameVariant === "junior")
+ return qsTr("Green")
+ return qsTr("Blue")
+ }
+ onClicked:
+ if (gameModel.nuColors === 4
+ && gameModel.nuPlayers === 2)
+ checkBox2.checked = checked
+ }
+ }
+ Row {
+ Layout.fillWidth: true
+
+ Rectangle {
+ width: font.pixelSize; height: width
+ radius: width / 2
+ anchors.verticalCenter: parent.verticalCenter
+ color: gameDisplay.color1[0]
+ }
+ Rectangle {
+ visible: gameModel.nuColors === 4
+ && gameModel.nuPlayers === 2
+ width: font.pixelSize; height: width
+ radius: width / 2
+ anchors.verticalCenter: parent.verticalCenter
+ color: gameDisplay.color3[0]
+ }
+ CheckBox {
+ id: checkBox1
+
+ enabled: ! isRated
+ text: {
+ if (gameModel.nuColors === 4
+ && gameModel.nuPlayers === 2)
+ return qsTr("Yellow/Green")
+ if (gameModel.gameVariant === "duo"
+ || gameModel.gameVariant === "junior")
+ return qsTr("Orange")
+ if (gameModel.nuColors === 2)
+ return qsTr("Green")
+ return qsTr("Yellow")
+ }
+ onClicked:
+ if (gameModel.nuColors === 4
+ && gameModel.nuPlayers === 2)
+ checkBox3.checked = checked
+ }
+ }
+ Row {
+ visible: gameModel.nuPlayers > 3
+ Layout.fillWidth: true
+
+ Rectangle {
+ width: font.pixelSize; height: width
+ radius: width / 2
+ anchors.verticalCenter: parent.verticalCenter
+ color: gameDisplay.color3[0]
+ }
+ CheckBox {
+ id: checkBox3
+
+ enabled: ! isRated
+ text: qsTr("Green")
+ }
+ }
+ Row {
+ visible: gameModel.nuPlayers > 2
+ Layout.fillWidth: true
+
+ Rectangle {
+ width: font.pixelSize; height: width
+ radius: width / 2
+ anchors.verticalCenter: parent.verticalCenter
+ color: gameDisplay.color2[0]
+ }
+ CheckBox {
+ id: checkBox2
+
+ enabled: ! isRated
+ text: qsTr("Red")
+ }
+ }
+ }
+ }
+ RowLayout {
+ Layout.topMargin: 0.6 * font.pixelSize
+ Layout.fillWidth: true
+
+ Label {
+ id: labelLevel
+
+ enabled: ! isRated
+ text: qsTr("Level %1").arg(slider.value)
+ }
+ Slider {
+ id: slider
+
+ enabled: ! isRated
+ from: 1; to: playerModel.maxLevel; stepSize: 1
+ // Implicit width of main contentItem might not fully fit
+ // on small screens if 2x2 check boxes are displayed. In
+ // this case, there is no need to clip the slider also,
+ // which expands to the dialog width if ! isDesktop
+ Layout.maximumWidth:
+ maxContentWidth - labelLevel.implicitWidth
+ - parent.spacing
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Controls.js
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+// Helper function to add a mnemonic without using an ampersand in the
+// translatable MenuItem text or in the Action text.
+// Ampersands in texts cause problems for translation with Weblate because
+// Weblate does not support searching strings containing ampersands.
+// Also, there seems to be a bug in Qt that in some cases registers mnemonics
+// in Action texts used in a MenuItem as global shortcuts (last occurred with
+// Qt 5.11.1)
+function addMnemonic(text, mnemonic) {
+ if (! isDesktop || mnemonic === "")
+ return text
+ mnemonic = mnemonic.toLowerCase()
+ var textLower = text.toLowerCase()
+ var pos = textLower.indexOf(mnemonic)
+ // Prefer beginning of word
+ while (pos >= 0 && pos < textLower.length) {
+ if (pos === 0 || textLower.charAt(pos - 1) === " ")
+ break
+ pos = textLower.indexOf(mnemonic, pos + 1)
+ }
+ if (pos < 0 || pos >= textLower.length)
+ pos = textLower.indexOf(mnemonic)
+ if (pos < 0) {
+ console.warn("mnemonic", mnemonic, "not found in", text)
+ return text
+ }
+ return text.substring(0, pos) + "&" + text.substring(pos)
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Dialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.2
+
+Dialog {
+ property real maxContentWidth:
+ rootWindow.contentItem.width - leftPadding - rightPadding
+ property real maxContentHeight: {
+ var h = rootWindow.contentItem.height - topPadding - bottomPadding
+ if (header && header.visible)
+ h -= header.implicitHeight
+ if (footer && footer.visible)
+ h -= footer.implicitHeight
+ return h
+ }
+ property real minContentWidth:
+ // Match window width on mobile devices within reason
+ isDesktop ? 0 : Math.min(40 * font.pixelSize, maxContentWidth)
+
+ function centerDialog() {
+ // Don't bind x and y because that can cause a binding loop if the
+ // application window is interactively resized
+ if (ApplicationWindow.window) {
+ x = (ApplicationWindow.window.width - width) / 2
+ y = (ApplicationWindow.window.height - height) / 2
+ }
+ }
+ // Qt 5.11 doesn't support default buttons yet, this function can be
+ // called as a replacement if the Return key is pressed and should be
+ // reimplemented if needed in derived dialogs.
+ // We don't handle the return key inside the dialog because the dialog will
+ // not consume the event in Qt 5.11 even if it accepts the key event and
+ // might therefore trigger global actions.
+ function returnPressed() {
+ if (! hasButtonFocus())
+ accept()
+ }
+ // Check if any button in the footer the focus. We don't want to handle
+ // the return key as accept (see comemnt above) if any button has the
+ // visual focus because the user might expect that pressing return triggers
+ // the button with the focus.
+ function hasButtonFocus() {
+ for (var i = 0; i < footer.contentChildren.length; ++i) {
+ if (footer.contentChildren[i].visualFocus)
+ return true
+ }
+ return false
+ }
+
+ modal: true
+ focus: true
+ clip: true
+ closePolicy: Popup.CloseOnEscape
+ onOpened: centerDialog()
+ onWidthChanged: centerDialog()
+ onHeightChanged: centerDialog()
+ ApplicationWindow.onWindowChanged:
+ if (ApplicationWindow.window) {
+ ApplicationWindow.window.onWidthChanged.connect(centerDialog)
+ ApplicationWindow.window.onHeightChanged.connect(centerDialog)
+ }
+ Component.onCompleted:
+ if (! isDesktop)
+ // Save some screen space on smartphones
+ title = ""
+
+ Shortcut {
+ sequence: "Return"
+ enabled: visible
+ onActivated: returnPressed()
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/DialogButtonBox.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.4
+
+DialogButtonBox {
+ // In Qt 5.11, undefined alignment can cause a binding loop for
+ // implicitWidth of the dialog in default style
+ alignment: Qt.AlignRight
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/DialogButtonBoxOkCancel.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.DialogButtonBox {
+ ButtonCancel { }
+ ButtonOk { }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/DialogLoader.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Loader {
+ property string url
+
+ function get() {
+ if (! item) source = url
+ return item
+ }
+ function open() { get().open() }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ExportImageDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ footer: Pentobi.DialogButtonBox {
+ ButtonOk {
+ enabled: textField.acceptableInput
+ onClicked: checkAccept()
+ DialogButtonBox.buttonRole: DialogButtonBox.InvalidRole
+ }
+ ButtonCancel { }
+ }
+ onOpened: textField.selectAll()
+ onAccepted: {
+ exportImageWidth = parseInt(textField.text)
+ var dialog = imageSaveDialog.get()
+ dialog.name = gameModel.suggestFileName(folder, "png")
+ dialog.selectNameFilter(0)
+ dialog.open()
+ }
+
+ function returnPressed() {
+ if (! hasButtonFocus())
+ checkAccept()
+ }
+ function checkAccept() {
+ if (textField.acceptableInput)
+ accept()
+ }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(rowLayout.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: rowLayout.implicitHeight
+
+ RowLayout {
+ id: rowLayout
+
+ anchors.fill: parent
+
+ Label { text: qsTr("Image width:") }
+ TextField {
+ id: textField
+
+ text: exportImageWidth
+ focus: true
+ inputMethodHints: Qt.ImhDigitsOnly
+ validator: IntValidator{ bottom: 0; top: 32767 }
+ selectByMouse: true
+ Layout.preferredWidth: font.pixelSize * 5
+ }
+ Item { Layout.fillWidth: true }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/FatalMessage.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+MessageDialog {
+ onClosed: Qt.quit()
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/FileDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.4
+import QtQuick.Layouts 1.1
+import Qt.labs.folderlistmodel 2.11
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+ id: root
+
+ property bool selectExisting: true
+ property alias name: nameField.text
+ property url folder
+ property url fileUrl
+ property var nameFilterLabels
+ property var nameFilters
+ readonly property url defaultFolder: androidUtils.getDefaultFolder()
+
+ function returnPressed() {
+ if (! hasButtonFocus())
+ checkAccept()
+ }
+ function selectNameField() {
+ if (! isAndroid) {
+ var pos = name.lastIndexOf(".")
+ if (pos < 0)
+ nameField.selectAll()
+ else
+ nameField.select(0, pos)
+ }
+ view.currentIndex = -1
+ }
+
+ function selectNameFilter(index) {
+ comboBoxNameFilter.currentIndex = index
+ }
+
+ signal nameFilterChanged(int index)
+
+ property url _lastFolder
+
+ function isValidName(name) {
+ return name.trim().length > 0
+ && ! (! selectExisting && name.trim().startsWith("."))
+ }
+
+ function checkAccept() {
+ if (! isValidName(name))
+ return
+ folder = folderModel.folder
+ fileUrl = folder + "/" + name.trim()
+ if (! selectExisting
+ && gameModel.checkFileExists(Logic.getFileFromUrl(fileUrl))) {
+ Logic.showQuestion(qsTr("Overwrite file?"), accept)
+ return
+ }
+ accept()
+ }
+
+ footer: Pentobi.DialogButtonBox {
+ Button {
+ enabled: isValidName(name)
+ text: selectExisting ? qsTr("Open") : qsTr("Save")
+ onClicked: checkAccept()
+ }
+ ButtonCancel { }
+ }
+ onOpened: {
+ if (isAndroid && ! folder.toString().startsWith(defaultFolder.toString()))
+ folder = defaultFolder
+ selectNameField()
+ }
+
+ Item {
+ implicitWidth: Math.max(Math.min(font.pixelSize * 30, maxContentWidth),
+ minContentWidth)
+ implicitHeight: Math.min(font.pixelSize * 30, maxContentHeight)
+
+ Shortcut {
+ sequence: "Alt+Left"
+ onActivated: backButton.onClicked()
+ }
+ ColumnLayout
+ {
+ anchors.fill: parent
+
+ TextField {
+ id: nameField
+
+ visible: ! selectExisting
+ focus: ! isAndroid
+ selectByMouse: true
+ Layout.fillWidth: true
+ Component.onCompleted: nameField.cursorPosition = nameField.length
+ onTextEdited: view.currentIndex = -1
+ }
+ RowLayout {
+ Layout.fillWidth: true
+
+ ToolButton {
+ id: backButton
+
+ property bool hasParent:
+ ! folderModel.folder.toString().endsWith(":///")
+ && ! (isAndroid && folderModel.folder === defaultFolder)
+
+ opacity: hasParent ? 1 : 0.5
+ onClicked:
+ if (hasParent) {
+ _lastFolder = folderModel.folder
+ folderModel.folder = folderModel.parentFolder
+ if (selectExisting)
+ name = ""
+ }
+ icon {
+ source: "icons/filedialog-parent.svg"
+ // Icon size is 16x16
+ width: font.pixelSize < 20 ? 16 : font.pixelSize
+ height: font.pixelSize < 20 ? 16 : font.pixelSize
+ color: frame.palette.buttonText
+ }
+ }
+ Label {
+ text: {
+ if (isAndroid
+ && folderModel.folder.toString().startsWith(defaultFolder.toString()))
+ return folderModel.folder.toString().substr(defaultFolder.toString().length + 1)
+ Logic.getFileFromUrl(folderModel.folder)
+ }
+ elide: Text.ElideLeft
+ Layout.fillWidth: true
+ }
+ ToolButton {
+ visible: ! selectExisting
+ icon {
+ source: "icons/filedialog-newfolder.svg"
+ // Icon size is 16x16
+ width: font.pixelSize < 20 ? 16 : font.pixelSize
+ height: font.pixelSize < 20 ? 16 : font.pixelSize
+ color: frame.palette.buttonText
+ }
+ onClicked: {
+ var dialog = newFolderDialog.get()
+ dialog.folder = folderModel.folder
+ dialog.open()
+ }
+ }
+ }
+ Frame {
+ id: frame
+
+ padding: 0.1 * font.pixelSize
+ focusPolicy: Qt.TabFocus
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ background: Rectangle {
+ color: frame.palette.base
+ border.color: frame.activeFocus ? frame.palette.highlight : frame.palette.mid
+ radius: 2
+ }
+ ListView {
+ id: view
+
+ anchors.fill: parent
+ clip: true
+ model: folderModel
+ boundsBehavior: Flickable.StopAtBounds
+ highlight: Rectangle {
+ // Should logically use palette.highlight, but in
+ // most styles other than the desktop style Fusion,
+ // palette.highlight is a too flashy color.
+ color: isDesktop ? palette.highlight : palette.midlight
+ }
+ highlightMoveDuration: 0
+ focus: true
+ onActiveFocusChanged:
+ if (activeFocus && currentIndex < 0 && count)
+ currentIndex = 0
+ onCurrentIndexChanged:
+ if (currentIndex >= 0
+ && ! folderModel.isFolder(currentIndex))
+ name = folderModel.get(currentIndex, "fileName")
+ delegate: AbstractButton {
+ width: view.width
+ height: 2 * font.pixelSize
+ focusPolicy: Qt.NoFocus
+ contentItem: Row {
+ spacing: 0.3 * font.pixelSize
+ leftPadding: 0.2 * font.pixelSize
+
+ Image {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: folderModel.isFolder(index)
+ // Icon size is 16x16
+ width: font.pixelSize < 20 ? 16 : font.pixelSize
+ height: width
+ source: "icons/filedialog-folder.svg"
+ sourceSize { width: width; height: height }
+ }
+ Label {
+ width: parent.width - parent.spacing - parent.leftPadding
+ text: index < 0 ? "" : fileName
+ anchors.verticalCenter: parent.verticalCenter
+ color: view.currentIndex == index ?
+ // See comment at highlight
+ (isDesktop ? frame.palette.highlightedText
+ : frame.palette.buttonText) :
+ frame.palette.text
+ horizontalAlignment: Text.AlignHLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideMiddle
+ }
+ }
+ onClicked: {
+ if (folderModel.isFolder(index)) {
+ delayedOpenFolderTimer.folderName = fileName
+ delayedOpenFolderTimer.restart()
+ }
+ else {
+ name = fileName
+ if (! selectExisting)
+ selectNameField()
+ }
+ view.currentIndex = index
+ }
+ onDoubleClicked:
+ if (! folderModel.isFolder(index))
+ checkAccept()
+ }
+ ScrollBar.vertical: ScrollBar { active: true }
+
+ FolderListModel {
+ id: folderModel
+
+ folder: root.folder
+ nameFilters: [ root.nameFilter ]
+ showDirsFirst: true
+ onStatusChanged:
+ if (status === FolderListModel.Ready) {
+ var i = folderModel.indexOf(_lastFolder)
+ if (i >= 0)
+ view.currentIndex = i
+ else
+ view.currentIndex = -1
+ }
+ }
+ // Open folder with small delay such that the folder name
+ // is visibly highlighted when clicked before it opens. We
+ // can't set view.currentIndex in onPressed, otherwise the
+ // item is unwantedly highlighted when flicking the list.
+ Timer {
+ id: delayedOpenFolderTimer
+
+ property string folderName
+
+ interval: 100
+
+ onTriggered: {
+ if (! folderModel.folder.toString().endsWith("/"))
+ folderModel.folder = folderModel.folder + "/"
+ _lastFolder = ""
+ folderModel.folder = folderModel.folder + folderName
+ if (selectExisting)
+ name = ""
+ }
+ }
+ }
+ }
+ ComboBox {
+ id: comboBoxNameFilter
+
+ model: {
+ var result = nameFilterLabels
+ nameFilterLabels.push(qsTr("All files"))
+ return result
+ }
+ onCurrentIndexChanged: {
+ if (currentIndex < root.nameFilters.length)
+ folderModel.nameFilters = root.nameFilters[currentIndex]
+ else
+ folderModel.nameFilters = [ "*" ]
+ nameFilterChanged(currentIndex)
+ }
+ Layout.preferredWidth:
+ Math.min(font.pixelSize * 14, maxContentWidth)
+ Layout.alignment: Qt.AlignRight
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameDisplay.js
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+function createColorPieces(component, pieceModels) {
+ if (pieceModels.length === 0)
+ return []
+ var properties = { }
+ var pieces = []
+ for (var i = 0; i < pieceModels.length; ++i) {
+ properties["pieceModel"] = pieceModels[i]
+ pieces.push(component.createObject(gameDisplay, properties))
+ }
+ return pieces
+}
+
+function createPieces() {
+ destroyPieces()
+ var file
+ var gameVariant = gameModel.gameVariant
+ if (gameVariant.startsWith("trigon"))
+ file = "PieceTrigon.qml"
+ else if (gameVariant.startsWith("nexos"))
+ file = "PieceNexos.qml"
+ else if (gameVariant.startsWith("callisto"))
+ file = "PieceCallisto.qml"
+ else if (gameVariant.startsWith("gembloq"))
+ file = "PieceGembloQ.qml"
+ else
+ file = "PieceClassic.qml"
+ var component = Qt.createComponent(file)
+ if (component.status !== Component.Ready)
+ console.warn(component.errorString())
+ pieces0 = createColorPieces(component, gameModel.getPieceModels(0))
+ pieces1 = createColorPieces(component, gameModel.getPieceModels(1))
+ pieces2 = createColorPieces(component, gameModel.getPieceModels(2))
+ pieces3 = createColorPieces(component, gameModel.getPieceModels(3))
+ 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()
+ }
+}
+
+function destroyPieces() {
+ pieceSelector.transitionsEnabled = false
+ pickedPiece = null
+ destroyColorPieces(pieces0); pieces0 = []
+ destroyColorPieces(pieces1); pieces1 = []
+ destroyColorPieces(pieces2); pieces2 = []
+ destroyColorPieces(pieces3); pieces3 = []
+}
+
+function dropPieceFast() {
+ if (! pickedPiece)
+ return
+ var old = enableAnimations
+ enableAnimations = false
+ pickedPiece = null
+ enableAnimations = old
+}
+
+function findPiece(pieceModel) {
+ var pieces
+ switch (pieceModel.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 movePiece(x, y) {
+ if (pickedPiece == null)
+ return
+ var pos = pieceManipulator.mapToItem(
+ board, pieceManipulator.width / 2, pieceManipulator.height / 2)
+ var fastMove
+ if (! board.contains(pos)) {
+ // Outside board before moving, move to center of board
+ pos = mapFromItem(board, board.width / 2, board.height / 2)
+ x = pos.x - pieceManipulator.width / 2
+ y = pos.y - pieceManipulator.height / 2
+ fastMove = false
+ }
+ else {
+ pos = pieceManipulator.mapToItem(
+ board,
+ pieceManipulator.width / 2 + x - pieceManipulator.x,
+ pieceManipulator.height / 2 + y - pieceManipulator.y)
+ pos.x = Math.max(0, pos.x)
+ pos.x = Math.min(board.width - 1, pos.x)
+ pos.y = Math.max(0, pos.y)
+ pos.y = Math.min(board.height - 1, pos.y)
+ pos = mapFromItem(board, pos.x, pos.y)
+ x = pos.x - pieceManipulator.width / 2
+ y = pos.y - pieceManipulator.height / 2
+ fastMove = true
+ }
+ pieceManipulator.fastMove = fastMove
+ pieceManipulator.x = x
+ pieceManipulator.y = y
+ pieceManipulator.fastMove = false
+}
+
+function onBoardClicked(pos) {
+ dropCommentFocus()
+ if (! setupMode)
+ return
+ var pieceModel = gameModel.addEmpty(pos)
+ if (! pieceModel)
+ return
+ var piece = findPiece(pieceModel)
+ pos = mapFromItem(piece, (piece.width - pieceManipulator.width) / 2,
+ (piece.height - pieceManipulator.height) / 2)
+ pieceManipulator.x = pos.x
+ pieceManipulator.y = pos.y
+ pickedPiece = piece
+}
+
+function onBoardRightClicked(pos) {
+ dropCommentFocus()
+ var n = gameModel.getMoveNumberAt(pos)
+ if (n < 0)
+ return
+ gameDisplay.openBoardContextMenu(
+ n, board.mapFromGameX(pos.x + 0.5),
+ board.mapFromGameY(pos.y + 0.5))
+}
+
+function shiftPiece(dx, dy) {
+ if (gameModel.gameVariant.startsWith("gembloq"))
+ // In GembloQ, every piece has at least one full square, so we can use
+ // half the x resolution, which makes positioning easier for the user
+ movePiece(pieceManipulator.x + dx * board.gridWidth,
+ pieceManipulator.y + dy * board.gridHeight / 2)
+ else if (gameModel.gameVariant.startsWith("nexos"))
+ movePiece(pieceManipulator.x + dx * board.gridWidth,
+ pieceManipulator.y + dy * board.gridHeight)
+ else
+ movePiece(pieceManipulator.x + dx * board.gridWidth / 2,
+ pieceManipulator.y + dy * board.gridHeight / 2)
+}
+
+function shiftPieceFast(dx, dy) {
+ movePiece(pieceManipulator.x + dx * board.width / 4,
+ pieceManipulator.y + dy * board.height / 4)
+}
+
+function pickPiece(piece) {
+ pickPieceAt(piece, mapFromItem(piece, 0, 0))
+}
+
+function pickPieceAt(piece, coord) {
+ if (playerModel.isGenMoveRunning || gameModel.isGameOver)
+ return
+ if (piece.pieceModel.color !== gameModel.toPlay && ! setupMode) {
+ gameDisplay.showToPlay()
+ return
+ }
+ if (! pieceManipulator.pieceModel) {
+ // Position pieceManipulator at center of piece if possible, but
+ // make sure it is completely visible
+ var x = coord.x - pieceManipulator.width / 2
+ var y = coord.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 pickPieceAtBoard(piece) {
+ pickPieceAt(piece, mapFromItem(board, board.width / 2, board.height / 2))
+}
+
+function playPickedPiece() {
+ if (! pickedPiece)
+ return
+ var pos = pieceManipulator.mapToItem(board, pieceManipulator.width / 2,
+ pieceManipulator.height / 2)
+ if (! board.contains(pos))
+ pickedPiece = null
+ else if (setupMode)
+ gameModel.addSetup(pieceManipulator.pieceModel, board.mapToGame(pos))
+ else if (pieceManipulator.legal)
+ play(pieceManipulator.pieceModel, board.mapToGame(pos))
+}
+
+function showMove(move) {
+ var pieceModel = gameModel.preparePiece(move)
+ if (pieceModel === null)
+ return
+ var newPickedPiece = findPiece(pieceModel)
+ if (pickedPiece && newPickedPiece !== pickedPiece)
+ pickedPiece = null
+ 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 = newPickedPiece
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameDisplayDesktop.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Controls 2.0
+import Qt.labs.settings 1.0
+import "." as Pentobi
+import "GameDisplay.js" as Logic
+
+Item
+{
+ id: root
+
+ property Item pickedPiece
+
+ // Values: "last_dot", "last_number", "all_number", "none"
+ property string moveMarking: "last_dot"
+
+ property alias showCoordinates: board.showCoordinates
+ property bool enableAnimations: true
+ property real animationDurationMove: enableAnimations ? 300 : 0
+ property real animationDurationFast: enableAnimations ? 80 : 0
+ property bool setupMode
+ property string commentMode: "as_needed"
+ property bool showMoveNumber
+
+ property size imageSourceSize: {
+ var width = board.gridWidth, height = board.gridHeight
+ if (board.isTrigon || board.isGembloQ)
+ return Qt.size(2 * width, height)
+ if (board.isNexos)
+ return Qt.size(1.5 * width, 0.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
+ property var color0: {
+ if (gameModel.gameVariant === "duo") return theme.colorPurple
+ if (gameModel.gameVariant === "junior") return theme.colorGreen
+ return theme.colorBlue
+ }
+ property var color1: {
+ if (gameModel.gameVariant === "duo"
+ || gameModel.gameVariant === "junior") return theme.colorOrange
+ if (gameModel.nuColors === 2) return theme.colorGreen
+ return theme.colorYellow
+ }
+ property var color2: theme.colorRed
+ property var color3: theme.colorGreen
+ property alias isCommentVisible: comment.visible
+
+ readonly property real _relativeBoardWidth: 0.52
+
+ signal play(var pieceModel, point gameCoord)
+
+ function createPieces() { Logic.createPieces() }
+ function destroyPieces() { Logic.destroyPieces() }
+ function findPiece(pieceModel) { return Logic.findPiece(pieceModel) }
+ function pickPieceAtBoard(piece) { Logic.pickPieceAtBoard(piece) }
+ function shiftPiece(dx, dy) { Logic.shiftPiece(dx, dy) }
+ function shiftPieceFast(dx, dy) { Logic.shiftPieceFast(dx, dy) }
+ function playPickedPiece() { Logic.playPickedPiece() }
+ function showToPlay() { }
+ function showComment() { comment.visible = true }
+ function setCommentVisible(visible) { comment.visible = visible }
+ function showPieces() { }
+ function dropCommentFocus() { if (comment.item) comment.item.dropFocus() }
+ function showMove(move) { Logic.showMove(move) }
+ function getBoard() { return board }
+ function showTemporaryMessage(text) {
+ showStatus(text)
+ messageTimer.restart()
+ }
+ function startSearch() { showStatus(qsTr("Computer is thinking…")) }
+ function endSearch() { if (! messageTimer.running) clearStatus() }
+ function startAnalysis() {
+ showStatus(qsTr("Running game analysis…"))
+ comment.visible = false
+ }
+ function endAnalysis() { if (! messageTimer.running) clearStatus() }
+ function deleteAnalysis() { }
+ function analysisAutoloaded() { comment.visible = false }
+ function searchCallback(elapsedSeconds, 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 (! playerModel.isGenMoveRunning || elapsedSeconds < 10)
+ return
+ var text
+ var seconds = Math.ceil(remainingSeconds)
+ if (seconds < 90)
+ text = qsTr("Computer is thinking… (up to %1 seconds remaining)").arg(seconds)
+ else
+ {
+ var minutes = Math.ceil(remainingSeconds / 60)
+ text = qsTr("Computer is thinking… (up to %1 minutes remaining)").arg(minutes)
+ }
+ showStatus(text)
+ }
+ function openBoardContextMenu(moveNumber, x, y) {
+ if (! boardContextMenu.item)
+ boardContextMenu.sourceComponent = boardContextMenuComponent
+ boardContextMenu.item.moveNumber = moveNumber
+ if (isDesktop)
+ boardContextMenu.item.popup()
+ else
+ boardContextMenu.item.popup(x, y)
+ }
+
+ function showStatus(text) {
+ messageTimer.stop()
+ statusText.text = text
+ statusText.opacity = 1
+ }
+ function clearStatus() { statusText.opacity = 0 }
+
+ function _updateCommentVisible() {
+ if (commentMode === "always")
+ comment.visible = true
+ else if (commentMode === "never")
+ comment.visible = false
+ else
+ comment.visible = gameModel.comment !== ""
+ }
+
+ onWidthChanged: Logic.dropPieceFast()
+ onHeightChanged: Logic.dropPieceFast()
+ onCommentModeChanged: _updateCommentVisible()
+
+ Settings {
+ property alias enableAnimations: root.enableAnimations
+ property alias moveMarking: root.moveMarking
+ property alias showCoordinates: root.showCoordinates
+ property alias showMoveNumber: root.showMoveNumber
+ property alias setupMode: root.setupMode
+ property alias commentMode: root.commentMode
+
+ category: "GameDisplayDesktop"
+ }
+ Item {
+ id: mainContent
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ leftMargin: 3
+ rightMargin: 3
+ topMargin: 2
+ top: parent.top
+ bottom: statusBar.top
+ }
+
+ Item {
+ anchors.centerIn: parent
+ width: Math.min(parent.width, parent.height / _relativeBoardWidth)
+ height: {
+ var height = width * _relativeBoardWidth
+ if (board.isTrigon)
+ height *= Math.sqrt(3) / 2
+ return height
+ }
+
+ Board {
+ id: board
+
+ anchors {
+ left: parent.left
+ top: parent.top
+ bottom: parent.bottom
+ }
+ width: _relativeBoardWidth * parent.width
+ onClicked: Logic.onBoardClicked(pos)
+ onRightClicked: Logic.onBoardRightClicked(pos)
+
+ Loader {
+ id: boardContextMenu
+
+ Component {
+ id: boardContextMenuComponent
+
+ BoardContextMenu { }
+ }
+ }
+ }
+ Item {
+ anchors {
+ left: board.right
+ right: parent.right
+ leftMargin:
+ Math.min(
+ Math.max(
+ 5,
+ mainContent.width
+ - mainContent.height / _relativeBoardWidth),
+ 0.03 * board.width)
+ verticalCenter: board.verticalCenter
+ }
+ height: board.grabImageTarget.height
+
+ ScoreDisplay {
+ id: scoreDisplay
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+ height: 0.035 * parent.height
+ }
+ PieceSelectorDesktop {
+ id: pieceSelector
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: scoreDisplay.bottom
+ topMargin: 0.02 * parent.height
+ }
+ height: (board.isTrigon ? 0.75 : 0.7) * parent.height
+ transitionsEnabled: false
+ onPiecePicked: Logic.pickPiece(piece)
+ }
+ Item {
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: pieceSelector.bottom
+ bottom: parent.bottom
+ }
+
+ Loader {
+ id: comment
+
+ anchors.fill: parent
+ visible: false
+ sourceComponent:
+ visible || item ? commentComponent : null
+
+ Component {
+ id: commentComponent
+
+ Comment { }
+ }
+ }
+ Loader {
+ id: analyzeGame
+
+ anchors.fill: parent
+ visible: ! comment.visible
+ && (analyzeGameModel.elements.length > 0
+ || analyzeGameModel.isRunning)
+ sourceComponent:
+ visible || item ? analyzeGameComponent : null
+
+ Component {
+ id: analyzeGameComponent
+
+ AnalyzeGame { theme: rootWindow.theme }
+ }
+ }
+ }
+ }
+ }
+ }
+ Item {
+ id: statusBar
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ bottom: parent.bottom
+ }
+ height: 1.7 * statusText.font.pixelSize
+
+ Label {
+ id: statusText
+
+ anchors {
+ left: parent.left
+ top: top.right
+ bottom: parent.bottom
+ leftMargin: 5
+ }
+ opacity: 0
+ color: theme.colorText
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: animationDurationFast
+ }
+ }
+ }
+ Label {
+ visible: root.showMoveNumber
+ anchors {
+ right: parent.right
+ top: top.right
+ bottom: parent.bottom
+ rightMargin: 5
+ }
+ text: gameModel.positionInfoShort
+ color: theme.colorText
+ opacity: 0.8
+ }
+ Timer {
+ id: messageTimer
+
+ interval: 3000
+ onTriggered: clearStatus()
+ }
+ }
+ PieceManipulator {
+ id: pieceManipulator
+
+ legal: {
+ if (pickedPiece === null) return false
+ // Need explicit dependencies on x, y, pieceModel.state
+ var pos = parent.mapToItem(board, x + width / 2, y + height / 2)
+ if (setupMode)
+ return gameModel.isLegalSetupPos(pickedPiece.pieceModel,
+ pickedPiece.pieceModel.state,
+ board.mapToGame(pos))
+ return gameModel.isLegalPos(pickedPiece.pieceModel,
+ pickedPiece.pieceModel.state,
+ board.mapToGame(pos))
+ }
+ width: {
+ var f
+ if (board.isTrigon) f = 7
+ else if (board.isNexos) f = 11
+ else if (board.isGembloQ) f = 10.5
+ else if (board.isCallisto) f = 6.5
+ else f = 7.3
+ return Math.max(200, f * board.gridHeight)
+ }
+ height: width
+ pieceModel: pickedPiece ? pickedPiece.pieceModel : null
+ onPiecePlayed: Logic.playPickedPiece()
+ }
+ Connections {
+ target: gameModel
+ onPositionChanged:
+ if (analyzeGameModel.elements.length > 0
+ || analyzeGameModel.isRunning)
+ comment.visible = false
+ else
+ _updateCommentVisible()
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameDisplayMobile.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.0
+import Qt.labs.settings 1.0
+import "." as Pentobi
+import "GameDisplay.js" as Logic
+
+Item
+{
+ id: root
+
+ property Item pickedPiece
+
+ // Values: "last_dot", "last_number", "all_number", "none"
+ property string moveMarking: "last_dot"
+
+ property alias showCoordinates: board.showCoordinates
+ property bool enableAnimations: true
+ property real animationDurationMove: enableAnimations ? 300 : 0
+ property real animationDurationFast: enableAnimations ? 80 : 0
+ property bool setupMode
+ property alias boardContextMenu: boardContextMenu
+ property size imageSourceSize: {
+ var width = board.gridWidth, height = board.gridHeight
+ if (board.isTrigon || board.isGembloQ)
+ 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
+ property var color0: {
+ if (gameModel.gameVariant === "duo") return theme.colorPurple
+ if (gameModel.gameVariant === "junior") return theme.colorGreen
+ return theme.colorBlue
+ }
+ property var color1: {
+ if (gameModel.gameVariant === "duo"
+ || gameModel.gameVariant === "junior") return theme.colorOrange
+ if (gameModel.nuColors === 2) return theme.colorGreen
+ return theme.colorYellow
+ }
+ property var color2: theme.colorRed
+ property var color3: theme.colorGreen
+ property bool isCommentVisible: swipeView.currentIndex === 1
+
+ signal play(var pieceModel, point gameCoord)
+
+ function createPieces() { Logic.createPieces() }
+ function destroyPieces() { Logic.destroyPieces() }
+ function findPiece(pieceModel) { return Logic.findPiece(pieceModel) }
+ function pickPieceAtBoard(piece) { Logic.pickPieceAtBoard(piece) }
+ function shiftPiece(dx, dy) { Logic.shiftPiece(dx, dy) }
+ function shiftPieceFast(dx, dy) { Logic.shiftPieceFast(dx, dy) }
+ function playPickedPiece() { Logic.playPickedPiece() }
+ function showToPlay() { pieceSelector.contentY = 0 }
+ function showAnalyzeGame() { pickedPiece = null; swipeView.currentIndex = 2 }
+ function showComment() { pickedPiece = null; swipeView.currentIndex = 1 }
+ function showPieces() { swipeView.currentIndex = 0 }
+ function dropCommentFocus() { navigationPanel.dropCommentFocus() }
+ function showMove(move) { Logic.showMove(move) }
+ function getBoard() { return board }
+ function showTemporaryMessage(text) { message.showTemporary(text) }
+ function searchCallback(elapsedSeconds, remainingSeconds) { }
+ function startSearch() { }
+ function endSearch() { }
+ function startAnalysis() { showAnalyzeGame() }
+ function endAnalysis() { }
+ function deleteAnalysis() { if (swipeView.currentIndex === 2) showPieces() }
+ function analysisAutoloaded() { }
+ function openBoardContextMenu(moveNumber, x, y) {
+ if (! boardContextMenu.item)
+ boardContextMenu.sourceComponent = boardContextMenuComponent
+ boardContextMenu.item.moveNumber = moveNumber
+ if (isDesktop)
+ boardContextMenu.item.popup()
+ else
+ boardContextMenu.item.popup(x, y)
+ }
+
+ onWidthChanged: Logic.dropPieceFast()
+ onHeightChanged: Logic.dropPieceFast()
+
+ Settings {
+ property alias enableAnimations: root.enableAnimations
+ property alias moveMarking: root.moveMarking
+ property alias showCoordinates: root.showCoordinates
+ property alias swipeViewCurrentIndex: swipeView.currentIndex
+ property alias setupMode: root.setupMode
+
+ category: "GameDisplayMobile"
+ }
+ Column {
+ id: column
+
+ width: root.width
+ anchors.centerIn: root
+ spacing: 0.01 * board.width
+
+ Board {
+ id: board
+
+ width: Math.min(parent.width, 0.7 * root.height)
+ height: isTrigon ? Math.sqrt(3) / 2 * width : width
+ anchors.horizontalCenter: parent.horizontalCenter
+ onClicked: Logic.onBoardClicked(pos)
+ onRightClicked: Logic.onBoardRightClicked(pos)
+
+ Loader {
+ id: boardContextMenu
+
+ Component {
+ id: boardContextMenuComponent
+
+ BoardContextMenu { }
+ }
+ }
+ }
+ SwipeView {
+ id: swipeView
+
+ width: Math.min(1.3 * board.width, root.width)
+ height: Math.min(root.height - board.height, board.height)
+ clip: width < rootWindow.contentItem.width
+ anchors.horizontalCenter: board.horizontalCenter
+
+ Column {
+ id: columnPieces
+
+ spacing: 2
+
+ ScoreDisplay {
+ id: scoreDisplay
+
+ width: swipeView.width
+ height: 0.06 * swipeView.width
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ PieceSelectorMobile {
+ id: pieceSelector
+
+ property real elementSize:
+ // Show at least 3 rows
+ Math.min(board.width / columns, height / 3)
+
+ columns: pieces0 && pieces0.length <= 21 ? 7 : 8
+ width: elementSize * columns
+ height: swipeView.height - scoreDisplay.height
+ - columnPieces.spacing
+ rowSpacing: {
+ // Don't show partial pieces
+ var n = Math.floor(height / elementSize)
+ return (height - n * elementSize) / n
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ transitionsEnabled: false
+ onPiecePicked: Logic.pickPiece(piece)
+ }
+ }
+ NavigationPanel {
+ id: navigationPanel
+ }
+ ColumnLayout {
+ AnalyzeGame {
+ theme: rootWindow.theme
+ Layout.margins: 0.01 * parent.width
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ NavigationButtons
+ {
+ Layout.fillWidth: true
+ Layout.maximumHeight:
+ Math.min(50, 0.08 * rootWindow.contentItem.height,
+ root.width / 6)
+ }
+ }
+ }
+ }
+ Pentobi.BusyIndicator {
+ id: busyIndicator
+
+ running: busyIndicatorRunning
+ width: Math.min(0.2 * swipeView.width, swipeView.height)
+ height: width
+ x: (root.width - width) / 2
+ y: column.y + swipeView.y + (swipeView.height - height) / 2
+ opacity: 0.7
+ }
+ Rectangle {
+ id: message
+
+ function showTemporary(text) {
+ messageText.text = text
+ opacity = 1
+ messageTimer.restart()
+ }
+
+ opacity: 0
+ x: (root.width - width) / 2
+ y: column.y + swipeView.y + (swipeView.height - height) / 2
+ radius: 0.1 * height
+ color: theme.colorMessageBase
+ implicitWidth: messageText.implicitWidth + 0.5 * messageText.implicitHeight
+ implicitHeight: 1.5 * messageText.implicitHeight
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: animationDurationFast
+ }
+ }
+
+ Text {
+ id: messageText
+
+ anchors.centerIn: parent
+ color: theme.colorMessageText
+ }
+ Timer {
+ id: messageTimer
+
+ interval: 2500
+ onTriggered: message.opacity = 0
+ }
+ }
+ PieceManipulator {
+ id: pieceManipulator
+
+ legal: {
+ if (pickedPiece === null) return false
+ // Need explicit dependencies on x, y, pieceModel.state
+ var pos = parent.mapToItem(board, x + width / 2, y + height / 2)
+ if (setupMode)
+ return gameModel.isLegalSetupPos(pickedPiece.pieceModel,
+ pickedPiece.pieceModel.state,
+ board.mapToGame(pos))
+ return gameModel.isLegalPos(pickedPiece.pieceModel,
+ pickedPiece.pieceModel.state,
+ board.mapToGame(pos))
+ }
+ width: {
+ var f
+ if (board.isTrigon) f = 7
+ else if (board.isNexos) f = 12.5
+ else if (board.isGembloQ) f = 12
+ else if (board.isCallisto) f = 6.7
+ else f = 8.7
+ return Math.max(200, f * board.gridHeight)
+ }
+ height: width
+ pieceModel: pickedPiece ? pickedPiece.pieceModel : null
+ onPiecePlayed: Logic.playPickedPiece()
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameInfoDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+
+Pentobi.Dialog {
+ footer: DialogButtonBoxOkCancel { }
+ onOpened: {
+ textFieldPlayerName0.text = gameModel.playerName0
+ textFieldPlayerName1.text = gameModel.playerName1
+ textFieldPlayerName2.text = gameModel.playerName2
+ textFieldPlayerName3.text = gameModel.playerName3
+ textFieldDate.text = gameModel.date
+ textFieldTime.text = gameModel.time
+ textFieldEvent.text = gameModel.event
+ textFieldRound.text = gameModel.round
+ }
+ onAccepted: {
+ gameModel.playerName0 = textFieldPlayerName0.text
+ gameModel.playerName1 = textFieldPlayerName1.text
+ gameModel.playerName2 = textFieldPlayerName2.text
+ gameModel.playerName3 = textFieldPlayerName3.text
+ gameModel.date = textFieldDate.text
+ gameModel.time = textFieldTime.text
+ gameModel.event = textFieldEvent.text
+ gameModel.round = textFieldRound.text
+ }
+
+ Flickable {
+ implicitWidth: Math.max(Math.min(font.pixelSize * 22, maxContentWidth),
+ minContentWidth)
+ implicitHeight: Math.min(gridLayout.implicitHeight, maxContentHeight)
+ contentHeight: gridLayout.implicitHeight
+ clip: true
+
+ GridLayout {
+ id: gridLayout
+
+ anchors.fill: parent
+ columns: 2
+
+ Label {
+ text: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return qsTr("Player Blue/Red:")
+ if (gameModel.gameVariant === "duo")
+ return qsTr("Player Purple:")
+ if (gameModel.gameVariant === "junior")
+ return qsTr("Player Green:")
+ return qsTr("Player Blue:")
+ }
+ }
+ TextField {
+ id: textFieldPlayerName0
+
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ Label {
+ text: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return qsTr("Player Yellow/Green:")
+ if (gameModel.gameVariant === "duo" || gameModel.gameVariant === "junior")
+ return qsTr("Player Orange:")
+ if (gameModel.nuColors === 2)
+ return qsTr("Player Green:")
+ return qsTr("Player Yellow:")
+ }
+ }
+ TextField {
+ id: textFieldPlayerName1
+
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ Label {
+ visible: textFieldPlayerName2.visible
+ text: qsTr("Player Red:")
+ }
+ TextField {
+ id: textFieldPlayerName2
+
+ visible: gameModel.nuPlayers > 2
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ Label {
+ visible: textFieldPlayerName3.visible
+ text: qsTr("Player Green:")
+ }
+ TextField {
+ id: textFieldPlayerName3
+
+ visible: gameModel.nuPlayers > 3
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ Label { text: qsTr("Date:") }
+ TextField {
+ id: textFieldDate
+
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ Label { text: qsTr("Time:") }
+ TextField {
+ id: textFieldTime
+
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ Label { text: qsTr("Event:") }
+ TextField {
+ id: textFieldEvent
+
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ Label { text: qsTr("Round:") }
+ TextField {
+ id: textFieldRound
+
+ selectByMouse: true
+ Layout.fillWidth: true
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GameVariantDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+ property string gameVariant
+
+ footer: DialogButtonBoxOkCancel { }
+ onOpened: {
+ gameVariant = gameModel.gameVariant
+ if (gameVariant.startsWith("classic")) comboBox.currentIndex = 0
+ else if (gameVariant === "duo") comboBox.currentIndex = 1
+ else if (gameVariant === "junior") comboBox.currentIndex = 2
+ else if (gameVariant.startsWith("trigon")) comboBox.currentIndex = 3
+ else if (gameVariant.startsWith("nexos")) comboBox.currentIndex = 4
+ else if (gameVariant.startsWith("gembloq")) comboBox.currentIndex = 5
+ else if (gameVariant.startsWith("callisto")) comboBox.currentIndex = 6
+ }
+ onAccepted: Logic.changeGameVariant(gameVariant)
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: columnLayout.implicitHeight
+
+
+ ColumnLayout {
+ id: columnLayout
+
+ anchors.fill: parent
+
+ ComboBox {
+ id: comboBox
+
+ model: [
+ qsTr("Classic"), qsTr("Duo"), qsTr("Junior"),
+ qsTr("Trigon"), qsTr("Nexos"), qsTr("GembloQ"),
+ qsTr("Callisto") ]
+ onCurrentIndexChanged:
+ switch (currentIndex) {
+ case 0:
+ if (! gameVariant.startsWith("classic"))
+ gameVariant = "classic_2"
+ break
+ case 1:
+ if (gameVariant !== "duo")
+ gameVariant = "duo"
+ break
+ case 2:
+ if (gameVariant !== "junior")
+ gameVariant = "junior"
+ break
+ case 3:
+ if (! gameVariant.startsWith("trigon"))
+ gameVariant = "trigon_2"
+ break
+ case 4:
+ if (! gameVariant.startsWith("nexos"))
+ gameVariant = "nexos_2"
+ break
+ case 5:
+ if (! gameVariant.startsWith("gembloq"))
+ gameVariant = "gembloq_2"
+ break
+ case 6:
+ if (! gameVariant.startsWith("callisto"))
+ gameVariant = "callisto_2"
+ break
+ }
+ Layout.fillWidth: true
+ }
+ GridLayout {
+ columns: 2
+ Layout.fillWidth: true
+
+ Label {
+ text: qsTr("Players:")
+ Layout.fillWidth: true
+ }
+ RowLayout {
+ Layout.fillWidth: true
+
+ RadioButton {
+ text: "2"
+ checked: gameVariant === "classic_2"
+ || gameVariant === "duo"
+ || gameVariant === "junior"
+ || gameVariant === "trigon_2"
+ || gameVariant === "nexos_2"
+ || gameVariant === "gembloq_2"
+ || gameVariant === "callisto_2"
+ || gameVariant === "gembloq_2_4"
+ || gameVariant === "callisto_2_4"
+ onClicked:
+ if (checked) {
+ if (gameVariant.startsWith("classic"))
+ gameVariant = "classic_2"
+ else if (gameVariant.startsWith("trigon"))
+ gameVariant = "trigon_2"
+ else if (gameVariant.startsWith("nexos"))
+ gameVariant = "nexos_2"
+ else if (gameVariant === "callisto")
+ gameVariant = "callisto_2_4"
+ else if (gameVariant === "callisto_3")
+ gameVariant = "callisto_2"
+ else if (gameVariant === "gembloq")
+ gameVariant = "gembloq_2_4"
+ else if (gameVariant === "gembloq_3")
+ gameVariant = "gembloq_2"
+ }
+ Layout.fillWidth: true
+ }
+ RadioButton {
+ text: "3"
+ opacity: enabled
+ enabled: gameVariant.startsWith("classic")
+ || gameVariant.startsWith("trigon")
+ || gameVariant.startsWith("gembloq")
+ || gameVariant.startsWith("callisto")
+ checked: gameVariant === "classic_3"
+ || gameVariant === "trigon_3"
+ || gameVariant === "gembloq_3"
+ || gameVariant === "callisto_3"
+ onClicked:
+ if (checked) {
+ if (gameVariant.startsWith("classic"))
+ gameVariant = "classic_3"
+ else if (gameVariant.startsWith("trigon"))
+ gameVariant = "trigon_3"
+ else if (gameVariant.startsWith("gembloq"))
+ gameVariant = "gembloq_3"
+ else if (gameVariant.startsWith("callisto"))
+ gameVariant = "callisto_3"
+ }
+ Layout.fillWidth: true
+ }
+ RadioButton {
+ text: "4"
+ opacity: enabled
+ enabled: gameVariant.startsWith("classic")
+ || gameVariant.startsWith("trigon")
+ || gameVariant.startsWith("nexos")
+ || gameVariant.startsWith("gembloq")
+ || gameVariant.startsWith("callisto")
+ checked: gameVariant === "classic"
+ || gameVariant === "trigon"
+ || gameVariant === "nexos"
+ || gameVariant === "gembloq"
+ || gameVariant === "callisto"
+ onClicked:
+ if (checked) {
+ if (gameVariant.startsWith("classic"))
+ gameVariant = "classic"
+ else if (gameVariant.startsWith("trigon"))
+ gameVariant = "trigon"
+ else if (gameVariant.startsWith("nexos"))
+ gameVariant = "nexos"
+ else if (gameVariant.startsWith("gembloq"))
+ gameVariant = "gembloq"
+ else if (gameVariant.startsWith("callisto"))
+ gameVariant = "callisto"
+ }
+ Layout.fillWidth: true
+ }
+ }
+ Label {
+ text: qsTr("Colors:")
+ Layout.fillWidth: true
+ }
+ RowLayout {
+ Layout.fillWidth: true
+
+ RadioButton {
+ text: "2"
+ opacity: gameVariant === "duo"
+ || gameVariant === "junior"
+ || gameVariant === "gembloq_2"
+ || gameVariant === "gembloq_2_4"
+ || gameVariant === "callisto_2"
+ || gameVariant === "callisto_2_4"
+ enabled: gameVariant === "duo"
+ || gameVariant === "junior"
+ || gameVariant.startsWith("gembloq")
+ || gameVariant.startsWith("callisto")
+ checked: gameVariant === "duo"
+ || gameVariant === "junior"
+ || gameVariant === "gembloq_2"
+ || gameVariant === "callisto_2"
+ onClicked:
+ if (checked) {
+ if (gameVariant.startsWith("callisto"))
+ gameVariant = "callisto_2"
+ else if (gameVariant.startsWith("gembloq"))
+ gameVariant = "gembloq_2"
+ }
+ Layout.fillWidth: true
+ }
+ RadioButton {
+ text: "3"
+ opacity: checked
+ enabled: gameVariant.startsWith("trigon")
+ || gameVariant.startsWith("gembloq")
+ || gameVariant.startsWith("callisto")
+ checked: gameVariant === "trigon_3"
+ || gameVariant === "gembloq_3"
+ || gameVariant === "callisto_3"
+ onClicked:
+ if (checked) {
+ if (gameVariant.startsWith("trigon"))
+ gameVariant = "trigon_3"
+ else if (gameVariant.startsWith("gembloq"))
+ gameVariant = "gembloq_3"
+ else if (gameVariant.startsWith("callisto"))
+ gameVariant = "callisto_3"
+ }
+ Layout.fillWidth: true
+ }
+ RadioButton {
+ text: "4"
+ opacity: gameVariant.startsWith("classic")
+ || gameVariant === "trigon"
+ || gameVariant === "trigon_2"
+ || gameVariant.startsWith("nexos")
+ || gameVariant === "gembloq"
+ || gameVariant === "gembloq_2"
+ || gameVariant === "gembloq_2_4"
+ || gameVariant === "callisto"
+ || gameVariant === "callisto_2"
+ || gameVariant === "callisto_2_4"
+ enabled: gameVariant.startsWith("classic")
+ || gameVariant.startsWith("trigon")
+ || gameVariant.startsWith("nexos")
+ || gameVariant.startsWith("gembloq")
+ || gameVariant.startsWith("callisto")
+ checked: gameVariant.startsWith("classic")
+ || gameVariant === "trigon"
+ || gameVariant === "trigon_2"
+ || gameVariant.startsWith("nexos")
+ || gameVariant === "gembloq"
+ || gameVariant === "gembloq_2_4"
+ || gameVariant === "callisto"
+ || gameVariant === "callisto_2_4"
+ onClicked:
+ if (checked) {
+ if (gameVariant.startsWith("trigon"))
+ gameVariant = "trigon"
+ else if (gameVariant.startsWith("nexos"))
+ gameVariant = "nexos"
+ else if (gameVariant === "gembloq_2")
+ gameVariant = "gembloq_2_4"
+ else if (gameVariant === "gembloq_3")
+ gameVariant = "gembloq"
+ else if (gameVariant === "callisto_2")
+ gameVariant = "callisto_2_4"
+ else if (gameVariant === "callisto_3")
+ gameVariant = "callisto"
+ }
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/GotoMoveDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ footer: Pentobi.DialogButtonBox {
+ ButtonOk {
+ enabled: textField.acceptableInput
+ onClicked: checkAccept()
+ DialogButtonBox.buttonRole: DialogButtonBox.InvalidRole
+ }
+ ButtonCancel { }
+ }
+ onOpened: textField.selectAll()
+ onAccepted: gameModel.gotoMove(parseInt(textField.text))
+
+ function returnPressed() {
+ if (! hasButtonFocus())
+ checkAccept()
+ }
+ function checkAccept() {
+ if (textField.acceptableInput)
+ accept()
+ }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(rowLayout.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: rowLayout.implicitHeight
+
+ RowLayout {
+ id: rowLayout
+
+ anchors.fill: parent
+
+ Label { text: qsTr("Move number:") }
+ TextField {
+ id: textField
+
+ text: gameModel.moveNumber === 0 ?
+ gameModel.moveNumber + gameModel.movesLeft : gameModel.moveNumber
+ focus: true
+ selectByMouse: true
+ inputMethodHints: Qt.ImhDigitsOnly
+ validator: IntValidator{
+ bottom: 0
+ top: gameModel.moveNumber + gameModel.movesLeft
+ }
+ Layout.preferredWidth: font.pixelSize * 5
+ }
+ Item { Layout.fillWidth: true }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/HelpWindow.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import QtQuick.Layouts 1.1
+import QtQuick.Window 2.1
+import QtQuick.Controls 2.3
+import QtWebView 1.1
+import pentobi 1.0
+import Qt.labs.settings 1.0
+
+Window {
+ id: root
+
+ property url startUrl
+ property real defaultWidth: Math.min(font.pixelSize * 48, Screen.desktopAvailableWidth)
+ property real defaultHeight: Math.min(font.pixelSize * 57, Screen.desktopAvailableHeight)
+
+ // Instead of initializing webView.url, we provide an init function that
+ // needs to be called after show() to work around an issue with the initial
+ // zoom factor of WebView sometimes very large on Android. Note that this
+ // workaround only reduces the likelihood for this bug to occur
+ // (QTBUG-58290, last occurred with Qt 5.11.2)
+ function init() { webView.url = startUrl }
+
+ width: defaultWidth; height: defaultHeight
+ minimumWidth: 240; minimumHeight: 240
+ x: (Screen.width - defaultWidth) / 2
+ y: (Screen.height - defaultHeight) / 2
+ title: qsTr("Pentobi Help")
+
+ // Note that Android doesn't actually support multiple windows, but using
+ // WebView in a window works around a bug related to QTBUG-62409, which
+ // makes WebView consume Back button events, so we cannot close the help
+ // window with the back key. But we need to destroy the window after
+ // closing, otherwise it doesn't show when made visible again.
+ onClosing: if (isAndroid) helpWindow.source = ""
+
+ WebView {
+ id: webView
+
+ anchors.fill: parent
+ }
+ Shortcut {
+ sequence: "Ctrl+W"
+ onActivated: close()
+ }
+ Shortcut {
+ sequence: "Alt+Left"
+ onActivated: webView.goBack()
+ }
+ Shortcut {
+ sequence: "Alt+Right"
+ onActivated: webView.goForward()
+ }
+ Settings {
+ property alias x: root.x
+ property alias y: root.y
+ property alias width: root.width
+ property alias height: root.height
+
+ category: "HelpWindow"
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ImageSaveDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+ title: qsTr("Save Image")
+ selectExisting: false
+ nameFilterLabels: [
+ qsTr("PNG image files"),
+ qsTr("JPEG image files")
+ ]
+ nameFilters: [
+ [ "*.png", "*.PNG" ],
+ [ "*.jpg", "*.JPG", "*.jpeg", "*.JPEG" ]
+ ]
+ folder: rootWindow.folder
+ onNameFilterChanged: {
+ if (index >= nameFilters.length)
+ return
+ var pos = name.lastIndexOf(".")
+ if (pos < 0)
+ return
+ var newName = name.substr(0, pos + 1)
+ pos = nameFilters[index][0].lastIndexOf(".")
+ newName += nameFilters[index][0].substr(pos + 1)
+ name = newName
+ selectNameField()
+ }
+ onAccepted: {
+ rootWindow.folder = folder
+ Logic.exportImage(fileUrl)
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/InitialRatingDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+ footer: DialogButtonBoxOkCancel { }
+ onAccepted: {
+ ratingModel.setInitialRating(Math.round(slider.value))
+ Logic.ratedGameNoVerify()
+ }
+
+ ColumnLayout
+ {
+ Item {
+ implicitWidth:
+ Math.max(Math.min(textLabel.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: textLabel.implicitHeight
+ Layout.fillWidth: true
+
+ Label {
+ id: textLabel
+
+ anchors.fill: parent
+ text: qsTr("Initialize your rating for this game variant.")
+ wrapMode: Text.Wrap
+ }
+ }
+ RowLayout {
+ Layout.topMargin: 0.6 * font.pixelSize
+
+ Label {
+ text: qsTr("Initial rating:")
+ }
+ Label {
+ text: Math.round(slider.value)
+ font.bold: true
+ }
+ }
+ Slider {
+ id: slider
+
+ value: 800
+ from: 800; to: 2000; stepSize: 100
+ Layout.fillWidth: true
+ }
+ RowLayout {
+ Layout.fillWidth: true
+
+ Label { text: qsTr("Beginner") }
+ Item { Layout.fillWidth: true }
+ Label { text: qsTr("Expert") }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/LineSegment.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece element for Nexos. See Square.qml for comments.
+Item {
+ id: root
+
+ property bool isHorizontal
+
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacity0
+ sourceComponent: opacity > 0 || item ? component0 : null
+
+ Component {
+ id: component0
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ mirror: ! isHorizontal
+ rotation: isHorizontal ? 0 : -90
+ }
+ }
+ }
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacitySmall0
+ sourceComponent: opacity > 0 || item ? componentSmall0 : null
+
+ Component {
+ id: componentSmall0
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ mirror: ! isHorizontal
+ rotation: isHorizontal ? 0 : -90
+ }
+ }
+ }
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacity90
+ sourceComponent: opacity > 0 || item ? component90 : null
+
+ Component {
+ id: component90
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ mirror: isHorizontal
+ rotation: isHorizontal ? -180 : -90
+ }
+ }
+ }
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacitySmall90
+ sourceComponent: opacity > 0 || item ? componentSmall90 : null
+
+ Component {
+ id: componentSmall90
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ mirror: isHorizontal
+ rotation: isHorizontal ? -180 : -90
+ }
+ }
+ }
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacity180
+ sourceComponent: opacity > 0 || item ? component180 : null
+
+ Component {
+ id: component180
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ mirror: ! isHorizontal
+ rotation: isHorizontal ? -180 : -270
+ }
+ }
+ }
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacitySmall180
+ sourceComponent: opacity > 0 || item ? componentSmall180 : null
+
+ Component {
+ id: componentSmall180
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ mirror: ! isHorizontal
+ rotation: isHorizontal ? -180 : -270
+ }
+ }
+ }
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacity270
+ sourceComponent: opacity > 0 || item ? component270 : null
+
+ Component {
+ id: component270
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ mirror: isHorizontal
+ rotation: isHorizontal ? 0 : -270
+ }
+ }
+ }
+ Loader {
+ anchors.fill: root
+ opacity: imageOpacitySmall270
+ sourceComponent: opacity > 0 || item ? componentSmall270 : null
+
+ Component {
+ id: componentSmall270
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ antialiasing: true
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ mirror: isHorizontal
+ rotation: isHorizontal ? 0 : -270
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Main.js
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+function analyzeGame(nuSimulations) {
+ if (! gameModel.isMainVar) {
+ showInfo(qsTr("Game analysis is only possible in main variation."))
+ return
+ }
+ gameDisplay.startAnalysis()
+ cancelRunning()
+ analyzeGameModel.start(gameModel, playerModel, nuSimulations)
+}
+
+function autoSaveNoVerify() {
+ gameModel.autoSave()
+ syncSettings.setValueBool("computerPlays0", computerPlays0)
+ syncSettings.setValueBool("computerPlays1", computerPlays1)
+ syncSettings.setValueBool("computerPlays2", computerPlays2)
+ syncSettings.setValueBool("computerPlays3", computerPlays3)
+ syncSettings.setValueBool("isRated", isRated)
+ syncSettings.setValueBool("initComputerColorsOnNewGame", initComputerColorsOnNewGame)
+ syncSettings.setValueInt("level", playerModel.level)
+ syncSettings.sync()
+ analyzeGameModel.autoSave(gameModel)
+ // This will lose the geometry if the user has changed it, maximized the
+ // window and then closed it before returning to windowed state. But better
+ // than overwriting the geometry with the maximized/fullscreen one.
+ if (visibility === Window.Windowed) {
+ settings.x = x
+ settings.y = y
+ settings.width = width
+ settings.height = height
+ }
+ settings.visibility = visibility
+}
+
+function autoSaveNoVerifyAndQuit() {
+ autoSaveNoVerify()
+ Qt.quit()
+}
+
+function cancelRunning(showMessage) {
+ if (analyzeGameModel.isRunning) {
+ analyzeGameModel.cancel()
+ if (showMessage)
+ showTemporaryMessage(qsTr("Game analysis aborted"))
+ }
+ if (playerModel.isGenMoveRunning) {
+ playerModel.cancelGenMove()
+ if (showMessage)
+ showTemporaryMessage(qsTr("Computer move aborted"))
+ }
+ delayedCheckComputerMove.stop()
+}
+
+function changeGameVariant(gameVariant) {
+ if (gameModel.gameVariant === gameVariant)
+ return
+ verify(function() { changeGameVariantNoVerify(gameVariant) })
+}
+
+function changeGameVariantNoVerify(gameVariant) {
+ cancelRunning()
+ lengthyCommand.run(function() {
+ // Destroy pieces before changing game variant to avoid flickering
+ // in PieceSelectorMobile if toPlay != 0
+ gameDisplay.destroyPieces()
+ gameModel.changeGameVariant(gameVariant)
+ gameDisplay.createPieces()
+ gameDisplay.showToPlay()
+ gameDisplay.setupMode = false
+ isRated = false
+ analyzeGameModel.clear()
+ gameDisplay.showPieces()
+ initComputerColors()
+ })
+}
+
+function checkComputerMove() {
+ if (gameModel.isGameOver) {
+ var msg = gameModel.getResultMessage()
+ if (isRated) {
+ var oldRating = Math.round(ratingModel.rating)
+ ratingModel.addResult(gameModel, playerModel.level)
+ var newRating = Math.round(ratingModel.rating)
+ msg += "\n"
+ if (newRating > oldRating)
+ msg += qsTr("Your rating has increased from %1 to %2.").arg(oldRating).arg(newRating)
+ else if (newRating < oldRating)
+ msg += qsTr("Your rating has decreased from %1 to %2.").arg(oldRating).arg(newRating)
+ else
+ msg += qsTr("Your rating stays at %1.").arg(newRating)
+ isRated = false
+ }
+ showInfo(msg)
+ 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()
+}
+
+function checkStoragePermission() {
+ if (! androidUtils.checkPermission("android.permission.WRITE_EXTERNAL_STORAGE")) {
+ showInfo(qsTr("No permission to access storage"))
+ return false
+ }
+ return true
+}
+
+function clearRating() {
+ showQuestion(qsTr("Delete all rating information for the current game variant?"),
+ clearRatingNoVerify)
+}
+
+function clearRatingNoVerify() {
+ ratingModel.clearRating()
+ showTemporaryMessage(qsTr("Rating information deleted"))
+}
+
+/** 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()) {
+ setComputerNone()
+ 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
+ {
+ 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
+ }
+ }
+ initComputerColorsOnNewGame = true
+ }
+ 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 = "themes/" + themeName + "/Theme.qml"
+ var component = Qt.createComponent(source)
+ if (component.status !== Component.Ready) {
+ console.warn(component.errorString())
+ source = "themes/light/Theme.qml"
+ component = Qt.createComponent(source)
+ }
+ return component.createObject(rootWindow)
+}
+
+function deleteAllVar() {
+ showQuestion(qsTr("Delete all variations?"), deleteAllVarNoVerify)
+}
+
+function deleteAllVarNoVerify() {
+ gameModel.deleteAllVar()
+ showTemporaryMessage(qsTr("Variations deleted"))
+}
+
+function exportAsciiArt(fileUrl) {
+ if (! checkStoragePermission())
+ return
+ var file = getFileFromUrl(fileUrl)
+ if (! gameModel.saveAsciiArt(file))
+ showInfo(qsTr("Save failed.") + "\n" + gameModel.getError())
+ else {
+ androidUtils.scanFile(file)
+ showTemporaryMessage(qsTr("File saved"))
+ }
+}
+
+function exportImage(fileUrl) {
+ if (! checkStoragePermission())
+ return
+ var board = gameDisplay.getBoard()
+ var size = Qt.size(exportImageWidth, exportImageWidth * board.height / board.width)
+ if (! board.grabImageTarget.grabToImage(function(result) {
+ var file = getFileFromUrl(fileUrl)
+ if (! result.saveToFile(file))
+ showInfo(qsTr("Saving image failed or unsupported image format"))
+ else {
+ androidUtils.scanFile(file)
+ showTemporaryMessage(qsTr("Image saved"))
+ }
+ }, size))
+ showInfo(qsTr("Creating image failed"))
+}
+
+function findNextComment() {
+ if (gameModel.findNextComment()) {
+ gameDisplay.showComment()
+ return
+ }
+ if (gameModel.canGoBackward)
+ // Current is not root
+ showQuestion(qsTr("End of tree was reached. Continue search from start of the tree?"),
+ findNextCommentContinueFromRoot)
+ else
+ showInfo(qsTr("No comment found"))
+
+}
+
+function findNextCommentContinueFromRoot() {
+ if (gameModel.findNextCommentContinueFromRoot()) {
+ gameDisplay.showComment()
+ return
+ }
+ showInfo(qsTr("No comment found"))
+}
+
+function genMove() {
+ cancelRunning()
+ gameDisplay.pickedPiece = null
+ gameDisplay.showToPlay()
+ playerModel.startGenMove(gameModel)
+}
+
+function getFileFromUrl(fileUrl) {
+ var file = fileUrl.toString()
+ file = file.replace(/^(file:\/{3})/,"/")
+ return decodeURIComponent(file)
+}
+
+function getFileInfo(isRated, file, isModified) {
+ if (isRated)
+ //: Label for rated game. The argument is the game number.
+ return qsTr("Rated Game %1").arg(ratingModel.numberGames + 1)
+ if (isModified)
+ return qsTr("%1 (modified)").arg(file)
+ return file
+}
+
+function getGameLabel(setupMode, isRated, file, isModified, short) {
+ if (setupMode)
+ return short ?
+ //: Small-screen label for setup mode (short for
+ //: "Setup Mode").
+ qsTr("Setup")
+ : qsTr("Setup Mode")
+ if (isRated)
+ //: Label for ongoing rated game
+ return qsTr("Rated")
+ if (file === "")
+ return ""
+ var label
+ var n = ratingModel.getGameNumberOfFile(file)
+ if (n > 0)
+ label = short ?
+ //: Small-screen label for finished rated game (short for
+ //: "Rated Game"). The argument is the game number.
+ qsTr("Rated %1").arg(n)
+ : qsTr("Rated Game %1").arg(n)
+ else {
+ var pos = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"))
+ label = file.substring(pos + 1)
+ if (label.toLowerCase().endsWith(".blksgf"))
+ label = label.substring(0, label.length - ".blksgf".length)
+ }
+ return (isModified ? "*" : "") + label
+}
+
+function getWindowTitle(file, isModified) {
+ if (file === "")
+ //: Window title if no file is loaded.
+ return qsTr("Pentobi")
+ var pos = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"))
+ var name = file.substring(pos + 1)
+ if (isModified)
+ name = "*" + name
+ //: Window title if file is loaded. The argument is the file name
+ //: prepended with a star if the file has been modified.
+ return qsTr("%1 - Pentobi").arg(name)
+}
+
+function help() {
+ var lang = Qt.locale().name
+ var pos = lang.indexOf("_")
+ if (pos >= 0)
+ lang = lang.substr(0, pos)
+ if (lang !== "C" && lang !== "de")
+ lang = "C"
+ var url
+ if (isAndroid)
+ url = androidUtils.extractHelp(lang)
+ else if (helpDir)
+ url = "file://" + helpDir + "/" + lang + "/pentobi/index.html"
+ else
+ url = "qrc:///qml/help/" + lang + "/pentobi/index.html"
+ if (openHelpExternally) {
+ Qt.openUrlExternally(url)
+ return
+ }
+ if (! helpWindow.item) {
+ helpWindow.source = "HelpWindow.qml"
+ helpWindow.item.startUrl = url
+ }
+ helpWindow.item.show()
+ helpWindow.item.init()
+}
+
+function init() {
+ if (gameModel.loadAutoSave()) {
+ computerPlays0 =
+ syncSettings.valueBool("computerPlays0", computerPlays0)
+ computerPlays1 =
+ syncSettings.valueBool("computerPlays1", computerPlays1)
+ computerPlays2 =
+ syncSettings.valueBool("computerPlays2", computerPlays2)
+ computerPlays3 =
+ syncSettings.valueBool("computerPlays3", computerPlays3)
+ isRated = syncSettings.valueBool("isRated", isRated)
+ initComputerColorsOnNewGame =
+ syncSettings.valueBool("initComputerColorsOnNewGame",
+ initComputerColorsOnNewGame)
+ }
+ playerModel.level = syncSettings.valueInt("level", 1)
+ if (isMultiColor()) {
+ computerPlays2 = computerPlays0
+ computerPlays3 = computerPlays1
+ }
+ gameDisplay.createPieces()
+ if (gameModel.checkFileModifiedOutside())
+ {
+ showWindow()
+ showQuestion(qsTr("File has been modified by another application. Reload?"), reloadFile)
+ return
+ }
+ analyzeGameModel.loadAutoSave(gameModel)
+ if (analyzeGameModel.elements.length > 0)
+ gameDisplay.analysisAutoloaded()
+ // initialFile is a context property set from command line argument
+ if (initialFile) {
+ if (gameModel.isModified)
+ showWindow()
+ verify(function() { openFileBlocking(initialFile) })
+ }
+ showWindow()
+ if (isRated) {
+ // Game-related properties in settings could be inconsistent with
+ // autosaved game, better initialize with info from ratingModel
+ var player = ratingModel.getNextHumanPlayer()
+ computerPlays0 = (player !== 0)
+ computerPlays1 = (player !== 1)
+ computerPlays2 = (player !== 2)
+ computerPlays3 = (player !== 3)
+ if (isMultiColor()) {
+ computerPlays2 = computerPlays0
+ computerPlays3 = computerPlays1
+ }
+ playerModel.level = ratingModel.getNextLevel(playerModel.maxLevel)
+ showInfo(qsTr("Continuing rated game"))
+ checkComputerMove()
+ return
+ }
+ if (isComputerToPlay() && ! gameModel.canGoForward
+ && ! gameModel.isGameOver)
+ showQuestion(qsTr("Continue computer move?"), checkComputerMove)
+}
+
+function initComputerColors() {
+ if (! initComputerColorsOnNewGame)
+ return
+ // Default setting is that the computer plays all colors but the first
+ computerPlays0 = false
+ computerPlays1 = true
+ computerPlays2 = true
+ computerPlays3 = true
+ if (isMultiColor())
+ computerPlays2 = false
+}
+
+function isComputerToPlay() {
+ if (gameModel.gameVariant == "classic_3" && gameModel.toPlay === 3)
+ return computerPlays(gameModel.altPlayer)
+ return computerPlays(gameModel.toPlay)
+}
+
+function isMultiColor() {
+ return gameModel.nuColors === 4 && gameModel.nuPlayers === 2
+}
+
+function keepOnlyPosition() {
+ showQuestion(qsTr("Keep only position?"), keepOnlyPositionNoVerify)
+}
+
+function keepOnlyPositionNoVerify() {
+ gameModel.keepOnlyPosition()
+ showTemporaryMessage(qsTr("Kept only position"))
+}
+
+function keepOnlySubtree() {
+ showQuestion(qsTr("Keep only subtree?"), keepOnlySubtreeNoVerify)
+}
+
+function keepOnlySubtreeNoVerify() {
+ gameModel.keepOnlySubtree()
+ showTemporaryMessage(qsTr("Kept only subtree"))
+}
+
+function moveDownVar() {
+ gameModel.moveDownVar()
+ showVariationInfo()
+}
+
+function moveGenerated(move) {
+ gameModel.playMove(move)
+ if (isPlaySingleMoveRunning)
+ isPlaySingleMoveRunning = false
+ else
+ delayedCheckComputerMove.restart()
+}
+
+function moveUpVar() {
+ gameModel.moveUpVar()
+ showVariationInfo()
+}
+
+function newGame()
+{
+ verify(newGameNoVerify)
+}
+
+function newGameNoVerify()
+{
+ gameModel.newGame()
+ gameDisplay.setupMode = false
+ gameDisplay.showToPlay()
+ gameDisplay.showPieces()
+ isRated = false
+ analyzeGameModel.clear()
+ initComputerColors()
+}
+
+function nextPiece() {
+ var currentPickedPiece = null
+ if (gameDisplay.pickedPiece)
+ currentPickedPiece = gameDisplay.pickedPiece.pieceModel
+ var pieceModel = gameModel.nextPiece(currentPickedPiece)
+ if (pieceModel)
+ gameDisplay.pickPieceAtBoard(gameDisplay.findPiece(pieceModel))
+}
+
+function open() {
+ if (! checkStoragePermission())
+ return
+ verify(openNoVerify)
+}
+
+function openNoVerify() {
+ openDialog.open()
+}
+
+function openFile(file) {
+ lengthyCommand.run(function() { openFileBlocking(file) })
+}
+
+function openFileBlocking(file) {
+ var oldGameVariant = gameModel.gameVariant
+ var oldEnableAnimations = gameDisplay.enableAnimations
+ gameDisplay.enableAnimations = false
+ if (! gameModel.openFile(file))
+ showInfo(qsTr("Open failed.") + "\n" + gameModel.getError())
+ else
+ setComputerNone()
+ if (gameModel.gameVariant != oldGameVariant)
+ gameDisplay.createPieces()
+ gameDisplay.showToPlay()
+ gameDisplay.enableAnimations = oldEnableAnimations
+ gameDisplay.setupMode = false
+ isRated = false
+ analyzeGameModel.clear()
+ if (gameModel.comment.length > 0)
+ gameDisplay.showComment()
+ else
+ gameDisplay.showPieces()
+}
+
+function openFileUrl() {
+ openFile(getFileFromUrl(openDialog.item.fileUrl))
+}
+
+function openClipboard()
+{
+ verify(openClipboardNoVerify)
+}
+
+function openClipboardNoVerify() {
+ lengthyCommand.run(function() {
+ var oldGameVariant = gameModel.gameVariant
+ var oldEnableAnimations = gameDisplay.enableAnimations
+ gameDisplay.enableAnimations = false
+ if (! gameModel.openClipboard())
+ showInfo(qsTr("Open failed.") + "\n" + gameModel.getError())
+ else
+ setComputerNone()
+ if (gameModel.gameVariant != oldGameVariant)
+ gameDisplay.createPieces()
+ gameDisplay.showToPlay()
+ gameDisplay.enableAnimations = oldEnableAnimations
+ gameDisplay.setupMode = false
+ isRated = false
+ analyzeGameModel.clear()
+ })
+}
+
+function openRecentFile(file) {
+ verify(function() { openFile(file) })
+}
+
+function pickNamedPiece(name) {
+ var currentPickedPiece = null
+ if (gameDisplay.pickedPiece)
+ currentPickedPiece = gameDisplay.pickedPiece.pieceModel
+ var pieceModel = gameModel.pickNamedPiece(name, currentPickedPiece)
+ if (pieceModel)
+ gameDisplay.pickPieceAtBoard(gameDisplay.findPiece(pieceModel))
+}
+
+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 prevPiece() {
+ var currentPickedPiece = null
+ if (gameDisplay.pickedPiece)
+ currentPickedPiece = gameDisplay.pickedPiece.pieceModel
+ var pieceModel = gameModel.previousPiece(currentPickedPiece)
+ if (pieceModel)
+ gameDisplay.pickPieceAtBoard(gameDisplay.findPiece(pieceModel))
+}
+
+function quit() {
+ if (gameModel.checkAutosaveModifiedOutside()) {
+ if (! gameModel.isModified)
+ return true
+ showQuestion(qsTr("Autosaved game was changed by another instance of Pentobi. Overwrite?"),
+ autoSaveNoVerifyAndQuit)
+ return false
+ }
+ autoSaveNoVerify()
+ return true
+}
+
+function ratedGame()
+{
+ verify(ratedGameCheckFirstGame)
+}
+
+function ratedGameCheckFirstGame() {
+ if (ratingModel.numberGames === 0)
+ initialRatingDialog.open()
+ else
+ ratedGameNoVerify()
+}
+
+function ratedGameNoVerify()
+{
+ var player = ratingModel.getNextHumanPlayer()
+ var level = ratingModel.getNextLevel(playerModel.maxLevel)
+ var gameVariant = gameModel.gameVariant
+ var msg
+ switch (player) {
+ case 0:
+ if (gameVariant === "duo")
+ msg = qsTr("Start rated game with Purple against Pentobi level %1?").arg(level)
+ else if (gameVariant === "junior")
+ msg = qsTr("Start rated game with Green against Pentobi level %1?").arg(level)
+ else if (isMultiColor())
+ msg = qsTr("Start rated game with Blue/Red against Pentobi level %1?").arg(level)
+ else
+ msg = qsTr("Start rated game with Blue against Pentobi level %1?").arg(level)
+ break
+ case 1:
+ if (gameVariant === "duo" || gameVariant === "junior")
+ msg = qsTr("Start rated game with Orange against Pentobi level %1?").arg(level)
+ else if (isMultiColor())
+ msg = qsTr("Start rated game with Yellow/Green against Pentobi level %1?").arg(level)
+ else if (gameModel.nuColors === 2)
+ msg = qsTr("Start rated game with Green against Pentobi level %1?").arg(level)
+ else
+ msg = qsTr("Start rated game with Yellow against Pentobi level %1?").arg(level)
+ break
+ case 2:
+ msg = qsTr("Start rated game with Red against Pentobi level %1?").arg(level)
+ break
+ case 3:
+ msg = qsTr("Start rated game with Green against Pentobi level %1?").arg(level)
+ break
+ }
+ showQuestion(msg, ratedGameStart)
+}
+
+function ratedGameStart() {
+ var player = ratingModel.getNextHumanPlayer()
+ computerPlays0 = (player !== 0)
+ computerPlays1 = (player !== 1)
+ computerPlays2 = (player !== 2)
+ computerPlays3 = (player !== 3)
+ if (isMultiColor()) {
+ computerPlays2 = computerPlays0
+ computerPlays3 = computerPlays1
+ }
+ var level = ratingModel.getNextLevel(playerModel.maxLevel)
+ playerModel.level = level
+ gameModel.newGame()
+ //: Player name for game info in rated game. First argument is version of
+ //: Pentobi, second argument is level.
+ var computerName =
+ qsTr("Pentobi %1 (level %2)").arg(Qt.application.version).arg(level)
+ //: Player name for game info in rated game.
+ var humanName = qsTr("Human")
+ gameModel.playerName0 = computerPlays0 ? computerName : humanName
+ gameModel.playerName1 = computerPlays1 ? computerName : humanName
+ if (gameModel.nuPlayers > 2)
+ gameModel.playerName2 = computerPlays2 ? computerName : humanName
+ if (gameModel.nuPlayers > 3)
+ gameModel.playerName3 = computerPlays3 ? computerName : humanName
+ gameModel.event = qsTr("Rated game")
+ gameModel.round = ratingModel.numberGames + 1
+ gameDisplay.setupMode = false
+ gameDisplay.showToPlay()
+ gameDisplay.showPieces()
+ isRated = true
+ analyzeGameModel.clear()
+ checkComputerMove()
+}
+
+function rating() {
+ if (ratingModel.numberGames === 0) {
+ showInfo(qsTr("You have not yet played rated games in this game variant."))
+ return
+ }
+ // Never reuse RatingDialog
+ // See comment in Main.qml at ratingModel.onHistoryChanged
+ ratingDialog.sourceComponent = null
+ ratingDialog.open()
+}
+
+function reloadFile() {
+ openFile(gameModel.file)
+}
+
+function save() {
+ if (! checkStoragePermission())
+ return
+ if (gameModel.checkFileModifiedOutside())
+ showQuestion(qsTr("File has been modified by another application. Overwrite?"),
+ saveCurrentFile)
+ else
+ saveCurrentFile()
+}
+
+function saveAs() {
+ if (! checkStoragePermission())
+ return
+ var dialog = saveDialog.get()
+ dialog.name = gameModel.suggestGameFileName(folder)
+ dialog.open()
+}
+
+function saveCurrentFile() {
+ saveFile(gameModel.file)
+}
+
+function saveFile(file) {
+ if (! gameModel.save(file))
+ showInfo(qsTr("Save failed.") + "\n" + gameModel.getError())
+ else
+ showTemporaryMessage(qsTr("File saved"))
+}
+
+function setComputerNone() {
+ computerPlays0 = false
+ computerPlays1 = false
+ computerPlays2 = false
+ computerPlays3 = false
+}
+
+function showFatal(text) {
+ var dialog = fatalMessage.get()
+ dialog.text = text
+ dialog.open()
+}
+
+function showInfo(text) {
+ var dialog = infoMessage.get()
+ dialog.text = text
+ dialog.open()
+}
+
+function showQuestion(text, acceptedFunc) {
+ questionMessage.get().openWithCallback(text, acceptedFunc)
+}
+
+function showTemporaryMessage(text) {
+ gameDisplay.showTemporaryMessage(text)
+}
+
+function showVariationInfo() {
+ showTemporaryMessage(qsTr("Variation is now %1").arg(gameModel.getVariationInfo()))
+}
+
+function showWindow() {
+ x = settings.x
+ y = settings.y
+ width = settings.width
+ height = settings.height
+ switch (settings.visibility) {
+ case Window.Maximized: showMaximized(); break
+ case Window.FullScreen: showFullScreen(); break
+ default: show()
+ }
+}
+
+function truncate() {
+ showQuestion(qsTr("Truncate this subtree?"), gameModel.truncate)
+}
+
+function truncateChildren() {
+ showQuestion(qsTr("Truncate children?"), truncateChildrenNoVerify)
+}
+
+function truncateChildrenNoVerify() {
+ gameModel.truncateChildren()
+ showTemporaryMessage(qsTr("Children truncated"))
+}
+
+function undo() {
+ gameModel.undo()
+}
+
+function verify(callback)
+{
+ if (gameModel.isModified) {
+ showQuestion(qsTr("Discard game?"), callback)
+ return
+ }
+ callback()
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Main.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQml 2.2
+import QtQuick 2.11
+import QtQuick.Controls 2.3
+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: rootWindow
+
+ property bool computerPlays0
+ property bool computerPlays1: true
+ property bool computerPlays2: true
+ property bool computerPlays3: true
+ property bool isPlaySingleMoveRunning
+ property bool isRated
+
+ property alias gameDisplay: gameDisplayLoader.item
+
+ // If the user manually disabled all computer colors in the dialog, we
+ // assume that they want to edit games rather than play, and we will not
+ // initialize the computer colors on New Game but only clear the board.
+ property bool initComputerColorsOnNewGame: true
+
+ property bool isAndroid: Qt.platform.os === "android"
+ property string themeName: isAndroid ? "dark" : "system"
+ property QtObject theme: Logic.createTheme(themeName)
+ property url folder: androidUtils.getDefaultFolder()
+
+ property real defaultWidth:
+ isAndroid ? Screen.desktopAvailableWidth
+ : Math.min(Screen.desktopAvailableWidth, 1164)
+ property real defaultHeight:
+ isAndroid ? Screen.desktopAvailableHeight
+ : Math.min(Screen.desktopAvailableHeight, 662)
+
+ property int exportImageWidth: 420
+ property bool busyIndicatorRunning: lengthyCommand.isRunning
+ || playerModel.isGenMoveRunning
+ || analyzeGameModel.isRunning
+ property bool showToolBar: true
+
+ minimumWidth: isDesktop ? 481 : 240
+ minimumHeight: isDesktop ? 303 : 301
+ color: theme.colorBackground
+ title: Logic.getWindowTitle(gameModel.file, gameModel.isModified)
+ onClosing: if ( ! Logic.quit()) close.accepted = false
+ Component.onCompleted: Logic.init()
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: gameDisplay.dropCommentFocus()
+ }
+ Pentobi.ToolBar {
+ id: toolBar
+
+ visible: isDesktop || visibility !== Window.FullScreen
+ showContent: ! isDesktop || showToolBar
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ margins: isDesktop ? 2 : 0
+ }
+ }
+ Loader {
+ id: gameDisplayLoader
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: toolBar.visible ? toolBar.bottom : parent.top
+ bottom: parent.bottom
+ margins: isDesktop ? 2 : 0
+ }
+ source:
+ isDesktop ? "GameDisplayDesktop.qml" : "GameDisplayMobile.qml"
+
+ Connections {
+ target: gameDisplayLoader.item
+ onPlay: Logic.play(pieceModel, gameCoord)
+ }
+ }
+ MouseArea {
+ visible: isDesktop
+ acceptedButtons: Qt.NoButton // only for setting cursor shape
+ anchors.fill: parent
+ cursorShape: busyIndicatorRunning ? Qt.BusyCursor : Qt.ArrowCursor
+ }
+ Settings {
+ id: settings
+
+ property real x: (Screen.width - defaultWidth) / 2
+ property real y: (Screen.height - defaultHeight) / 2
+ property real width: defaultWidth
+ property real height: defaultHeight
+ property int visibility
+ property alias folder: rootWindow.folder
+ property alias themeName: rootWindow.themeName
+ property alias exportImageWidth: rootWindow.exportImageWidth
+ property alias showToolBar: rootWindow.showToolBar
+ property alias showVariations: gameModel.showVariations
+ }
+ GameModel {
+ id: gameModel
+
+ onPositionAboutToChange: Logic.cancelRunning(true)
+ onPositionChanged: {
+ gameDisplay.pickedPiece = null
+ if (gameModel.canGoBackward || gameModel.canGoForward
+ || gameModel.moveNumber > 0)
+ gameDisplay.setupMode = false
+ analyzeGameModel.markCurrentMove(gameModel)
+ gameDisplay.dropCommentFocus()
+ }
+ onInvalidSgfFile: Logic.showInfo(gameModel.getError())
+ }
+ PlayerModel {
+ id: playerModel
+
+ gameVariant: gameModel.gameVariant
+ onMoveGenerated: Logic.moveGenerated(move)
+ onSearchCallback: gameDisplay.searchCallback(elapsedSeconds, remainingSeconds)
+ onIsGenMoveRunningChanged:
+ if (isGenMoveRunning) gameDisplay.startSearch()
+ else gameDisplay.endSearch()
+ Component.onCompleted:
+ if (notEnoughMemory())
+ Logic.showFatal(qsTr("Not enough memory"))
+ }
+ AnalyzeGameModel {
+ id: analyzeGameModel
+
+ onIsRunningChanged: if (! isRunning) gameDisplay.endAnalysis()
+ }
+ RatingModel {
+ id: ratingModel
+
+ gameVariant: gameModel.gameVariant
+ }
+ AndroidUtils { id: androidUtils }
+ SyncSettings { id: syncSettings }
+ DialogLoader { id: aboutDialog; url: "AboutDialog.qml" }
+ DialogLoader { id: computerDialog; url: "ComputerDialog.qml" }
+ DialogLoader { id: fatalMessage; url: "FatalMessage.qml" }
+ DialogLoader { id: gameVariantDialog; url: "GameVariantDialog.qml" }
+ DialogLoader { id: gameInfoDialog; url: "GameInfoDialog.qml" }
+ DialogLoader { id: initialRatingDialog; url: "InitialRatingDialog.qml" }
+ DialogLoader { id: newFolderDialog; url: "NewFolderDialog.qml" }
+ DialogLoader { id: openDialog; url: "OpenDialog.qml" }
+ DialogLoader { id: exportImageDialog; url: "ExportImageDialog.qml" }
+ DialogLoader { id: imageSaveDialog; url: "ImageSaveDialog.qml" }
+ DialogLoader { id: asciiArtSaveDialog; url: "AsciiArtSaveDialog.qml" }
+ DialogLoader { id: gotoMoveDialog; url: "GotoMoveDialog.qml" }
+ DialogLoader { id: ratingDialog; url: "RatingDialog.qml" }
+ DialogLoader { id: saveDialog; url: "SaveDialog.qml" }
+ DialogLoader { id: infoMessage; url: "MessageDialog.qml" }
+ DialogLoader { id: questionMessage; url: "QuestionDialog.qml" }
+ DialogLoader { id: analyzeDialog; url: "AnalyzeDialog.qml" }
+ DialogLoader { id: appearanceDialog; url: "AppearanceDialog.qml" }
+ DialogLoader { id: moveAnnotationDialog; url: "MoveAnnotationDialog.qml" }
+ Loader { id: helpWindow }
+
+ // 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: 500
+ onTriggered: Logic.checkComputerMove()
+ }
+
+ // Delay lengthy blocking function calls such that 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
+ enabled: isAndroid
+ onStateChanged:
+ if (Qt.application.state === Qt.ApplicationSuspended)
+ Logic.autoSaveNoVerify()
+ }
+
+ Action {
+ id: actionBackToMainVar
+
+ shortcut: "Ctrl+M"
+ text: qsTr("Main Variation")
+ enabled: ! isRated && ! gameModel.isMainVar
+ onTriggered: Qt.callLater(function() { gameModel.backToMainVar() }) // QTBUG-69682
+ }
+ Action {
+ id: actionBackward
+
+ shortcut: "Ctrl+Left"
+ enabled: gameModel.canGoBackward && ! isRated
+ onTriggered: gameModel.goBackward()
+ }
+ Action {
+ id: actionBackward10
+
+ shortcut: "Ctrl+Shift+Left"
+ enabled: gameModel.canGoBackward && ! isRated
+ onTriggered: gameModel.goBackward10()
+ }
+ Action {
+ id: actionBeginning
+
+ shortcut: "Ctrl+Home"
+ enabled: gameModel.canGoBackward && ! isRated
+ onTriggered: gameModel.goBeginning()
+ }
+ Action {
+ id: actionForward
+
+ shortcut: "Ctrl+Right"
+ enabled: gameModel.canGoForward && ! isRated
+ onTriggered: gameModel.goForward()
+ }
+ Action {
+ id: actionForward10
+
+ shortcut: "Ctrl+Shift+Right"
+ enabled: gameModel.canGoForward && ! isRated
+ onTriggered: gameModel.goForward10()
+ }
+ Action {
+ id: actionEnd
+
+ shortcut: "Ctrl+End"
+ enabled: gameModel.canGoForward && ! isRated
+ onTriggered: gameModel.goEnd()
+ }
+ Action {
+ id: actionPrevVar
+
+ shortcut: "Ctrl+Up"
+ enabled: gameModel.hasPrevVar && ! isRated
+ onTriggered: gameModel.goPrevVar()
+ }
+ Action {
+ id: actionNextVar
+
+ shortcut: "Ctrl+Down"
+ enabled: gameModel.hasNextVar && ! isRated
+ onTriggered: gameModel.goNextVar()
+ }
+ Action {
+ id: actionBeginningOfBranch
+
+ shortcut: "Ctrl+B"
+ text: qsTr("Beginning of Branch")
+ enabled: ! isRated && gameModel.hasEarlierVar
+ onTriggered: Qt.callLater(function() { gameModel.gotoBeginningOfBranch() }) // QTBUG-69682
+ }
+ Action {
+ id: actionComment
+
+ shortcut: "Ctrl+T"
+ text: qsTr("Comment")
+ checkable: true
+ checked: gameDisplay.isCommentVisible
+ onTriggered:
+ if (isDesktop)
+ gameDisplay.setCommentVisible(checked)
+ else {
+ if (checked)
+ gameDisplay.showComment()
+ else
+ gameDisplay.showPieces()
+ }
+ }
+ Action {
+ id: actionComputerSettings
+
+ shortcut: "Ctrl+U"
+ //: Menu item Computer/Settings
+ text: qsTr("Settings")
+ onTriggered: computerDialog.open()
+ }
+ Action {
+ id: actionFindMove
+
+ shortcut: "Ctrl+H"
+ text: qsTr("Find Move")
+ enabled: ! gameModel.isGameOver
+ onTriggered: gameDisplay.showMove(gameModel.findMoveNext())
+ }
+ Action {
+ id: actionNextComment
+
+ shortcut: "Ctrl+E"
+ text: qsTr("Next Comment")
+ enabled: ! isRated && (gameModel.canGoForward || gameModel.canGoBackward)
+ onTriggered: Logic.findNextComment()
+ }
+ Action {
+ id: actionFullscreen
+
+ shortcut: "F11"
+ text: qsTr("Fullscreen")
+ checkable: true
+ checked: visibility === Window.FullScreen
+ onTriggered: {
+ if (visibility !== Window.FullScreen)
+ visibility = Window.FullScreen
+ else
+ visibility = Window.AutomaticVisibility
+ }
+ }
+ Action {
+ id: actionGameInfo
+
+ shortcut: "Ctrl+I"
+ text: qsTr("Game Info")
+ onTriggered: gameInfoDialog.open()
+ }
+ Action {
+ id: actionGotoMove
+
+ shortcut: "Ctrl+G"
+ text: qsTr("Move Number…")
+ enabled: ! isRated && (gameModel.moveNumber + gameModel.movesLeft >= 1)
+ onTriggered: gotoMoveDialog.open()
+ }
+ Action {
+ id: actionHelp
+
+ shortcut: "F1"
+ text: qsTr("Pentobi Help")
+ onTriggered: Logic.help()
+ }
+ Action {
+ id: actionNew
+
+ shortcut: "Ctrl+N"
+ text: qsTr("New")
+ enabled: gameDisplay.setupMode || gameModel.isModified
+ || gameModel.file !== "" || isRated
+ onTriggered: Qt.callLater(function() { Logic.newGame() }) // QTBUG-69682
+ }
+ Action {
+ id: actionNewRated
+
+ shortcut: "Ctrl+Shift+N"
+ text: qsTr("Rated Game")
+ enabled: ! isRated
+ onTriggered: Logic.ratedGame()
+ }
+ Action {
+ id: actionOpen
+
+ shortcut: "Ctrl+O"
+ text: qsTr("Open…")
+ onTriggered: Logic.open()
+ }
+ Action {
+ id: actionPlay
+
+ shortcut: "Ctrl+L"
+ text: qsTr("Play")
+ enabled: ! gameModel.isGameOver && ! isRated
+ onTriggered: Logic.computerPlay()
+ }
+ Action {
+ id: actionPlaySingle
+
+ shortcut: "Ctrl+Shift+L"
+ //: Play a single move
+ text: qsTr("Play Move")
+ enabled: ! gameModel.isGameOver && ! isRated
+ onTriggered: { isPlaySingleMoveRunning = true; Logic.genMove() }
+ }
+ Action {
+ id: actionQuit
+
+ shortcut: "Ctrl+Q"
+ text: qsTr("Quit")
+ onTriggered: close()
+ }
+ Action {
+ id: actionSave
+
+ shortcut: "Ctrl+S"
+ text: qsTr("Save")
+ enabled: gameModel.isModified
+ onTriggered: if (gameModel.file !== "") Logic.save(); else Logic.saveAs()
+ }
+ Action {
+ id: actionSaveAs
+
+ shortcut: "Ctrl+Shift+S"
+ text: qsTr("Save As…")
+ enabled: gameModel.isModified || gameModel.file !== ""
+ onTriggered: Logic.saveAs()
+ }
+ Action {
+ id: actionStop
+
+ text: qsTr("Stop")
+ enabled: (playerModel.isGenMoveRunning
+ || delayedCheckComputerMove.running
+ || analyzeGameModel.isRunning)
+ && ! isRated
+ onTriggered:
+ Qt.callLater(function() { Logic.cancelRunning(true) }) // QTBUG-69682
+ }
+ Action {
+ id: actionUndo
+
+ text: qsTr("Undo Move")
+ enabled: gameModel.canUndo && ! gameDisplay.setupMode && ! isRated
+ onTriggered: Qt.callLater(function() { Logic.undo() }) // QTBUG-69682
+ }
+ Instantiator {
+ model: [ "1", "2", "A", "C", "E", "F", "G", "H", "I", "J", "L",
+ "N", "O", "P", "S", "T", "U", "V", "W", "X", "Y", "Z" ]
+
+ Shortcut {
+ sequence: modelData
+ onActivated: Logic.pickNamedPiece(modelData)
+ }
+ }
+ Shortcut {
+ sequence: "Back"
+ enabled: isAndroid
+ onActivated: {
+ if (visibility === Window.FullScreen)
+ visibility = Window.AutomaticVisibility
+ else
+ close()
+ }
+ }
+ Shortcut {
+ sequence: "Return"
+ enabled: ! isAndroid
+ onActivated: gameDisplay.playPickedPiece()
+ }
+ Shortcut {
+ sequence: "Escape"
+ onActivated:
+ if (gameDisplay.pickedPiece)
+ gameDisplay.pickedPiece = null
+ else if (visibility === Window.FullScreen)
+ visibility = Window.AutomaticVisibility
+ }
+ Shortcut {
+ sequence: "Ctrl+Shift+H"
+ enabled: ! gameModel.isGameOver
+ onActivated: gameDisplay.showMove(gameModel.findMovePrevious())
+ }
+ Shortcut {
+ sequence: "Down"
+ onActivated: gameDisplay.shiftPiece(0, 1)
+ }
+ Shortcut {
+ sequence: "Shift+Down"
+ onActivated: gameDisplay.shiftPieceFast(0, 1)
+ }
+ Shortcut {
+ sequence: "Left"
+ onActivated: gameDisplay.shiftPiece(-1, 0)
+ }
+ Shortcut {
+ sequence: "Shift+Left"
+ onActivated: gameDisplay.shiftPieceFast(-1, 0)
+ }
+ Shortcut {
+ sequence: "Right"
+ onActivated: gameDisplay.shiftPiece(1, 0)
+ }
+ Shortcut {
+ sequence: "Shift+Right"
+ onActivated: gameDisplay.shiftPieceFast(1, 0)
+ }
+ Shortcut {
+ sequence: "Up"
+ onActivated: gameDisplay.shiftPiece(0, -1)
+ }
+ Shortcut {
+ sequence: "Shift+Up"
+ onActivated: gameDisplay.shiftPieceFast(0, -1)
+ }
+ Shortcut {
+ enabled: gameDisplay.pickedPiece
+ sequence: "Space"
+ onActivated: gameDisplay.pickedPiece.pieceModel.nextOrientation()
+ }
+ Shortcut {
+ sequence: "+"
+ onActivated: Logic.nextPiece()
+ }
+ Shortcut {
+ sequence: "Alt+M"
+ onActivated: toolBar.clickMenuButton()
+ }
+ Shortcut {
+ enabled: gameDisplay.pickedPiece
+ sequence: "Shift+Space"
+ onActivated: gameDisplay.pickedPiece.pieceModel.previousOrientation()
+ }
+ Shortcut {
+ sequence: "-"
+ onActivated: Logic.prevPiece()
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Menu.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import "Controls.js" as PentobiControls
+import "." as Pentobi
+
+Menu {
+ function addMnemonic(text, mnemonic) { return PentobiControls.addMnemonic(text, mnemonic) }
+
+ property bool dynamicWidth: isDesktop
+
+ width: {
+ if (! dynamicWidth)
+ return Math.min(font.pixelSize * 18, rootWindow.contentItem.width)
+ var maxWidth = 0
+ for (var i = 0; i < count; ++i)
+ maxWidth = Math.max(maxWidth, itemAt(i).implicitWidth)
+ return Math.min(maxWidth, rootWindow.contentItem.width)
+ }
+ cascade: isDesktop
+ closePolicy: isDesktop ?
+ Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+ : Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ delegate: Pentobi.MenuItem { }
+ background: Rectangle {
+ // Note that MenuItem in Qt 5.11 does neither fully use the system
+ // palette, nor make its actually used colors available in its own
+ // palette.
+ color: isDesktop ? palette.window : palette.base
+ border.color: palette.mid
+ }
+ // Workaround for QTBUG-69541 (Opened Menu highlights last used item on Android)
+ onOpened: if (isAndroid) currentIndex = -1
+ // Workaround for QTBUG-69540 (Menu highlights disabled item on click).
+ // Also part of workaround for QTBUG-70181, see Pentobi.MenuItem.Keys.onPressed
+ onCurrentIndexChanged: {
+ if (isAndroid || currentIndex < 0)
+ return
+ var i
+ for (i = currentIndex; i < count; ++i)
+ if (itemAt(i) instanceof MenuItem && itemAt(i).enabled) {
+ currentIndex = i
+ return
+ }
+ for (i = currentIndex - 1; i >= 0; --i)
+ if (itemAt(i) instanceof MenuItem && itemAt(i).enabled) {
+ currentIndex = i
+ return
+ }
+ currentIndex = -1
+ }
+ Component.onCompleted: {
+ // Sanity checks for mnemonics
+ if (! isDebug || ! isDesktop)
+ return
+ var allMnemonics = []
+ var i, j, text, pos, mnemonic, textWithoutMnemonic
+ for (i = 0; i < count; ++i) {
+ if (itemAt(i))
+ text = itemAt(i).text
+ else if (menuAt(i))
+ text = menuAt(i).title
+ if (! text)
+ continue
+ pos = text.indexOf("&")
+ if (pos < 0 || pos === text.length - 1) {
+ textWithoutMnemonic = text
+ continue
+ }
+ mnemonic = text.substr(pos + 1, 1).toLowerCase()
+ for (j = 0; j < allMnemonics.length; ++j)
+ if (allMnemonics[j] === mnemonic)
+ console.warn("Duplicate mnemonic:", text)
+ allMnemonics.push(mnemonic)
+ }
+ if (allMnemonics.length > 0 && textWithoutMnemonic)
+ console.warn("No mnemonic:", textWithoutMnemonic)
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuComputer.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Computer"),
+ //: Mnemonic for menu Computer. Leave empty for no mnemonic.
+ qsTr("C"))
+
+ Pentobi.MenuItem {
+ action: actionComputerSettings
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.
+ qsTr("S"))
+ }
+ Pentobi.MenuItem {
+ action: actionPlay
+ text: addMnemonic(actionPlay.text,
+ //: Mnemonic for menu item Play. Leave empty for no mnemonic.
+ qsTr("P"))
+ }
+ Pentobi.MenuItem {
+ action: actionPlaySingle
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Play Move. Leave empty for no mnemonic.
+ qsTr("M"))
+ }
+ Pentobi.MenuItem {
+ action: actionStop
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Stop. Leave empty for no mnemonic.
+ qsTr("O"))
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuEdit.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Edit"),
+ //: Mnemonic for menu Edit. Leave empty for no mnemonic.
+ qsTr("E"))
+
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Annotation…"),
+ //: Mnemonic for menu item Annotation. Leave empty for no mnemonic.
+ qsTr("A"))
+ enabled: gameModel.moveNumber > 0
+ onTriggered: {
+ var dialog = moveAnnotationDialog.get()
+ dialog.moveNumber = gameModel.moveNumber
+ moveAnnotationDialog.open()
+ }
+ }
+ Pentobi.MenuSeparator { }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Make Main Variation"),
+ //: Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.
+ qsTr("M"))
+ enabled: ! gameModel.isMainVar && ! isRated
+ onTriggered: {
+ gameModel.makeMainVar()
+ Logic.showTemporaryMessage(qsTr("Made main variation"))
+ }
+ }
+ Pentobi.MenuItem {
+ //: Short for Move Variation Up
+ text: addMnemonic(qsTr("Variation Up"),
+ //: Mnemonic for menu item Variation Up. Leave empty for no mnemonic.
+ qsTr("U"))
+ enabled: gameModel.hasPrevVar && ! isRated
+ onTriggered: Logic.moveUpVar()
+ }
+ Pentobi.MenuItem {
+ //: Short for Move Variation Down
+ text: addMnemonic(qsTr("Variation Down"),
+ //: Mnemonic for menu item Variation Down. Leave empty for no mnemonic.
+ qsTr("W"))
+ enabled: gameModel.hasNextVar && ! isRated
+ onTriggered: Logic.moveDownVar()
+ }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Delete Variations"),
+ //: Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.
+ qsTr("D"))
+ enabled: gameModel.hasVariations && ! isRated
+ onTriggered: Logic.deleteAllVar()
+ }
+ Pentobi.MenuSeparator { }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Truncate"),
+ //: Mnemonic for menu item Truncate. Leave empty for no mnemonic.
+ qsTr("T"))
+ enabled: gameModel.canGoBackward && ! isRated
+ onTriggered: Logic.truncate()
+ }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Truncate Children"),
+ //: Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.
+ qsTr("C"))
+ enabled: gameModel.canGoForward && ! isRated
+ onTriggered: Logic.truncateChildren()
+ }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Keep Position"),
+ //: Mnemonic for menu item Keep Position. Leave empty for no mnemonic.
+ qsTr("P"))
+ enabled: ! gameModel.isBoardEmpty && (gameModel.canGoBackward || gameModel.canGoForward) && ! isRated
+ onTriggered: Logic.keepOnlyPosition()
+ }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Keep Subtree"),
+ //: Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.
+ qsTr("S"))
+ enabled: gameModel.canGoBackward && gameModel.canGoForward && ! isRated
+ onTriggered: Logic.keepOnlySubtree()
+ }
+ Pentobi.MenuSeparator { }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Setup Mode"),
+ //: Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.
+ qsTr("O"))
+ checkable: true
+ enabled: ! gameModel.canGoBackward && ! gameModel.canGoForward
+ && gameModel.moveNumber === 0 && ! isRated
+ checked: gameDisplay.setupMode
+ onTriggered: {
+ checked = ! gameDisplay.setupMode // Workaround for QTBUG-69401
+ gameDisplay.setupMode = checked
+ if (checked)
+ gameDisplay.showPieces()
+ else
+ Logic.setComputerNone()
+ }
+ }
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Next Color"),
+ //: Mnemonic for menu item Next Color. Leave empty for no mnemonic.
+ qsTr("N"))
+ enabled: ! isRated
+ onTriggered: {
+ gameDisplay.pickedPiece = null
+ gameModel.nextColor()
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuExport.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Export"),
+ //: Mnemonic for menu Export. Leave empty for no mnemonic.
+ qsTr("E"))
+
+ Action {
+ text: addMnemonic(qsTr("Image…"),
+ //: Mnemonic for menu item Image. Leave empty for no mnemonic.
+ qsTr("M"))
+ onTriggered: exportImageDialog.open()
+ }
+ Action {
+ text: addMnemonic(qsTr("ASCII Art…"),
+ //: Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.
+ qsTr("A"))
+ onTriggered: {
+ var dialog = asciiArtSaveDialog.get()
+ dialog.name = gameModel.suggestFileName(folder, "txt")
+ dialog.selectNameFilter(0)
+ dialog.open()
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuGame.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Game"),
+ //: Mnemonic for menu Game. Leave empty for no mnemonic.
+ qsTr("G"))
+
+ Pentobi.MenuItem {
+ action: actionNew
+ text: addMnemonic(actionNew.text,
+ //: Mnemonic for menu item New. Leave empty for no mnemonic.
+ qsTr("N"))
+ }
+ Pentobi.MenuItem {
+ action: actionNewRated
+ text: addMnemonic(actionNewRated.text,
+ //: Mnemonic for menu item Rated Game. Leave empty for no mnemonic.
+ qsTr("R"))
+ }
+ Pentobi.MenuSeparator { }
+ Action {
+ text: addMnemonic(qsTr("Game Variant…"),
+ //: Mnemonic for menu item Game Variant. Leave empty for no mnemonic.
+ qsTr("V"))
+ onTriggered: gameVariantDialog.open()
+ }
+ Pentobi.MenuItem {
+ action: actionGameInfo
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Game Info. Leave empty for no mnemonic.
+ qsTr("I"))
+ }
+ Pentobi.MenuSeparator { }
+ Pentobi.MenuItem {
+ action: actionUndo
+ text: addMnemonic(actionUndo.text,
+ //: Mnemonic for menu item Undo. Leave empty for no mnemonic.
+ qsTr("U"))
+ }
+ Pentobi.MenuItem {
+ action: actionFindMove
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Find Move. Leave empty for no mnemonic.
+ qsTr("F"))
+ }
+ Pentobi.MenuSeparator { }
+ Pentobi.MenuItem {
+ action: actionOpen
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Open. Leave empty for no mnemonic.
+ qsTr("O"))
+ }
+ MenuRecentFiles { }
+ Action {
+ text: addMnemonic(qsTr("Open Clipboard"),
+ //: Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.
+ qsTr("C"))
+ onTriggered: Logic.openClipboard()
+ }
+ Pentobi.MenuItem {
+ action: actionSave
+ enabled: actionSave.enabled && gameModel.file !== ""
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Save. Leave empty for no mnemonic.
+ qsTr("S"))
+ }
+ Pentobi.MenuItem {
+ action: actionSaveAs
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Save As. Leave empty for no mnemonic.
+ qsTr("A"))
+ }
+ MenuExport { }
+ Pentobi.MenuSeparator { }
+ Pentobi.MenuItem {
+ action: actionQuit
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Quit. Leave empty for no mnemonic.
+ qsTr("Q"))
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuGo.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Go"),
+ //: Mnemonic for menu Go. Leave empty for no mnemonic.
+ qsTr("O"))
+
+ Pentobi.MenuItem {
+ action: actionGotoMove
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.
+ qsTr("N"))
+ }
+ Pentobi.MenuItem {
+ action: actionBackToMainVar
+ text: addMnemonic(actionBackToMainVar.text,
+ //: Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.
+ qsTr("M"))
+ }
+ Pentobi.MenuItem {
+ action: actionBeginningOfBranch
+ text: addMnemonic(actionBeginningOfBranch.text,
+ //: Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.
+ qsTr("B"))
+ }
+ Pentobi.MenuSeparator { }
+ Pentobi.MenuItem {
+ action: actionNextComment
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Next Comment. Leave empty for no mnemonic.
+ qsTr("C"))
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuHelp.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Help"),
+ //: Mnemonic for menu Help. Leave empty for no mnemonic.
+ qsTr("H"))
+
+ Pentobi.MenuItem {
+ action: actionHelp
+ text: addMnemonic(actionHelp.text,
+ //: Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.
+ qsTr("P"))
+ }
+ Action {
+ text: addMnemonic(qsTr("Report Bug"),
+ //: Mnemonic for menu item Report Bug. Leave empty for no mnemonic.
+ qsTr("B"))
+ onTriggered: Qt.openUrlExternally("https://sourceforge.net/p/pentobi/bugs/")
+ }
+ Action {
+ text: addMnemonic(qsTr("About Pentobi"),
+ //: Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.
+ qsTr("A"))
+ onTriggered: aboutDialog.open()
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuItem.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import QtQuick.Window 2.2
+import "Controls.js" as PentobiControls
+
+// Custom menu item that displays shortcuts (MenuItem in Qt 5.11 does not).
+MenuItem {
+ id: root
+
+ property string shortcut: action && action.shortcut ? action.shortcut : ""
+
+ function addMnemonic(text, mnemonic) {
+ return PentobiControls.addMnemonic(text, mnemonic)
+ }
+
+ property real _anyItemIndicatorWidth: {
+ if (menu)
+ for (var i = 0; i < menu.count; ++i)
+ if (menu.itemAt(i).checkable)
+ return menu.itemAt(i).indicator.width
+ return 0
+ }
+ property real _anyItemArrowWidth: {
+ if (menu)
+ for (var i = 0; i < menu.count; ++i)
+ if (menu.menuAt(i))
+ return menu.itemAt(i).arrow.width
+ return 0
+ }
+
+ // Qt 5.12.0 alpha doesn't set the width of menu items
+ width: menu.width
+ implicitHeight:
+ Math.round(font.pixelSize * (isDesktop ? 1.9 : 2.2)
+ * Screen.devicePixelRatio) / Screen.devicePixelRatio
+ // Explicitly set hoverEnabled to true, otherwise hover highlighting and
+ // submenu opening doesn't work in KDE on Ubuntu 18.10 (bug in Qt?)
+ hoverEnabled: true
+ Keys.onPressed:
+ // Workaround for QTBUG-70181 (disabled items take part in arrow key
+ // navigation). Only handle Up key, the Down key case is already
+ // handled in Pentobi.Menu.onCurrentIndexChanged
+ if (event.key === Qt.Key_Up && menu) {
+ for (var i = menu.currentIndex - 1; i >= 0; --i)
+ if (menu.itemAt(i) instanceof MenuItem && menu.itemAt(i).enabled) {
+ menu.currentIndex = i
+ break
+ }
+ event.accepted = true
+ }
+ background: Rectangle {
+ color: {
+ if (! root.highlighted)
+ return "transparent"
+ // Note that MenuItem in Qt 5.11 does neither fully use the system
+ // palette, nor make its actually used colors available in its own
+ // palette.
+ return isDesktop ? palette.highlight : palette.midlight
+ }
+ }
+ contentItem: RowLayout {
+ Label {
+ id: labelText
+
+ text: {
+ if (! isDesktop)
+ return root.text
+ var pos = root.text.indexOf("&")
+ if (pos < 0 || pos === root.text.length - 1)
+ return root.text
+ return root.text.substring(0, pos) + "<u>"
+ + root.text.substring(pos + 1, pos + 2)
+ + "</u>" + root.text.substring(pos + 2)
+ }
+ color: {
+ // See comment at background
+ if (root.highlighted)
+ return isDesktop ? palette.highlightedText : palette.buttonText
+ return palette.text
+ }
+ verticalAlignment: Text.AlignVCenter
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.leftMargin: 0.1 * font.pixelSize + _anyItemIndicatorWidth
+ + 0.2 * font.pixelSize
+ Layout.rightMargin: 0.4 * font.pixelSize
+ }
+ Label {
+ visible: isDesktop && shortcut !== ""
+ text: {
+ var text = shortcut
+ //: Shortcut modifier key as displayed in menu item text (abbreviate if long)
+ text = text.replace("Ctrl", qsTr("Ctrl"))
+ //: Shortcut modifier key as displayed in menu item text (abbreviate if long)
+ return text.replace("Shift", qsTr("Shift"))
+ }
+ color: labelText.color
+ opacity: 0.6
+ verticalAlignment: Text.AlignVCenter
+ Layout.fillHeight: true
+ Layout.rightMargin: _anyItemArrowWidth > 0 ? _anyItemArrowWidth
+ : 0.1 * font.pixelSize
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuRecentFiles.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.4
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Open Recent"),
+ //: Mnemonic for menu Open Recent. Leave empty for no mnemonic.
+ qsTr("P"))
+ enabled: gameModel.recentFiles.length > 0
+
+ function getText(recentFiles, index) {
+ if (index >= recentFiles.length)
+ return ""
+ var text = recentFiles[index]
+ text = text.substring(text.lastIndexOf("/") + 1)
+ if (isDesktop)
+ //: Format in recent files menu. First argument is the
+ //: file number, second argument the file name.
+ text = addMnemonic(qsTr("%1. %2").arg(index + 1).arg(text),
+ (index + 1).toString())
+ return text
+ }
+
+ // Instantiator in Menu doesn't work reliably with Qt 5.11 or 5.12.0 alpha
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 0
+ // Invisible menu item still use space in Qt 5.11
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 0)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[0])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 1
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 1)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[1])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 2
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 2)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[2])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 3
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 3)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[3])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 4
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 4)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[4])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 5
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 5)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[5])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 6
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 6)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[6])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 7
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 7)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[7])
+ }
+ Pentobi.MenuItem {
+ visible: gameModel.recentFiles.length > 8
+ height: visible ? implicitHeight : 0
+ text: getText(gameModel.recentFiles, 8)
+ onTriggered: Logic.openRecentFile(gameModel.recentFiles[8])
+ }
+
+ Pentobi.MenuSeparator { }
+ Action {
+ //: Menu item for clearing the recent files list
+ text: addMnemonic(qsTr("Clear List"),
+ //: Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.
+ qsTr("C"))
+ onTriggered: Qt.callLater(function() { // QTBUG-69682
+ gameModel.clearRecentFiles()
+ })
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuSeparator.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+
+MenuSeparator {
+ // Qt 5.12.0 alpha doesn't set the width of menu items
+ width: parent.width
+ // Default implicitWidth is too large. Since we either use fixed-width
+ // menus or compute the menu width from the maximum implicitWidth of the
+ // items, we set implicitWidth of the separators to 0
+ implicitWidth: 0
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuTools.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("Tools"),
+ //: Mnemonic for menu Tools. Leave empty for no mnemonic.
+ qsTr("T"))
+
+ Pentobi.MenuItem {
+ text: addMnemonic(qsTr("Rating"),
+ //: Mnemonic for menu item Rating. Leave empty for no mnemonic.
+ qsTr("R"))
+ onTriggered: Logic.rating()
+ }
+ Action {
+ enabled: ! isRated && ratingModel.numberGames > 0
+ text: addMnemonic(qsTr("Clear Rating"),
+ //: Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.
+ qsTr("C"))
+ onTriggered: Logic.clearRating()
+ }
+ Pentobi.MenuSeparator { }
+ Action {
+ enabled: ! isRated && (gameModel.canGoBackward || gameModel.canGoForward)
+ // Text needs to end with ellipsis on desktop because it opens a
+ // dialog asking for analysis speed, but not on Android
+ text: addMnemonic(isAndroid ? qsTr("Analyze Game") : qsTr("Analyze Game…"),
+ //: Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.
+ qsTr("A"))
+ onTriggered: {
+ if (isAndroid)
+ Logic.analyzeGame(3000)
+ else
+ analyzeDialog.open()
+ }
+ }
+ Action {
+ enabled: analyzeGameModel.elements.length !== 0
+ text: addMnemonic(qsTr("Clear Analysis"),
+ //: Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.
+ qsTr("E"))
+ onTriggered:
+ Qt.callLater(function() { // QTBUG-69682
+ analyzeGameModel.clear()
+ gameDisplay.deleteAnalysis()
+ })
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MenuView.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick.Controls 2.3
+import QtQuick.Window 2.1
+import "." as Pentobi
+
+Pentobi.Menu {
+ title: addMnemonic(qsTr("View"),
+ //: Mnemonic for menu View. Leave empty for no mnemonic.
+ qsTr("V"))
+
+ Action {
+ text: addMnemonic(qsTr("Appearance"),
+ //: Mnemonic for menu Appearance. Leave empty for no mnemonic.
+ qsTr("A"))
+ onTriggered: appearanceDialog.open()
+ }
+ Pentobi.MenuItem {
+ visible: isDesktop
+ // Invisible menu item still use space in Qt 5.11
+ height: visible ? implicitHeight : 0
+ text: addMnemonic(qsTr("Toolbar"),
+ //: Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.
+ qsTr("T"))
+ checkable: true
+ checked: rootWindow.showToolBar
+ onTriggered: rootWindow.showToolBar = checked
+ }
+ Pentobi.MenuItem {
+ action: actionComment
+ text: addMnemonic(actionComment.text,
+ //: Mnemonic for menu item View/Comment. Leave empty for no mnemonic.
+ qsTr("C"))
+ }
+ Pentobi.MenuItem {
+ action: actionFullscreen
+ text: addMnemonic(action.text,
+ //: Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.
+ qsTr("F"))
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MessageDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ property alias text: label.text
+
+ footer: Pentobi.DialogButtonBox { ButtonOk { } }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(label.implicitWidth,
+ font.pixelSize * 25, maxContentWidth),
+ font.pixelSize * 15, minContentWidth)
+ implicitHeight: label.implicitHeight
+
+ Label {
+ id: label
+
+ anchors.fill: parent
+ wrapMode: Text.Wrap
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/MoveAnnotationDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ property int moveNumber
+
+ footer: DialogButtonBoxOkCancel { }
+ onOpened: {
+ var annotation = gameModel.getMoveAnnotation(moveNumber)
+ if (annotation === "")
+ comboBox.currentIndex = 0
+ else if (annotation === "‼")
+ comboBox.currentIndex = 1
+ else if (annotation === "!")
+ comboBox.currentIndex = 2
+ else if (annotation === "⁉")
+ comboBox.currentIndex = 3
+ else if (annotation === "⁈")
+ comboBox.currentIndex = 4
+ else if (annotation === "?")
+ comboBox.currentIndex = 5
+ else if (annotation === "⁇")
+ comboBox.currentIndex = 6
+ }
+ onAccepted: {
+ var annotation
+ switch (comboBox.currentIndex) {
+ case 0: annotation = ""; break
+ case 1: annotation = "‼"; break
+ case 2: annotation = "!"; break
+ case 3: annotation = "⁉"; break
+ case 4: annotation = "⁈"; break
+ case 5: annotation = "?"; break
+ case 6: annotation = "⁇"; break
+ }
+ gameModel.setMoveAnnotation(moveNumber, annotation)
+ }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(columnLayout.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: columnLayout.implicitHeight
+
+ ColumnLayout {
+ id: columnLayout
+
+ anchors.fill: parent
+
+ Label { text: qsTr("Move %1").arg(moveNumber) }
+ ComboBox {
+ id: comboBox
+
+ model: [
+ qsTr("No annotation"),
+ qsTr("Very good"),
+ qsTr("Good"),
+ qsTr("Interesting"),
+ qsTr("Doubtful"),
+ qsTr("Bad"),
+ qsTr("Very Bad")
+ ]
+ Layout.preferredWidth: 15 * font.pixelSize
+ Layout.fillWidth: true
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/NavigationButtons.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+
+RowLayout
+{
+ spacing: 0
+
+ Pentobi.Button {
+ id: buttonBeginning
+
+ icon.source: theme.getImage("pentobi-beginning")
+ action: actionBeginning
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ Pentobi.Button {
+ icon.source: theme.getImage("pentobi-backward")
+ action: actionBackward
+ autoRepeat: true
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ Pentobi.Button {
+ icon.source: theme.getImage("pentobi-forward")
+ action: actionForward
+ autoRepeat: true
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ Pentobi.Button {
+ icon.source: theme.getImage("pentobi-end")
+ action: actionEnd
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ Pentobi.Button {
+ icon.source: theme.getImage("pentobi-previous-variation")
+ action: actionPrevVar
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ Pentobi.Button {
+ icon.source: theme.getImage("pentobi-next-variation")
+ action: actionNextVar
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/NavigationPanel.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+
+ColumnLayout {
+ id: root
+
+ function dropCommentFocus() { comment.dropFocus() }
+
+ Comment {
+ id: comment
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ Label {
+ text: gameModel.positionInfo
+ color: theme.colorText
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ }
+ NavigationButtons {
+ Layout.fillWidth: true
+ Layout.maximumHeight:
+ Math.min(50, 0.08 * rootWindow.contentItem.height, root.width / 6)
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/NewFolderDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+import QtQuick.Controls 2.2
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+ id: root
+
+ property url folder
+ property alias name: textField.text
+
+ function returnPressed() {
+ if (! hasButtonFocus())
+ checkAccept()
+ }
+ function checkAccept() {
+ if (! isValidName(name))
+ return
+ if (! gameModel.createFolder(folder + "/" + name)) {
+ Logic.showInfo(gameModel.getError())
+ return
+ }
+ accept()
+ }
+
+ function isValidName(name) { return name.trim().length > 0 }
+
+ footer: Pentobi.DialogButtonBox {
+ ButtonOk {
+ enabled: isValidName(name)
+ onClicked: checkAccept()
+ DialogButtonBox.buttonRole: DialogButtonBox.InvalidRole
+ }
+ ButtonCancel { }
+ }
+ onOpened: {
+ name = gameModel.suggestNewFolderName(folder)
+ textField.selectAll()
+ }
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(rowLayout.implicitWidth, maxContentWidth),
+ minContentWidth)
+ implicitHeight: rowLayout.implicitHeight
+
+ ColumnLayout {
+ id: rowLayout
+
+ anchors.fill: parent
+
+ Label { text: qsTr("Folder name:") }
+ TextField {
+ id: textField
+
+ focus: true
+ selectByMouse: true
+ onAccepted: checkAccept()
+ Layout.fillWidth: true
+ }
+ Item { Layout.fillWidth: true }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/OpenDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+ title: qsTr("Open")
+ nameFilterLabels: [ qsTr("Blokus games") ]
+ nameFilters: [ [ "*.blksgf", "*.BLKSGF" ] ]
+ folder: rootWindow.folder
+ onOpened: name = ""
+ onAccepted: {
+ rootWindow.folder = folder
+ Logic.openFileUrl()
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceCallisto.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+import QtQuick.Window 2.2
+
+// See PieceClassic.qml for comments
+Item
+{
+ id: root
+
+ property QtObject pieceModel
+ property var color:
+ switch (pieceModel.color) {
+ case 0: return color0
+ case 1: return color1
+ case 2: return color2
+ case 3: return color3
+ }
+ property Item parentUnplayed
+ property string imageName:
+ "image://pentobi/" +
+ (pieceModel.elements.length === 1 ? "frame" : "square") +
+ "/" + color[0] + "/" + color[1] + "/" + color[2]
+ // Avoid fractional sizes for square piece elements
+ property real scaleUnplayed:
+ parentUnplayed ?
+ Math.floor(0.25 * parentUnplayed.width) / board.gridWidth : 0
+ // We only use flipX.angle [0..360]
+ property bool flippedX: Math.abs(flipX.angle - 180) < 90
+ // We only use flipY.angle [0..180]
+ property bool flippedY: flipY.angle > 90
+ property real pieceAngle: {
+ if (! flippedY && ! flippedX) return rotation
+ if (! flippedY && flippedX) return rotation + 90
+ if (flippedX) return rotation + 180
+ return rotation + 270
+ }
+ property real isSmall: scale < 0.5 ? 1 : 0
+ property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+ property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+ property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+ property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+ property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+ property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+ property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ }
+ ]
+
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (pieceAngle - imgAngle + 360) % 360
+ return angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)
+ }
+
+ Repeater {
+ model: pieceModel.elements
+
+ Item {
+ // Right junction
+ Rectangle {
+ visible: pieceModel.junctionType[index] === 0
+ || pieceModel.junctionType[index] === 1
+ color: root.color[0]
+ width: board.gridWidth - square.width
+ height: 0.8 * board.gridHeight
+ x: (modelData.x - pieceModel.center.x + 1) * board.gridWidth
+ - width / 2
+ y: (modelData.y - pieceModel.center.y) * board.gridHeight
+ + (board.gridHeight - height) / 2
+ antialiasing: true
+ }
+ // Down junction
+ Rectangle {
+ visible: pieceModel.junctionType[index] === 0
+ || pieceModel.junctionType[index] === 2
+ color: root.color[0]
+ width: 0.8 * board.gridWidth
+ height: board.gridHeight - square.height
+ x: (modelData.x - pieceModel.center.x) * board.gridWidth
+ + (board.gridWidth - width) / 2
+ y: (modelData.y - pieceModel.center.y + 1) * board.gridHeight
+ - height / 2
+ antialiasing: true
+ }
+ Square {
+ id: square
+
+ // Avoid fractional piece element size
+ width:
+ Math.round(0.9 * board.gridWidth * Screen.devicePixelRatio)
+ / Screen.devicePixelRatio
+ height:
+ Math.round(0.9 * board.gridHeight * Screen.devicePixelRatio)
+ / Screen.devicePixelRatio
+ x: (modelData.x - pieceModel.center.x) * board.gridWidth
+ + (board.gridWidth - width) / 2
+ y: (modelData.y - pieceModel.center.y) * board.gridHeight
+ + (board.gridHeight - height) / 2
+ }
+ }
+ }
+ Rectangle {
+ opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+ color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+ border { width: 0.2 * width; color: root.color[3] }
+ width: 0.3 * board.gridHeight
+ height: width
+ radius: width / 2
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+ }
+ Loader {
+ sourceComponent: moveMarking === "all_number"
+ || moveMarking === "last_number" || item ?
+ textComponent : null
+
+ Component {
+ id: textComponent
+
+ Text {
+ text: moveMarking == "all_number"
+ || (moveMarking == "last_number"
+ && pieceModel.isLastMove) ?
+ pieceModel.moveLabel : ""
+ opacity: text === "" ? 0 : 1
+ color: root.color[3]
+ width: board.gridWidth
+ height: board.gridHeight
+ fontSizeMode: Text.Fit
+ font {
+ pixelSize: 0.5 * board.gridHeight
+ preferShaping: false
+ }
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ minimumPixelSize: 5
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ transform: [
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 0; y: 1; z: 0 }
+ angle: flippedY ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 1; y: 0; z: 0 }
+ angle: flippedX ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ angle: -root.rotation
+ }
+ ]
+ Behavior on opacity {
+ NumberAnimation { duration: animationDurationFast }
+ }
+ }
+ }
+ }
+ 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: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot90,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot270,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ PieceFlipAnimation { target: flipX }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: root === pickedPiece
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board.grabImageTarget
+ x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+ y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ scale: scaleUnplayed
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 1
+ }
+ ParentAnimation {
+ via: isDesktop ? null : gameDisplay
+
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: animationDurationMove
+ easing.type: Easing.InOutSine
+ }
+ }
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 0
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceClassic.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item
+{
+ id: root
+
+ property QtObject pieceModel
+ property var color:
+ switch (pieceModel.color) {
+ case 0: return color0
+ case 1: return color1
+ case 2: return color2
+ case 3: return color3
+ }
+ property Item parentUnplayed
+ property string imageName:
+ "image://pentobi/square/" + color[0] + "/" + color[1] + "/" + color[2]
+ // Avoid fractional sizes for square piece elements
+ property real scaleUnplayed:
+ parentUnplayed ?
+ Math.floor(0.19 * parentUnplayed.width) / board.gridWidth : 0
+ property bool flippedX: Math.abs(flipX.angle - 180) < 90
+ property bool flippedY: flipY.angle > 90
+ property real pieceAngle: {
+ if (! flippedY && ! flippedX) return rotation
+ if (! flippedY && flippedX) return rotation + 90
+ if (flippedX) return rotation + 180
+ return rotation + 270
+ }
+ property real isSmall: scale < 0.5 ? 1 : 0
+ property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+ property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+ property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+ property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+ property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+ property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+ property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ }
+ ]
+
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (pieceAngle - imgAngle + 360) % 360
+ return angle >= 90 && angle <= 270 ? 0 : Math.cos(angle * Math.PI / 180)
+ }
+
+ Repeater {
+ model: pieceModel.elements
+
+ Square {
+ x: (modelData.x - pieceModel.center.x) * board.gridWidth
+ y: (modelData.y - pieceModel.center.y) * board.gridHeight
+ }
+ }
+ Rectangle {
+ opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+ color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+ border { width: 0.2 * width; color: root.color[3] }
+ width: 0.3 * board.gridHeight
+ height: width
+ radius: width / 2
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+ }
+ Loader {
+ sourceComponent: moveMarking === "all_number"
+ || moveMarking === "last_number" || item ?
+ textComponent : null
+
+ Component {
+ id: textComponent
+
+ Text {
+ text: moveMarking == "all_number"
+ || (moveMarking == "last_number"
+ && pieceModel.isLastMove) ?
+ pieceModel.moveLabel : ""
+ opacity: text === "" ? 0 : 1
+ color: root.color[3]
+ width: board.gridWidth
+ height: board.gridHeight
+ fontSizeMode: Text.Fit
+ font {
+ pixelSize: 0.5 * board.gridHeight
+ preferShaping: false
+ }
+ minimumPixelSize: 5
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ transform: [
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 0; y: 1; z: 0 }
+ angle: flippedY ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 1; y: 0; z: 0 }
+ angle: flippedX ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ angle: -root.rotation
+ }
+ ]
+ Behavior on opacity {
+ NumberAnimation { duration: animationDurationFast }
+ }
+ }
+ }
+ }
+ 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: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot90,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot270,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ PieceFlipAnimation { target: flipX }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: root === pickedPiece
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board.grabImageTarget
+ x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+ y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ scale: scaleUnplayed
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ // Avoid piece being overlapped by pieces of different color
+ // during ParentAnimation
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 1
+ }
+ ParentAnimation {
+ via: isDesktop ? null : gameDisplay
+
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: animationDurationMove
+ easing.type: Easing.InOutSine
+ }
+ }
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 0
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceFlipAnimation.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+RotationAnimation {
+ duration: animationDurationMove
+ direction: RotationAnimation.Shortest
+ property: "angle"
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceGembloQ.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece for GembloQ. See PieceClassic for comments.
+Item
+{
+ id: root
+
+ property QtObject pieceModel
+ property var color:
+ switch (pieceModel.color) {
+ case 0: return color0
+ case 1: return color1
+ case 2: return color2
+ case 3: return color3
+ }
+ property Item parentUnplayed
+ property string imageName:
+ "image://pentobi/quarter-square/" + color[0] + "/" + color[1] + "/" +
+ color[2]
+ // Avoid fractional sizes for square piece elements
+ property real scaleUnplayed:
+ parentUnplayed ? Math.floor(0.08 * 2 * parentUnplayed.width)
+ / (2 * board.gridWidth) : 0
+ property string imageNameBottom:
+ "image://pentobi/quarter-square-bottom/" + color[0] + "/" + color[1] +
+ "/" + color[2]
+ property bool flippedX: Math.abs(flipX.angle - 180) < 90
+ property real pieceAngle: flippedX ? rotation + 180 : rotation
+ property real isSmall: scale < 0.5 ? 1 : 0
+ property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+ property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+ property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+ property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+ property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+ property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+ property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (pieceAngle - imgAngle + 360) % 360
+ if (angle <= 90) return 0
+ if (angle <= 180) return -Math.cos(angle * Math.PI / 180)
+ if (angle <= 270) return 1
+ return -Math.sin(angle * Math.PI / 180)
+ }
+
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ }
+ ]
+
+ Repeater {
+ model: pieceModel.elements
+
+ QuarterSquare {
+ x: (modelData.x - pieceModel.center.x) * board.gridWidth
+ y: (modelData.y - pieceModel.center.y) * board.gridHeight
+ pointType: {
+ var t = modelData.x
+ if (modelData.y % 2 != 0) t += 2
+ return (t % 4 + 4) % 4
+ }
+ }
+ }
+ Rectangle {
+ opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+ color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+ border { width: 0.2 * width; color: root.color[3] }
+ width: 0.45 * board.gridHeight
+ height: width
+ radius: width / 2
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+ }
+ Loader {
+ sourceComponent: moveMarking === "all_number"
+ || moveMarking === "last_number" || item ?
+ textComponent : null
+
+ Component {
+ id: textComponent
+
+ Text {
+ property bool flippedY: Math.abs(flipY.angle - 180) < 90
+
+ text: moveMarking == "all_number"
+ || (moveMarking == "last_number"
+ && pieceModel.isLastMove) ?
+ pieceModel.moveLabel : ""
+ opacity: text === "" ? 0 : 1
+ color: root.color[3]
+ width: 2 * board.gridWidth
+ height: 2 * board.gridHeight
+ fontSizeMode: Text.Fit
+ font {
+ pixelSize: 0.7 * board.gridHeight
+ preferShaping: false
+ }
+ minimumPixelSize: 5
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ transform: [
+ Rotation {
+ origin { x: board.gridWidth; y: board.gridHeight }
+ axis { x: 0; y: 1; z: 0 }
+ angle: flippedY ? -180 : 0
+ },
+ Rotation {
+ origin { x: board.gridWidth; y: board.gridHeight }
+ axis { x: 1; y: 0; z: 0 }
+ angle: flippedX ? -180 : 0
+ },
+ Rotation {
+ origin { x: board.gridWidth; y: board.gridHeight }
+ angle: -root.rotation
+ }
+ ]
+ Behavior on opacity {
+ NumberAnimation { duration: animationDurationFast }
+ }
+ }
+ }
+ }
+ 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: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot90,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot270,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ PieceFlipAnimation { target: flipX }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: root === pickedPiece
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board.grabImageTarget
+ x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+ y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ scale: scaleUnplayed
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 1
+ }
+ ParentAnimation {
+ via: isDesktop ? null : gameDisplay
+
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: animationDurationMove
+ easing.type: Easing.InOutSine
+ }
+ }
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 0
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceList.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Grid {
+ id: root
+
+ property var pieces
+
+ signal piecePicked(var piece)
+
+ // Show unplayed pieces in slightly less bright colors, such that they
+ // don't distract from the pieces on board, but not in desktop mode, where
+ // the unplayed pieces are relatively small, or if the background is bright
+ // to avoid bad contrast with yellow pieces.
+ opacity:
+ isDesktop ? 1 :
+ Math.min(0.9 + 0.1 * theme.colorBackground.hslLightness, 1)
+
+ Repeater {
+ model: pieces
+
+ MouseArea {
+ id: mouseArea
+
+ width: root.width / columns; height: width
+ visible: ! modelData.pieceModel.isPlayed
+ onClicked: {
+ gameDisplay.dropCommentFocus()
+ piecePicked(modelData)
+ }
+ Component.onCompleted: modelData.parentUnplayed = mouseArea
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceManipulator.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+ id: root
+
+ property QtObject pieceModel
+ // True if piece manipulator is at a board location that is a legal move
+ property bool legal
+
+ // Fast move animation to make sure that the next grid cell is reached
+ // before the next auto-repeat keyboard command
+ property bool fastMove: false
+
+ // Manipulator buttons are smaller on desktop with mouse usage
+ property real buttonSize: (isDesktop ? 0.12 : 0.17) * root.width
+
+ property real animationDuration:
+ ! pieceModel || ! gameDisplay.enableAnimations ?
+ 0 : fastMove ? 50 : animationDurationMove
+
+ signal piecePlayed
+
+ enabled: pieceModel
+
+ Image {
+ anchors.fill: root
+ source: isDesktop ? theme.getImage("piece-manipulator-desktop")
+ : theme.getImage("piece-manipulator")
+ sourceSize { width: width; height: height }
+ opacity: pieceModel && ! legal ? 0.7 : 0
+
+ Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+ }
+ Image {
+ anchors.fill: root
+ source: isDesktop ? theme.getImage("piece-manipulator-desktop-legal")
+ : theme.getImage("piece-manipulator-legal")
+ sourceSize { width: width; height: height }
+ opacity: pieceModel && legal ? 0.55 : 0
+
+ Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+ }
+ MouseArea {
+ id: dragArea
+
+ anchors.centerIn: root
+ // Make drag area a bit larger than image to avoid accidental
+ // flicking in PieceSelectorMobile when wanting to drag
+ width: root.width + buttonSize; height: width
+ drag {
+ target: root
+ filterChildren: true
+ minimumX: -root.width / 2
+ maximumX: root.parent.width - root.width / 2
+ minimumY: -root.height / 2
+ maximumY: root.parent.height - root.height / 2
+ }
+ // Consume mouse hover events in case it is over toolbar
+ hoverEnabled: isDesktop
+
+ MouseArea {
+ anchors.centerIn: dragArea
+ width: 0.9 * (root.width - 2 * buttonSize); height: width
+ onClicked: piecePlayed()
+ }
+ MouseArea {
+ anchors {
+ top: dragArea.top
+ margins: (dragArea.width - root.width) / 2
+ horizontalCenter: dragArea.horizontalCenter
+ }
+ width: buttonSize; height: width
+ onClicked: pieceModel.rotateRight()
+ }
+ MouseArea {
+ anchors {
+ right: dragArea.right
+ margins: (dragArea.width - root.width) / 2
+ verticalCenter: dragArea.verticalCenter
+ }
+ width: buttonSize; height: width
+ onClicked: pieceModel.flipAcrossX()
+ }
+ MouseArea {
+ anchors {
+ bottom: dragArea.bottom
+ margins: (dragArea.width - root.width) / 2
+ horizontalCenter: dragArea.horizontalCenter
+ }
+ width: buttonSize; height: width
+ onClicked: pieceModel.flipAcrossY()
+ }
+ MouseArea {
+ anchors {
+ left: dragArea.left
+ margins: (dragArea.width - root.width) / 2
+ verticalCenter: dragArea.verticalCenter
+ }
+ width: buttonSize; height: width
+ onClicked: pieceModel.rotateLeft()
+ }
+ }
+
+ Behavior on x {
+ NumberAnimation {
+ duration: animationDuration
+ easing.type: Easing.InOutSine
+ }
+ }
+ Behavior on y {
+ NumberAnimation {
+ duration: animationDuration
+ easing.type: Easing.InOutSine
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceNexos.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece for Nexos. See PieceClassic for comments.
+Item
+{
+ id: root
+
+ property QtObject pieceModel
+ property var color:
+ switch (pieceModel.color) {
+ case 0: return color0
+ case 1: return color1
+ case 2: return color2
+ case 3: return color3
+ }
+ property Item parentUnplayed
+ property string imageName:
+ "image://pentobi/square/" + color[0] + "/" + color[1] + "/" + color[2]
+ // Avoid fractional sizes for square piece elements
+ property real scaleUnplayed:
+ parentUnplayed ?
+ Math.floor(0.12 * parentUnplayed.width) / board.gridWidth : 0
+ property bool flippedX: Math.abs(flipX.angle - 180) < 90
+ property bool flippedY: flipY.angle > 90
+ property real pieceAngle: {
+ if (! flippedY && ! flippedX) return rotation
+ if (! flippedY && flippedX) return rotation + 90
+ if (flippedX) return rotation + 180
+ return rotation + 270
+ }
+ property real isSmall: scale < 0.5 ? 1 : 0
+ property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+ property real imageOpacity90: imageOpacity(pieceAngle, 90) * (1 - isSmall)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+ property real imageOpacity270: imageOpacity(pieceAngle, 270) * (1 - isSmall)
+ property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+ property real imageOpacitySmall90: imageOpacity(pieceAngle, 90) * isSmall
+ property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+ property real imageOpacitySmall270: imageOpacity(pieceAngle, 270) * isSmall
+
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ }
+ ]
+
+ function isHorizontal(pos) { return pos.x % 2 != 0 }
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (pieceAngle - imgAngle + 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 * board.gridWidth
+ height: 0.5 * board.gridHeight
+ x: (modelData.x - pieceModel.center.x - 0.25) * board.gridWidth
+ y: (modelData.y - pieceModel.center.y + 0.25) * board.gridHeight
+ }
+ }
+ Repeater {
+ model: pieceModel.junctions
+
+ Image {
+ source: {
+ switch (pieceModel.junctionType[index]) {
+ case 0:
+ return "image://pentobi/junction-all/" + color[0]
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ return "image://pentobi/junction-t/" + color[0]
+ case 5:
+ case 6:
+ return "image://pentobi/junction-straight/" + color[0]
+ default:
+ return "image://pentobi/junction-right/" + color[0]
+ }
+ }
+ rotation: {
+ switch (pieceModel.junctionType[index]) {
+ case 1:
+ case 9:
+ return 270
+ case 2:
+ case 6:
+ case 8:
+ return 90
+ case 4:
+ case 7:
+ return 180
+ default:
+ return 0
+ }
+ }
+ width: 0.5 * board.gridWidth
+ height: 0.5 * board.gridHeight
+ x: (modelData.x - pieceModel.center.x + 0.25) * board.gridWidth
+ y: (modelData.y - pieceModel.center.y + 0.25) * board.gridHeight
+ sourceSize {
+ width: imageSourceSize.width / 3
+ height: imageSourceSize.height
+ }
+ antialiasing: true
+ }
+ }
+ Rectangle {
+ opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+ color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+ border { width: 0.2 * width; color: root.color[3] }
+ width: 0.3 * board.gridHeight
+ height: width
+ radius: width / 2
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+ }
+ Loader {
+ sourceComponent: moveMarking === "all_number"
+ || moveMarking === "last_number" || item ?
+ textComponent : null
+
+ Component {
+ id: textComponent
+
+ Text {
+ text: moveMarking == "all_number"
+ || (moveMarking == "last_number"
+ && pieceModel.isLastMove) ?
+ pieceModel.moveLabel : ""
+ opacity: text === "" ? 0 : 1
+ color: root.color[3]
+ width: board.gridWidth
+ height: board.gridHeight
+ fontSizeMode: Text.Fit
+ font {
+ pixelSize: 0.5 * board.gridHeight
+ preferShaping: false
+ }
+ minimumPixelSize: Math.max(3, 0.3 * board.gridHeight)
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ transform: [
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 0; y: 1; z: 0 }
+ angle: flippedY ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 1; y: 0; z: 0 }
+ angle: flippedX ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ angle: -root.rotation
+ }
+ ]
+ Behavior on opacity {
+ NumberAnimation { duration: animationDurationFast }
+ }
+ }
+ }
+ }
+ 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: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot90,rot270Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot270,rot90Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ PieceFlipAnimation { target: flipX }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: root === pickedPiece
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board.grabImageTarget
+ x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+ y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ scale: scaleUnplayed
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 1
+ }
+ ParentAnimation {
+ via: isDesktop ? null : gameDisplay
+
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: animationDurationMove
+ easing.type: Easing.InOutSine
+ }
+ }
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 0
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceRotationAnimation.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+RotationAnimation {
+ duration: animationDurationMove
+ direction: RotationAnimation.Shortest
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceSelectorDesktop.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+ id: root
+
+ property alias pieces0: pieceList0.pieces
+ property alias pieces1: pieceList1.pieces
+ property alias pieces2: pieceList2.pieces
+ property alias pieces3: pieceList3.pieces
+ property int columns: pieces0 ? Math.ceil(pieces0.length / 2) : 11
+ property alias transitionsEnabled: transition.enabled
+
+ signal piecePicked(var piece)
+
+ property real toPlayIndicatorWidth:
+ Math.max(Math.min(parent.width / columns, parent.height / 8.3) / 10, 2)
+
+ Row {
+ // Set size sich that width/height ration fits the number of columns,
+ // taking toPlayIndicator and column spacing into account
+ width: Math.min(parent.width - toPlayIndicatorWidth,
+ parent.height / 8.3 * columns)
+ height:
+ Math.min(parent.height,
+ (parent.width - toPlayIndicatorWidth) / columns * 8.3)
+ anchors.centerIn: parent
+
+ Rectangle {
+ id: toPlayIndicator
+
+ opacity: gameModel.isGameOver ? 0 : 0.3
+ x: 0
+ width: toPlayIndicatorWidth
+ radius: width / 2
+ color: theme.colorText
+ }
+ Column {
+ id: column
+
+ width: parent.width - toPlayIndicatorWidth
+ spacing: parent.height / 8.3 * 0.1
+
+ PieceList {
+ id: pieceList0
+
+ width: parent.width
+ columns: root.columns
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList1
+
+ width: parent.width
+ columns: root.columns
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList2
+
+ width: parent.width
+ columns: root.columns
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList3
+
+ width: parent.width
+ columns: root.columns
+ onPiecePicked: root.piecePicked(piece)
+ }
+ }
+ }
+
+ // It would be much simpler to use bindings for y/height and a Behavior for
+ // the y animation, but I haven't found a way to disable the animation if a
+ // game loaded at startup has toPlay != 0
+ states: [
+ State {
+ name: "toPlay0"
+ when: gameModel.toPlay === 0
+
+ PropertyChanges {
+ target: toPlayIndicator
+ y: column.mapToItem(parent, 0, pieceList0.y).y
+ height: pieceList0.height
+ }
+ },
+ State {
+ name: "toPlay1"
+ when: gameModel.toPlay === 1
+
+ PropertyChanges {
+ target: toPlayIndicator
+ y: column.mapToItem(parent, 0, pieceList1.y).y
+ height: pieceList1.height
+ }
+ },
+ State {
+ name: "toPlay2"
+ when: gameModel.toPlay === 2
+
+ PropertyChanges {
+ target: toPlayIndicator
+ y: column.mapToItem(parent, 0, pieceList2.y).y
+ height: pieceList2.height
+ }
+ },
+ State {
+ name: "toPlay3"
+ when: gameModel.toPlay === 3
+
+ PropertyChanges {
+ target: toPlayIndicator
+ y: column.mapToItem(parent, 0, pieceList3.y).y
+ height: pieceList0.height
+ }
+ }
+ ]
+ transitions: Transition {
+ id: transition
+
+ NumberAnimation {
+ target: toPlayIndicator
+ property: "y"
+ duration: 0.6 * animationDurationFast
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceSelectorMobile.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+
+Flickable {
+ id: root
+
+ property alias pieces0: pieceList0.pieces
+ property alias pieces1: pieceList1.pieces
+ property alias pieces2: pieceList2.pieces
+ property alias pieces3: pieceList3.pieces
+ property int columns
+ property real rowSpacing
+ property alias transitionsEnabled: transition.enabled
+
+ signal piecePicked(var piece)
+
+ flickableDirection: Flickable.VerticalFlick
+ contentHeight: Math.max(pieceList0.y + pieceList0.height,
+ pieceList1.y + pieceList1.height,
+ pieceList2.y + pieceList2.height,
+ pieceList3.y + pieceList3.height)
+ clip: true
+
+ Behavior on contentY { NumberAnimation { duration: animationDurationFast } }
+
+ PieceList {
+ id: pieceList0
+
+ width: root.width
+ columns: root.columns
+ rowSpacing: root.rowSpacing
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList1
+
+ width: root.width
+ columns: root.columns
+ rowSpacing: root.rowSpacing
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList2
+
+ width: root.width
+ columns: root.columns
+ rowSpacing: root.rowSpacing
+ onPiecePicked: root.piecePicked(piece)
+ }
+ PieceList {
+ id: pieceList3
+
+ width: root.width
+ columns: root.columns
+ rowSpacing: root.rowSpacing
+ onPiecePicked: root.piecePicked(piece)
+ }
+
+ // States order the piece lists such that the color to play is on top. If a
+ // player plays two colors, their second color follows, such that at least
+ // at the end of the game all of their remaining pieces should be in the
+ // visible area. Otherwise the colors are in order of play.
+ states: [
+ State {
+ name: "toPlay0"
+ when: gameModel.toPlay === 0
+
+ PropertyChanges {
+ target: pieceList0
+ y: 0.5 * rowSpacing
+ }
+ PropertyChanges {
+ target: pieceList1
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList0.height + pieceList2.height
+ + 2.5 * rowSpacing
+ return pieceList0.height + 1.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList2
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList0.height + 1.5 * rowSpacing
+ return pieceList0.height + pieceList1.height
+ + 2.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList3
+ y: pieceList0.height + pieceList1.height + pieceList2.height
+ + 3.5 * rowSpacing
+ }
+ },
+ State {
+ name: "toPlay1"
+ when: gameModel.toPlay === 1
+
+ PropertyChanges {
+ target: pieceList1
+ y: 0.5 * rowSpacing
+ }
+ PropertyChanges {
+ target: pieceList2
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList1.height + pieceList3.height
+ + 2.5 * rowSpacing
+ return pieceList1.height + 1.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList3
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList1.height + 1.5 * rowSpacing
+ return pieceList1.height + pieceList2.height
+ + 2.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList0
+ y: {
+ if (gameModel.nuColors === 2)
+ return pieceList1.height + 1.5 * rowSpacing
+ if (gameModel.nuColors === 3)
+ return pieceList1.height + pieceList2.height
+ + 2.5 * rowSpacing
+ return pieceList1.height + pieceList2.height
+ + pieceList3.height + 3.5 * rowSpacing
+ }
+ }
+ },
+ State {
+ name: "toPlay2"
+ when: gameModel.toPlay === 2
+
+ PropertyChanges {
+ target: pieceList2
+ y: 0.5 * rowSpacing
+ }
+ PropertyChanges {
+ target: pieceList3
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList2.height + pieceList0.height
+ + 2.5 * rowSpacing
+ return pieceList2.height + 1.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList0
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList2.height + 1.5 * rowSpacing
+ if (gameModel.nuColors === 3)
+ return pieceList2.height + pieceList3.height
+ + 2.5 * rowSpacing
+ return pieceList2.height + pieceList3.height
+ + 2.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList1
+ y: {
+ if (gameModel.nuColors === 3)
+ return pieceList2.height + pieceList0.height
+ + 2.5 * rowSpacing
+ return pieceList2.height + pieceList3.height
+ + pieceList0.height + 3.5 * rowSpacing
+ }
+ }
+ },
+ State {
+ name: "toPlay3"
+ when: gameModel.toPlay === 3
+
+ PropertyChanges {
+ target: pieceList3
+ y: 0.5 * rowSpacing
+ }
+ PropertyChanges {
+ target: pieceList0
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList3.height + pieceList1.height
+ + 2.5 * rowSpacing
+ return pieceList3.height + 1.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList1
+ y: {
+ if (gameModel.nuColors === 4 && gameModel.nuPlayers === 2)
+ return pieceList3.height + 1.5 * rowSpacing
+ return pieceList3.height + pieceList0.height
+ + 2.5 * rowSpacing
+ }
+ }
+ PropertyChanges {
+ target: pieceList2
+ y: pieceList3.height + pieceList0.height + pieceList1.height
+ + 3.5 * rowSpacing
+ }
+ }
+ ]
+ transitions:
+ Transition {
+ id: transition
+
+ 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:
+ Math.max(animationDurationMove - animationDurationFast,
+ 0)
+ }
+ NumberAnimation {
+ target: root
+ property: "opacity"
+ to: 0
+ duration: animationDurationFast
+ }
+ 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: animationDurationFast
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceSwitchedFlipAnimation.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+// Helper animation for pieces.
+// Unique piece 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.
+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 }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/PieceTrigon.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+// See PieceClassic.qml for comments
+Item
+{
+ id: root
+
+ property QtObject pieceModel
+ property var color:
+ switch (pieceModel.color) {
+ case 0: return color0
+ case 1: return color1
+ case 2: return color2
+ case 3: return color3
+ }
+ property Item parentUnplayed
+ property string imageName:
+ "image://pentobi/triangle/" + color[0] + "/" + color[1] + "/"
+ + color[2]
+ property string imageNameDownward:
+ "image://pentobi/triangle-down/" + color[0] + "/" + color[1] + "/"
+ + color[2]
+ property real scaleUnplayed:
+ parentUnplayed ? 0.14 * parentUnplayed.width / board.gridWidth : 0
+ property bool flippedX: Math.abs(flipX.angle - 180) < 90
+ property real pieceAngle: flippedX ? rotation + 180 : rotation
+ property real isSmall: scale < 0.5 ? 1 : 0
+ property real imageOpacity0: imageOpacity(pieceAngle, 0) * (1 - isSmall)
+ property real imageOpacity60: imageOpacity(pieceAngle, 60) * (1 - isSmall)
+ property real imageOpacity120: imageOpacity(pieceAngle, 120) * (1 - isSmall)
+ property real imageOpacity180: imageOpacity(pieceAngle, 180) * (1 - isSmall)
+ property real imageOpacity240: imageOpacity(pieceAngle, 240) * (1 - isSmall)
+ property real imageOpacity300: imageOpacity(pieceAngle, 300) * (1 - isSmall)
+ property real imageOpacitySmall0: imageOpacity(pieceAngle, 0) * isSmall
+ property real imageOpacitySmall60: imageOpacity(pieceAngle, 60) * isSmall
+ property real imageOpacitySmall120: imageOpacity(pieceAngle, 120) * isSmall
+ property real imageOpacitySmall180: imageOpacity(pieceAngle, 180) * isSmall
+ property real imageOpacitySmall240: imageOpacity(pieceAngle, 240) * isSmall
+ property real imageOpacitySmall300: imageOpacity(pieceAngle, 300) * isSmall
+
+ transform: [
+ Rotation {
+ id: flipX
+
+ axis { x: 1; y: 0; z: 0 }
+ },
+ Rotation {
+ id: flipY
+
+ axis { x: 0; y: 1; z: 0 }
+ }
+ ]
+
+ function _isDownward(pos) { return (pos.x % 2 == 0) != (pos.y % 2 == 0) }
+ function imageOpacity(pieceAngle, imgAngle) {
+ var angle = (pieceAngle - imgAngle + 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 * board.gridWidth
+ height: board.gridHeight
+ x: (modelData.x - pieceModel.center.x - 0.5) * board.gridWidth
+ y: (modelData.y - pieceModel.center.y) * board.gridHeight
+ }
+ }
+ Rectangle {
+ opacity: moveMarking == "last_dot" && pieceModel.isLastMove ? 0.5 : 0
+ color: gameModel.showVariations && ! gameModel.isMainVar ? "transparent" : border.color
+ border { width: 0.2 * width; color: root.color[3] }
+ width: 0.3 * board.gridHeight
+ height: width
+ radius: width / 2
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+
+ Behavior on opacity { NumberAnimation { duration: animationDurationFast } }
+ }
+ Loader {
+ sourceComponent: moveMarking === "all_number"
+ || moveMarking === "last_number" || item ?
+ textComponent : null
+
+ Component {
+ id: textComponent
+
+ Text {
+ property bool flippedY: Math.abs(flipY.angle - 180) < 90
+
+ text: moveMarking == "all_number"
+ || (moveMarking == "last_number"
+ && pieceModel.isLastMove) ?
+ pieceModel.moveLabel : ""
+ opacity: text === "" ? 0 : 1
+ color: root.color[3]
+ width: board.gridWidth
+ height: board.gridHeight
+ fontSizeMode: Text.Fit
+ font {
+ pixelSize: 0.5 * board.gridHeight
+ preferShaping: false
+ }
+ minimumPixelSize: 5
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ x: pieceModel.labelPos.x * board.gridWidth - width / 2
+ y: pieceModel.labelPos.y * board.gridHeight - height / 2
+ transform: [
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 0; y: 1; z: 0 }
+ angle: flippedY ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ axis { x: 1; y: 0; z: 0 }
+ angle: flippedX ? -180 : 0
+ },
+ Rotation {
+ origin {
+ x: board.gridWidth / 2; y: board.gridHeight / 2
+ }
+ angle: -root.rotation
+ }
+ ]
+ Behavior on opacity {
+ NumberAnimation { duration: animationDurationFast }
+ }
+ }
+ }
+ }
+ 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: ",rot180Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot60,rot240Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot120,rot300Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot180,flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot240,rot60Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ from: "rot300,rot120Flip"; to: from
+ enabled: enableAnimations
+
+ PieceSwitchedFlipAnimation { }
+ },
+ Transition {
+ enabled: enableAnimations
+
+ PieceRotationAnimation { }
+ PieceFlipAnimation { target: flipX }
+ }
+ ]
+ }
+
+ states: [
+ State {
+ name: "picked"
+ when: root === pickedPiece
+
+ ParentChange {
+ target: root
+ parent: pieceManipulator
+ x: pieceManipulator.width / 2
+ y: pieceManipulator.height / 2
+ }
+ },
+ State {
+ name: "played"
+ when: pieceModel.isPlayed
+
+ ParentChange {
+ target: root
+ parent: board.grabImageTarget
+ x: board.mapFromGameX(pieceModel.gameCoord.x) - board.grabImageTarget.x
+ y: board.mapFromGameY(pieceModel.gameCoord.y) - board.grabImageTarget.y
+ }
+ },
+ State {
+ name: "unplayed"
+ when: parentUnplayed != null
+
+ PropertyChanges {
+ target: root
+ scale: scaleUnplayed
+ }
+ ParentChange {
+ target: root
+ parent: parentUnplayed
+ x: parentUnplayed.width / 2
+ y: parentUnplayed.height / 2
+ }
+ }
+ ]
+
+ transitions:
+ Transition {
+ from: "unplayed,picked,played"; to: from
+ enabled: enableAnimations
+
+ SequentialAnimation {
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 1
+ }
+ ParentAnimation {
+ via: isDesktop ? null : gameDisplay
+
+ NumberAnimation {
+ properties: "x,y,scale"
+ duration: animationDurationMove
+ easing.type: Easing.InOutSine
+ }
+ }
+ PropertyAction {
+ target: parentUnplayed.parent
+ property: "z"; value: 0
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/QuarterSquare.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece element used in GembloQ. See Square.qml for comments
+Item {
+ property int pointType
+
+ Loader {
+ opacity: switch (pointType) {
+ case 0: return imageOpacity0
+ case 1: return imageOpacity180
+ case 2: return imageOpacity90
+ case 3: return imageOpacity270
+ }
+ sourceComponent: opacity > 0 || item ? componentTop : null
+
+ Component {
+ id: componentTop
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ // Don't set antialiasing, vertex antialiasing causes unwanted
+ // seams between edges of the quarter squares
+ antialiasing: false
+ rotation: switch (pointType) {
+ case 1: return 180
+ case 2: return 270
+ case 3: return 90
+ default: return 0
+ }
+ x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+ }
+ }
+ }
+ Loader {
+ opacity: switch (pointType) {
+ case 0: return imageOpacitySmall0
+ case 1: return imageOpacitySmall180
+ case 2: return imageOpacitySmall90
+ case 3: return imageOpacitySmall270
+ }
+ sourceComponent: opacity > 0 || item ? componentSmallTop : null
+
+ Component {
+ id: componentSmallTop
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ // Don't set antialiasing, see above
+ antialiasing: false
+ rotation: switch (pointType) {
+ case 1: return 180
+ case 2: return 270
+ case 3: return 90
+ default: return 0
+ }
+ x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+ }
+ }
+ }
+ Loader {
+ opacity: switch (pointType) {
+ case 0: return imageOpacity180
+ case 1: return imageOpacity0
+ case 2: return imageOpacity270
+ case 3: return imageOpacity90
+ }
+ sourceComponent: opacity > 0 || item ? componentBottom : null
+
+ Component {
+ id: componentBottom
+
+ Image {
+ source: imageNameBottom
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ // Don't set antialiasing, see above
+ antialiasing: false
+ rotation: switch (pointType) {
+ case 1: return 180
+ case 2: return 270
+ case 3: return 90
+ default: return 0
+ }
+ x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+ }
+ }
+ }
+ Loader {
+ opacity: switch (pointType) {
+ case 0: return imageOpacitySmall180
+ case 1: return imageOpacitySmall0
+ case 2: return imageOpacitySmall270
+ case 3: return imageOpacitySmall90
+ }
+ sourceComponent: opacity > 0 || item ? componentSmallBottom : null
+
+ Component {
+ id: componentSmallBottom
+
+ Image {
+ source: imageNameBottom
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ // Don't set antialiasing, see above
+ antialiasing: false
+ rotation: switch (pointType) {
+ case 1: return 180
+ case 2: return 270
+ case 3: return 90
+ default: return 0
+ }
+ x: pointType == 1 || pointType == 3 ? -width / 2 : 0
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/QuestionDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.Dialog {
+ id: root
+
+ function openWithCallback(text, acceptedFunc) {
+ label.text = text
+ _acceptedFunc = acceptedFunc
+ open()
+ }
+
+ property var _acceptedFunc
+
+ footer: DialogButtonBoxOkCancel { }
+ onAccepted: _acceptedFunc()
+
+ Item {
+ implicitWidth:
+ Math.max(Math.min(label.implicitWidth,
+ font.pixelSize * 25, maxContentWidth),
+ font.pixelSize * 15, minContentWidth)
+ implicitHeight: label.implicitHeight
+
+ Label {
+ id: label
+
+ anchors.fill: parent
+ wrapMode: Text.Wrap
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/RatingDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import "." as Pentobi
+import "Main.js" as Logic
+
+Pentobi.Dialog {
+ property int numberGames: ratingModel.numberGames
+ property var history: ratingModel.history
+
+ footer: Pentobi.DialogButtonBox { ButtonClose { } }
+
+ Item {
+ implicitWidth: Math.max(Math.min(font.pixelSize * 22, maxContentWidth),
+ minContentWidth)
+ implicitHeight: columnLayout.implicitHeight
+
+ ColumnLayout
+ {
+ id: columnLayout
+
+ anchors.fill: parent
+
+ GridLayout {
+ columns: 2
+
+ Label {
+ id: labelYourRating
+
+ text: qsTr("Your rating:")
+ }
+ Label {
+ text: ratingModel.numberGames === 0 ?
+ "" : Math.round(ratingModel.rating).toString()
+ Layout.fillWidth: true
+ font.bold: true
+ }
+ Label { text: qsTr("Game variant:") }
+ Label {
+ text: switch (ratingModel.gameVariant) {
+ case "classic_2":
+ //: Short for Classic (2 players)
+ return qsTr("Classic (2)")
+ case "classic_3":
+ //: Short for Classic (3 players)
+ return qsTr("Classic (3)")
+ case "classic":
+ //: Short for Classic (4 players)
+ return qsTr("Classic (4)")
+ case "duo":
+ return qsTr("Duo")
+ case "junior":
+ return qsTr("Junior")
+ case "trigon_2":
+ //: Short for Trigon (2 players)
+ return qsTr("Trigon (2)")
+ case "trigon_3":
+ //: Short for Trigon (3 players)
+ return qsTr("Trigon (3)")
+ case "trigon":
+ //: Short for Trigon (4 players)
+ return qsTr("Trigon (4)")
+ case "nexos_2":
+ //: Short for Nexos (2 players)
+ return qsTr("Nexos (2)")
+ case "nexos":
+ //: Short for Nexos (4 players)
+ return qsTr("Nexos (4)")
+ case "callisto_2":
+ //: Short for Callisto (2 players, 2 colors)
+ return qsTr("Callisto (2)")
+ case "callisto_2_4":
+ //: Short for Callisto (2 players, 4 colors)
+ return qsTr("Callisto (2/4)")
+ case "callisto_3":
+ //: Short for Callisto (3 players)
+ return qsTr("Callisto (3)")
+ case "callisto":
+ //: Short for Callisto (4 players)
+ return qsTr("Callisto (4)")
+ case "gembloq":
+ //: Short for GembloQ (4 players)
+ return qsTr("GembloQ (4)")
+ case "gembloq_2":
+ //: Short for GembloQ (2 players, 2 colors)
+ return qsTr("GembloQ (2)")
+ case "gembloq_2_4":
+ //: Short for GembloQ (2 players, 4 colors)
+ return qsTr("GembloQ (2/4)")
+ case "gembloq_3":
+ //: Short for GembloQ (3 players)
+ return qsTr("GembloQ (3)")
+ default: return ""
+ }
+ Layout.fillWidth: true
+ }
+ Label { text: qsTr("Rated games:") }
+ Label {
+ text: numberGames
+ Layout.fillWidth: true
+ }
+ Label {
+ visible: numberGames > 1
+ text: qsTr("Best previous rating:")
+ }
+ Label {
+ visible: numberGames > 1
+ text: Math.round(ratingModel.bestRating).toString()
+ Layout.fillWidth: true
+ }
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+
+ Label {
+ visible: history.length > 1
+ text: qsTr("Recent development:")
+ }
+ RatingGraph {
+ visible: history.length > 1
+ history: ratingModel.history
+ Layout.preferredHeight:
+ Math.min(font.pixelSize * 8,
+ 0.22 * rootWindow.contentItem.width,
+ 0.22 * rootWindow.contentItem.height)
+ Layout.fillWidth: true
+ }
+ }
+ ScrollView
+ {
+ visible: history.length > 0
+ clip: true
+ Layout.fillWidth: true
+ Layout.preferredHeight:
+ Math.min(font.pixelSize * 8,
+ 0.22 * rootWindow.contentItem.width,
+ 0.22 * rootWindow.contentItem.height)
+
+ Item
+ {
+ implicitHeight: grid.height
+ implicitWidth: grid.width
+
+ GridLayout {
+ id: grid
+
+ rows: history.length + 1
+ flow: Grid.TopToBottom
+
+ Label {
+ id: gameHeader
+
+ font.underline: true
+ text: qsTr("Game")
+ }
+ Repeater {
+ id: gameRepeater
+
+ model: history
+
+ Label { text: modelData.number }
+ }
+ Label { font.underline: true; text: qsTr("Result") }
+ Repeater {
+ model: history
+
+ Label {
+ text: switch (modelData.result) {
+ case 1:
+ //: Result of rated game is a win
+ return qsTr("Win")
+ case 0:
+ //: Result of rated game is a loss
+ return qsTr("Loss")
+ case 0.5:
+ //: Result of rated game is a tie. Abbreviate long translations to
+ //: ensure that all columns of rated games list are visible on
+ //: mobile devices with small screens.
+ return qsTr("Tie")
+ }
+ }
+ }
+ Label { font.underline: true; text: qsTr("Level") }
+ Repeater {
+ model: history
+
+ Label { text: modelData.level }
+ }
+ Label { font.underline: true; text: qsTr("Your Color") }
+ Repeater {
+ model: history
+
+ Label { text: gameModel.getPlayerString(modelData.color) }
+ }
+ Label { font.underline: true; text: qsTr("Date") }
+ Repeater {
+ model: history
+
+ Label { text: modelData.date }
+ }
+ }
+ MouseArea {
+ function openMenu(x, y) {
+ if (y < gameHeader.height)
+ return
+ var n = history.length
+ var i
+ for (i = 1; i < n; ++i)
+ if (y < gameRepeater.itemAt(i).y)
+ break
+ menu.row = i - 1
+ menu.popup(mouseX, mouseY)
+ }
+
+ anchors.fill: grid
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onClicked: openMenu(mouseX, mouseY)
+ onPressAndHold: openMenu(mouseX, mouseY)
+
+ Pentobi.Menu {
+ id: menu
+
+ property int row
+
+ width:
+ Math.min(font.pixelSize * 14, maxContentWidth)
+
+ Pentobi.MenuItem {
+ width: parent.width
+ text: history && menu.row < history.length ?
+ qsTr("Open Game %1").arg(history[menu.row].number) : ""
+ onTriggered: {
+ Logic.openFile(
+ ratingModel.getFile(
+ history[menu.row].number))
+ close()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/RatingGraph.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Canvas {
+ property var history
+
+ antialiasing: true
+ onHistoryChanged: requestPaint()
+ onPaint: {
+ var w = width
+ var h = height
+ var ctx = getContext("2d")
+ ctx.fillStyle = "white"
+ ctx.fillRect(0, 0, w, h)
+ if (history === null)
+ return
+ var n = history.length
+ if (n === 0)
+ return
+ var margin = w / 30
+ ctx.save()
+ ctx.translate(margin, margin)
+ w -= 2 * margin
+ h -= 2 * margin
+ var i
+ var minX = Number.POSITIVE_INFINITY
+ var maxX = Number.NEGATIVE_INFINITY
+ var minY = Number.POSITIVE_INFINITY
+ var maxY = Number.NEGATIVE_INFINITY
+ var info
+ for (i = 0; i < n; ++i) {
+ info = ratingModel.history[i]
+ minX = Math.min(minX, info.number)
+ maxX = Math.max(maxX, info.number)
+ minY = Math.min(minY, info.rating)
+ maxY = Math.max(maxY, info.rating)
+ }
+ maxX = minX + Math.ceil((maxX - minX) * 1.2)
+ minY = Math.floor(minY / 100) * 100
+ maxY = Math.ceil(maxY / 100) * 100
+ if (maxY - minY < 100)
+ maxY = minY + 100
+
+ ctx.beginPath()
+ var top = 0
+ ctx.moveTo(0, top)
+ ctx.lineTo(w, top)
+ var bottom = h
+ ctx.moveTo(0, bottom)
+ ctx.lineTo(w, bottom)
+ ctx.strokeStyle = "gray"
+ ctx.stroke()
+
+ ctx.font = Math.ceil(0.15 * h) + "px sans-serif"
+ ctx.fillStyle = "gray"
+ ctx.textAlign = "right"
+ ctx.fillText(minY, w, h - w / 60)
+ ctx.textBaseline = "top"
+ ctx.fillText(maxY, w, w / 60)
+
+ ctx.beginPath()
+ for (i = 0; i < n; ++i) {
+ info = ratingModel.history[i]
+ ctx.lineTo((info.number - minX) / (maxX - minX) * w,
+ h - (info.rating - minY) / (maxY - minY) * h)
+ }
+ ctx.strokeStyle = "red"
+ ctx.stroke()
+
+ ctx.restore()
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/SaveDialog.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import "Main.js" as Logic
+import "." as Pentobi
+
+Pentobi.FileDialog {
+ title: qsTr("Save")
+ selectExisting: false
+ nameFilterLabels: [ qsTr("Blokus games") ]
+ nameFilters: [ [ "*.blksgf", "*.BLKSGF" ] ]
+ folder: rootWindow.folder
+ onAccepted: {
+ rootWindow.folder = folder
+ Logic.saveFile(Logic.getFileFromUrl(fileUrl))
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ScoreDisplay.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Layouts 1.0
+
+Item {
+ id: root
+
+ property int altPlayer: gameModel.altPlayer
+ property real points0: gameModel.points0
+ property real points1: gameModel.points1
+ property real points2: gameModel.points2
+ property real points3: gameModel.points3
+ property real bonus0: gameModel.bonus0
+ property real bonus1: gameModel.bonus1
+ property real bonus2: gameModel.bonus2
+ property real bonus3: gameModel.bonus3
+ property bool hasMoves0: gameModel.hasMoves0
+ property bool hasMoves1: gameModel.hasMoves1
+ property bool hasMoves2: gameModel.hasMoves2
+ property bool hasMoves3: gameModel.hasMoves3
+
+ RowLayout {
+ id: rowLayout
+
+ width: root.width
+ height: Math.min(root.height, 0.047 * root.width)
+ anchors.centerIn: parent
+ spacing: 0
+
+ Item { Layout.fillWidth: true }
+ ScoreElement2 {
+ id: playerScore0
+
+ visible: gameModel.nuColors === 4 && gameModel.nuPlayers === 2
+ value: points0 + points2
+ isFinal: ! hasMoves0 && ! hasMoves2
+ fontSize: rowLayout.height
+ color1: gameDisplay.color0[0]
+ color2: gameDisplay.color2[0]
+ // Avoid position changes unless score text gets really long
+ Layout.minimumWidth: 3 * fontSize
+ }
+ Item { visible: playerScore0.visible; Layout.fillWidth: true }
+ ScoreElement2 {
+ id: playerScore1
+
+ visible: gameModel.nuColors === 4 && gameModel.nuPlayers === 2
+ value: points1 + points3
+ isFinal: ! hasMoves1 && ! hasMoves3
+ fontSize: rowLayout.height
+ color1: gameDisplay.color1[0]
+ color2: gameDisplay.color3[0]
+ // Avoid position changes unless score text gets really long
+ Layout.minimumWidth: 3 * fontSize
+ }
+ Item { visible: playerScore1.visible; Layout.fillWidth: true }
+ ScoreElement {
+ id: colorScore0
+
+ value: points0
+ bonus: bonus0
+ isFinal: ! hasMoves0
+ fontSize: rowLayout.height
+ color: color0[0]
+ // Avoid position changes unless score text gets really long
+ Layout.minimumWidth: 2.3 * fontSize
+ }
+ Item { visible: colorScore0.visible; Layout.fillWidth: true }
+ ScoreElement {
+ id: colorScore1
+
+ value: points1
+ bonus: bonus1
+ isFinal: ! hasMoves1
+ fontSize: rowLayout.height
+ color: color1[0]
+ // Avoid position changes unless score text gets really long
+ Layout.minimumWidth: 2.3 * fontSize
+ }
+ Item { visible: colorScore1.visible; Layout.fillWidth: true }
+ ScoreElement {
+ id: colorScore2
+
+ visible: gameModel.nuColors > 2
+ value: points2
+ bonus: bonus2
+ isFinal: ! hasMoves2
+ fontSize: rowLayout.height
+ color: color2[0]
+ // Avoid position changes unless score text gets really long
+ Layout.minimumWidth: 2.3 * fontSize
+ }
+ Item { visible: colorScore2.visible; Layout.fillWidth: true }
+ ScoreElement {
+ id: colorScore3
+
+ visible: gameModel.nuColors > 3
+ && gameModel.gameVariant !== "classic_3"
+ value: points3
+ bonus: bonus3
+ isFinal: ! hasMoves3
+ fontSize: rowLayout.height
+ color: color3[0]
+ // Avoid position changes unless score text gets really long
+ Layout.minimumWidth: 2.3 * fontSize
+ }
+ Item { visible: colorScore3.visible; Layout.fillWidth: true }
+ ScoreElement2 {
+ id: altColorIndicator
+
+ visible: gameModel.gameVariant === "classic_3" && hasMoves3
+ value: points3
+ isAltColor: true
+ isFinal: ! hasMoves3
+ fontSize: rowLayout.height
+ color1: theme.colorGreen[0]
+ color2:
+ switch (altPlayer) {
+ case 0: return gameDisplay.color0[0]
+ case 1: return gameDisplay.color1[0]
+ case 2: return gameDisplay.color2[0]
+ }
+ }
+ Item { visible: altColorIndicator.visible; Layout.fillWidth: true }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ScoreElement.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+ property alias color: point.color
+ property bool isFinal
+ property real value
+ property real bonus
+ property alias fontSize: text.font.pixelSize
+
+ implicitWidth: point.implicitWidth + text.implicitWidth
+ + text.anchors.leftMargin
+ implicitHeight: Math.max(point.implicitHeight, text.implicitHeight)
+
+ Rectangle {
+ id: point
+
+ anchors.verticalCenter: parent.verticalCenter
+ implicitWidth: 0.7 * fontSize
+ implicitHeight: 0.7 * fontSize
+ radius: width / 2
+ }
+ Text {
+ id: text
+
+ anchors {
+ verticalCenter: parent.verticalCenter
+ left: point.right
+ leftMargin: 0.14 * font.pixelSize
+ }
+ text: ! isFinal ?
+ "%L1".arg(value) :
+ "%1<u>%L2</u>".arg(bonus > 0 ? "★" : "").arg(value)
+ color: theme.colorText
+ opacity: 0.8
+ font.preferShaping: false
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ScoreElement2.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+
+Item {
+ id: root
+
+ property color color1
+ property color color2
+ property bool isFinal
+ property bool isAltColor
+ property real value
+ property alias fontSize: text.font.pixelSize
+
+ implicitWidth: point1.implicitWidth + point1.implicitWidth
+ + text.implicitWidth + text.anchors.leftMargin
+ implicitHeight: Math.max(point1.implicitHeight, point2.implicitHeight,
+ text.implicitHeight)
+
+ Rectangle {
+ id: point1
+
+ anchors.verticalCenter: parent.verticalCenter
+ implicitWidth: 0.7 * fontSize
+ implicitHeight: 0.7 * fontSize
+ color: color1
+ opacity: isAltColor && isFinal ? 0 : 1
+ radius: width / 2
+ }
+ Rectangle {
+ id: point2
+
+ anchors {
+ verticalCenter: parent.verticalCenter
+ left: point1.right
+ }
+ implicitWidth: 0.7 * fontSize
+ implicitHeight: 0.7 * fontSize
+ color: isAltColor && isFinal ? color1 : color2
+ radius: width / 2
+ }
+ Text {
+ id: text
+
+ anchors {
+ verticalCenter: parent.verticalCenter
+ left: point2.right
+ leftMargin: 0.14 * font.pixelSize
+ }
+ text: isAltColor ? ""
+ : isFinal ? "<u>%L1</u>".arg(value) : "%L1".arg(value)
+ color: theme.colorText
+ opacity: 0.8
+ font.preferShaping: false
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Square.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+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. All
+// images have two versions with the sourceSize optimzed for the statically
+// displayed states on the board and in the piece selector, which produces
+// better results than using Image.mipmap and avoids a mipmap bug with Nvidia
+// cards (QTBUG-57845).
+Item {
+ Loader {
+ opacity: imageOpacity0
+ sourceComponent: opacity > 0 || item ? component0 : null
+
+ Component {
+ id: component0
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall0
+ sourceComponent: opacity > 0 || item ? componentSmall0 : null
+
+ Component {
+ id: componentSmall0
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacity90
+ sourceComponent: opacity > 0 || item ? component90 : null
+
+ Component {
+ id: component90
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ rotation: -90
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall90
+ sourceComponent: opacity > 0 || item ? componentSmall90 : null
+
+ Component {
+ id: componentSmall90
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ rotation: -90
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacity180
+ sourceComponent: opacity > 0 || item ? component180 : null
+
+ Component {
+ id: component180
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ rotation: -180
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall180
+ sourceComponent: opacity > 0 || item ? componentSmall180 : null
+
+ Component {
+ id: componentSmall180
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ rotation: -180
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacity270
+ sourceComponent: opacity > 0 || item ? component270 : null
+
+ Component {
+ id: component270
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ rotation: -270
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall270
+ sourceComponent: opacity > 0 || item ? componentSmall270 : null
+
+ Component {
+ id: componentSmall270
+
+ Image {
+ source: imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ rotation: -270
+ }
+ }
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/ToolBar.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.0
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.1
+import "." as Pentobi
+import "Main.js" as Logic
+
+Item {
+ id: root
+
+ // Show toolbar content (menu button is always shown)
+ property bool showContent: true
+
+ function clickMenuButton() {
+ menuButton.checked = true
+ menuButton.onClicked()
+ menu.item.currentIndex = 0
+ }
+
+ implicitWidth: rowLayout.implicitWidth
+ implicitHeight: rowLayout.implicitHeight
+
+ RowLayout {
+ id: rowLayout
+
+ anchors.fill: parent
+ spacing: 0
+
+ // Like the label used for desktop after the toolbuttons, but with
+ // shorter text for small smartphone screens
+ Label {
+ id: mobileLabel
+
+ visible: ! isDesktop && showContent
+ color: theme.colorText
+ opacity: isRated ? 0.6 : 0.8
+ elide: Text.ElideRight
+ text: Logic.getGameLabel(gameDisplay.setupMode, isRated,
+ gameModel.file, gameModel.isModified, true)
+ // There is a bug in Qt 5.11 that in some situations elides the
+ // text even if there is enough room for it. It doesn't occur if
+ // we use implicitWidth + 1 instead if implicitWidth
+ Layout.maximumWidth: implicitWidth + 1
+ Layout.fillWidth: true
+ Layout.leftMargin: root.height / 10
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: if (mobileLabel.truncated) ToolTip.visible = true
+ ToolTip.text: mobileLabel.text
+ ToolTip.timeout: 2000
+ }
+ }
+ Item {
+ visible: ! isDesktop
+ Layout.fillWidth: true
+ }
+ Pentobi.Button {
+ id: newGame
+
+ icon.source: theme.getImage("pentobi-newgame")
+ action: actionNew
+ visible: showContent && (isDesktop || enabled)
+ }
+ Pentobi.Button {
+ id: newGameRated
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-rated-game")
+ action: actionNewRated
+ }
+ Pentobi.Button {
+ id: undo
+
+ icon.source: theme.getImage("pentobi-undo")
+ action: actionUndo
+ visible: showContent && (isDesktop || enabled)
+ autoRepeat: true
+ autoRepeatInterval:
+ rootWindow.gameDisplay.item ?
+ 2 * rootWindow.gameDisplay.item.animationDuration : 400
+
+ }
+ Pentobi.Button {
+ id: computerSettings
+
+ icon.source: theme.getImage("pentobi-computer-colors")
+ action: actionComputerSettings
+ visible: showContent && (isDesktop || enabled)
+ }
+ Pentobi.Button {
+ id: play
+
+ icon.source: theme.getImage("pentobi-play")
+ action: actionPlay
+ visible: showContent && (isDesktop || enabled)
+ autoRepeat: true
+ autoRepeatInterval:
+ rootWindow.gameDisplay.item ?
+ rootWindow.gameDisplay.item.animationDuration : 200
+ }
+ Pentobi.Button {
+ id: stop
+
+ icon.source: theme.getImage("pentobi-stop")
+ action: actionStop
+ visible: showContent && (isDesktop || ! isRated)
+ }
+ Item {
+ visible: isDesktop
+ Layout.fillWidth: true
+ Layout.maximumWidth: 0.3 * parent.height
+ }
+ Pentobi.Button {
+ id: beginning
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-beginning")
+ action: actionBeginning
+ }
+ Pentobi.Button {
+ id: backward10
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-backward10")
+ action: actionBackward10
+ autoRepeat: true
+ autoRepeatInterval:
+ rootWindow.gameDisplay.item ?
+ rootWindow.gameDisplay.item.animationDuration : 200
+ }
+ Pentobi.Button {
+ id: backward
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-backward")
+ action: actionBackward
+ autoRepeat: true
+ }
+ Pentobi.Button {
+ id: forward
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-forward")
+ action: actionForward
+ autoRepeat: true
+ }
+ Pentobi.Button {
+ id: forward10
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-forward10")
+ action: actionForward10
+ autoRepeat: true
+ autoRepeatInterval:
+ rootWindow.gameDisplay.item ?
+ rootWindow.gameDisplay.item.animationDuration : 200
+ }
+ Pentobi.Button {
+ id: end
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-end")
+ action: actionEnd
+ }
+ Item {
+ visible: isDesktop
+ Layout.fillWidth: true
+ Layout.maximumWidth: 0.3 * parent.height
+ }
+ Pentobi.Button {
+ id: prevVar
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-previous-variation")
+ action: actionPrevVar
+ autoRepeat: true
+ autoRepeatInterval:
+ rootWindow.gameDisplay.item ?
+ 2 * rootWindow.gameDisplay.item.animationDuration : 400
+ }
+ Pentobi.Button {
+ id: nextVar
+
+ visible: showContent && isDesktop
+ icon.source: theme.getImage("pentobi-next-variation")
+ action: actionNextVar
+ autoRepeat: true
+ autoRepeatInterval:
+ rootWindow.gameDisplay.item ?
+ 2 * rootWindow.gameDisplay.item.animationDuration : 400
+ }
+ Item {
+ visible: isDesktop
+ Layout.fillWidth: true
+ Layout.maximumWidth: 0.3 * parent.height
+ }
+ Label {
+ visible: showContent && isDesktop
+ text: Logic.getGameLabel(gameDisplay.setupMode, isRated,
+ gameModel.file, gameModel.isModified, false)
+ color: theme.colorText
+ opacity: 0.8
+ elide: Text.ElideRight
+ // See comment at Layout.maximumWidth of first label
+ Layout.maximumWidth: implicitWidth + 1
+ Layout.fillWidth: true
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ ToolTip.text: Logic.getFileInfo(isRated, gameModel.file,
+ gameModel.isModified)
+ ToolTip.visible: containsMouse && ! gameDisplay.setupMode
+ && (gameModel.file !== "" || isRated)
+ ToolTip.delay: 1000
+ ToolTip.timeout: 7000
+ }
+ }
+ Item {
+ Layout.fillWidth: true
+ Layout.maximumWidth: isDesktop ? root.width : 0.3 * parent.height
+ }
+ Pentobi.Button {
+ id: menuButton
+
+ icon.source: theme.getImage("menu")
+ down: pressed || (isDesktop && menu.item && menu.item.opened)
+ onClicked: {
+ if (! menu.item)
+ menu.sourceComponent = menuComponent
+ if (menu.item.opened)
+ menu.item.close()
+ else {
+ gameDisplay.dropCommentFocus()
+ menu.item.popup(0, isDesktop ? height : 0)
+ }
+ }
+
+ Loader {
+ id: menu
+
+ // Having the loader fill the button together with
+ // CloseOnPressOutsideParent and the function used in onClicked
+ // seems to be the only way to make a click on the button close
+ // the menu if it is already open. Is there a better way?
+ anchors.fill: parent
+
+ Component {
+ id: menuComponent
+
+ Pentobi.Menu {
+ dynamicWidth: false
+ width: Math.min(font.pixelSize * (isDesktop ? 11 : 18),
+ rootWindow.contentItem.width)
+ closePolicy: Popup.CloseOnPressOutsideParent
+ | Popup.CloseOnEscape
+
+ MenuGame { }
+ MenuGo { }
+ MenuEdit { }
+ MenuView { }
+ MenuComputer { }
+ MenuTools { }
+ MenuHelp { }
+ }
+ }
+ }
+ }
+ }
+ ButtonToolTip {
+ button: newGame
+ ToolTip.text: qsTr("Start a new game")
+ }
+ ButtonToolTip {
+ button: newGameRated
+ ToolTip.text: qsTr("Start a rated game")
+ }
+ ButtonToolTip {
+ button: undo
+ //: Tooltip for Undo button
+ ToolTip.text: qsTr("Undo move")
+ }
+ ButtonToolTip {
+ button: computerSettings
+ ToolTip.text: qsTr("Set the colors played by the computer")
+ }
+ ButtonToolTip {
+ button: play
+ ToolTip.text: {
+ var toPlay = gameModel.toPlay
+ if (gameModel.gameVariant === "classic_3" && toPlay === 3)
+ toPlay = gameModel.altPlayer
+ if ((computerPlays0 && toPlay === 0)
+ || (computerPlays1 && toPlay === 1)
+ || (computerPlays2 && toPlay === 2)
+ || (computerPlays3 && toPlay === 3))
+ return qsTr("Make the computer continue to play the current color")
+ return qsTr("Make the computer play the current color")
+ }
+ }
+ ButtonToolTip {
+ button: stop
+ ToolTip.text: analyzeGameModel.isRunning ?
+ qsTr("Abort game analysis")
+ : qsTr("Abort computer move")
+ }
+ ButtonToolTip {
+ button: beginning
+ ToolTip.text: qsTr("Go to beginning of game")
+ }
+ ButtonToolTip {
+ button: backward10
+ ToolTip.text: qsTr("Go ten moves backward")
+ }
+ ButtonToolTip {
+ button: backward
+ ToolTip.text: qsTr("Go one move backward")
+ }
+ ButtonToolTip {
+ button: forward
+ ToolTip.text: qsTr("Go one move forward")
+ }
+ ButtonToolTip {
+ button: forward10
+ ToolTip.text: qsTr("Go ten moves forward")
+ }
+ ButtonToolTip {
+ button: end
+ ToolTip.text: qsTr("Go to end of moves")
+ }
+ ButtonToolTip {
+ button: prevVar
+ ToolTip.text: qsTr("Go to previous variation")
+ }
+ ButtonToolTip {
+ button: nextVar
+ ToolTip.text: qsTr("Go to next variation")
+ }
+ ButtonToolTip {
+ button: menuButton
+ }
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/Triangle.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.3
+
+// Piece element used in Trigon. See Square.qml for comments
+Item {
+ property bool isDownward
+
+ Loader {
+ opacity: imageOpacity0
+ sourceComponent: opacity > 0 || item ? component0 : null
+
+ Component {
+ id: component0
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall0
+ sourceComponent: opacity > 0 || item ? componentSmall0 : null
+
+ Component {
+ id: componentSmall0
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacity60
+ sourceComponent: opacity > 0 || item ? component60 : null
+
+ Component {
+ id: component60
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ 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 {
+ opacity: imageOpacitySmall60
+ sourceComponent: opacity > 0 || item ? componentSmall60 : null
+
+ Component {
+ id: componentSmall60
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ 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 {
+ opacity: imageOpacity120
+ sourceComponent: opacity > 0 || item ? component120 : null
+
+ Component {
+ id: component120
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ transform: Rotation {
+ angle: -120
+ origin {
+ x: width / 2
+ y: isDownward ? height / 3 : 2 * height / 3
+ }
+ }
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall120
+ sourceComponent: opacity > 0 || item ? componentSmall120 : null
+
+ Component {
+ id: componentSmall120
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ transform: Rotation {
+ angle: -120
+ origin {
+ x: width / 2
+ y: isDownward ? height / 3 : 2 * height / 3
+ }
+ }
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacity180
+ sourceComponent: opacity > 0 || item ? component180 : null
+
+ Component {
+ id: component180
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ rotation: -180
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall180
+ sourceComponent: opacity > 0 || item ? componentSmall180 : null
+
+ Component {
+ id: componentSmall180
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ rotation: -180
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacity240
+ sourceComponent: opacity > 0 || item ? component240 : null
+
+ Component {
+ id: component240
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ transform: Rotation {
+ angle: -240
+ origin {
+ x: width / 2
+ y: isDownward ? height / 3 : 2 * height / 3
+ }
+ }
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall240
+ sourceComponent: opacity > 0 || item ? componentSmall240 : null
+
+ Component {
+ id: componentSmall240
+
+ Image {
+ source: isDownward ? imageNameDownward : imageName
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ antialiasing: true
+ transform: Rotation {
+ angle: -240
+ origin {
+ x: width / 2
+ y: isDownward ? height / 3 : 2 * height / 3
+ }
+ }
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacity300
+ sourceComponent: opacity > 0 || item ? component300 : null
+
+ Component {
+ id: component300
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize: imageSourceSize
+ antialiasing: true
+ transform: [
+ Rotation {
+ angle: -300
+ origin {
+ x: width / 2
+ y: isDownward ? 2 * height / 3 : height / 3
+ }
+ },
+ Translate { y: isDownward ? -height / 3 : height / 3 }
+ ]
+ }
+ }
+ }
+ Loader {
+ opacity: imageOpacitySmall300
+ sourceComponent: opacity > 0 || item ? componentSmall300 : null
+
+ Component {
+ id: componentSmall300
+
+ Image {
+ source: isDownward ? imageName : imageNameDownward
+ width: imageSourceSize.width
+ height: imageSourceSize.height
+ sourceSize {
+ width: scaleUnplayed * imageSourceSize.width
+ height: scaleUnplayed * imageSourceSize.height
+ }
+ 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">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <source>Copyright © 2011–%1 Markus Enzenberger</source>
+ <translation>Copyright © 2011–%1 Markus Enzenberger</translation>
+ </message>
+ <message>
+ <source>Computer opponent for the board game Blokus</source>
+ <translation>Computer-Gegner für das Brettspiel Blokus</translation>
+ </message>
+ <message>
+ <source>Pentobi %1</source>
+ <extracomment>The argument is the application version.</extracomment>
+ <translation>Pentobi %1</translation>
+ </message>
+</context>
+<context>
+ <name>Actions</name>
+ <message>
+ <source>Main Variation</source>
+ <translation type="vanished">Hauptvariante</translation>
+ </message>
+ <message>
+ <source>Beginning of Branch</source>
+ <translation type="vanished">Verzweigungsanfang</translation>
+ </message>
+ <message>
+ <source>Settings…</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation type="vanished">Einstellungen …</translation>
+ </message>
+ <message>
+ <source>Find Move</source>
+ <translation type="vanished">Zug finden</translation>
+ </message>
+ <message>
+ <source>Next Comment</source>
+ <translation type="vanished">Nächster Kommentar</translation>
+ </message>
+ <message>
+ <source>Fullscreen</source>
+ <translation type="vanished">Vollbild</translation>
+ </message>
+ <message>
+ <source>Move Number…</source>
+ <translation type="vanished">Zugnummer …</translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation type="vanished">Pentobi-Hilfe</translation>
+ </message>
+ <message>
+ <source>New</source>
+ <translation type="vanished">Neu</translation>
+ </message>
+ <message>
+ <source>Rated Game</source>
+ <translation type="vanished">Gewertetes Spiel</translation>
+ </message>
+ <message>
+ <source>Open…</source>
+ <translation type="vanished">Öffnen …</translation>
+ </message>
+ <message>
+ <source>Play</source>
+ <translation type="vanished">Spielen</translation>
+ </message>
+ <message>
+ <source>Play Move</source>
+ <extracomment>Play a single move</extracomment>
+ <translation type="vanished">Zug spielen</translation>
+ </message>
+ <message>
+ <source>Quit</source>
+ <translation type="vanished">Beenden</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation type="vanished">Speichern</translation>
+ </message>
+ <message>
+ <source>Save As…</source>
+ <translation type="vanished">Speichern unter …</translation>
+ </message>
+ <message>
+ <source>Stop</source>
+ <translation type="vanished">Stopp</translation>
+ </message>
+ <message>
+ <source>Undo Move</source>
+ <translation type="vanished">Zug rückgängig</translation>
+ </message>
+ <message>
+ <source>Game Info</source>
+ <translation type="vanished">Spielinformation</translation>
+ </message>
+ <message>
+ <source>Comment</source>
+ <translation type="vanished">Kommentar</translation>
+ </message>
+ <message>
+ <source>Settings</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation type="vanished">Einstellungen</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeDialog</name>
+ <message>
+ <source>Analysis speed:</source>
+ <translation>Analysegeschwindigkeit:</translation>
+ </message>
+ <message>
+ <source>Fast</source>
+ <translation>Schnell</translation>
+ </message>
+ <message>
+ <source>Normal</source>
+ <translation>Normal</translation>
+ </message>
+ <message>
+ <source>Slow</source>
+ <translation>Langsam</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeGame</name>
+ <message>
+ <source>(No analysis)</source>
+ <translation>(Keine Analyse)</translation>
+ </message>
+</context>
+<context>
+ <name>AppearanceDialog</name>
+ <message>
+ <source>Coordinates</source>
+ <translation>Koordinaten</translation>
+ </message>
+ <message>
+ <source>Show variations</source>
+ <translation>Varianten zeigen</translation>
+ </message>
+ <message>
+ <source>Light</source>
+ <translation>Hell</translation>
+ </message>
+ <message>
+ <source>Dark</source>
+ <translation>Dunkel</translation>
+ </message>
+ <message>
+ <source>Colorblind light</source>
+ <translation>Farbenblind hell</translation>
+ </message>
+ <message>
+ <source>Colorblind dark</source>
+ <translation>Farbenblind dunkel</translation>
+ </message>
+ <message>
+ <source>System</source>
+ <extracomment>Name of theme using default system colors</extracomment>
+ <translation>System</translation>
+ </message>
+ <message>
+ <source>Move marking:</source>
+ <translation>Zugmarkierung:</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>All with number</source>
+ <translation>Alle mit Nummer</translation>
+ </message>
+ <message>
+ <source>None</source>
+ <extracomment>Move marking/None</extracomment>
+ <translation>Keine</translation>
+ </message>
+ <message>
+ <source>Animations</source>
+ <translation>Animationen</translation>
+ </message>
+ <message>
+ <source>Show comment:</source>
+ <translation>Kommentar zeigen:</translation>
+ </message>
+ <message>
+ <source>Always</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Immer</translation>
+ </message>
+ <message>
+ <source>As needed</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Bei Bedarf</translation>
+ </message>
+ <message>
+ <source>Never</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Nie</translation>
+ </message>
+ <message>
+ <source>Color theme:</source>
+ <translation>Farbthema:</translation>
+ </message>
+ <message>
+ <source>Move number</source>
+ <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+ <translation>Zugnummer</translation>
+ </message>
+</context>
+<context>
+ <name>AsciiArtSaveDialog</name>
+ <message>
+ <source>Export ASCII Art</source>
+ <translation>ASCII-Art exportieren</translation>
+ </message>
+ <message>
+ <source>Text files</source>
+ <translation>Textdateien</translation>
+ </message>
+</context>
+<context>
+ <name>BoardContextMenu</name>
+ <message>
+ <source>Go to Move %1</source>
+ <translation>Gehe zu Zug %1</translation>
+ </message>
+ <message>
+ <source>Move Annotation</source>
+ <translation>Annotierung</translation>
+ </message>
+ <message>
+ <source>Move Annotation (%1)</source>
+ <extracomment>The argument is the annotation symbol for the current move</extracomment>
+ <translation>Annotierung (%1)</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonApply</name>
+ <message>
+ <source>Apply</source>
+ <translation>Anwenden</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonCancel</name>
+ <message>
+ <source>Cancel</source>
+ <translation>Abbrechen</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonClose</name>
+ <message>
+ <source>Close</source>
+ <translation>Schließen</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonOk</name>
+ <message>
+ <source>OK</source>
+ <translation>OK</translation>
+ </message>
+</context>
+<context>
+ <name>ComputerDialog</name>
+ <message>
+ <source>Computer plays:</source>
+ <translation>Computer spielt:</translation>
+ </message>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Blau/Rot</translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation>Lila</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Grün</translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation>Blau</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Gelb/Grün</translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation>Orange</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Gelb</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rot</translation>
+ </message>
+ <message>
+ <source>Level %1</source>
+ <translation>Stufe %1</translation>
+ </message>
+</context>
+<context>
+ <name>ExportImageDialog</name>
+ <message>
+ <source>Image width:</source>
+ <translation>Bildbreite:</translation>
+ </message>
+</context>
+<context>
+ <name>FileDialog</name>
+ <message>
+ <source>Overwrite file?</source>
+ <translation>Datei überschreiben?</translation>
+ </message>
+ <message>
+ <source>Open</source>
+ <translation>Öffnen</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation>Speichern</translation>
+ </message>
+ <message>
+ <source>All files</source>
+ <translation>Alle Dateien</translation>
+ </message>
+</context>
+<context>
+ <name>GameDisplayDesktop</name>
+ <message>
+ <source>Computer is thinking…</source>
+ <translation>Computer denkt …</translation>
+ </message>
+ <message>
+ <source>Running game analysis…</source>
+ <translation>Spiel wird analysiert …</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>
+</context>
+<context>
+ <name>GameInfoDialog</name>
+ <message>
+ <source>Player Blue/Red:</source>
+ <translation>Spieler Blau/Rot:</translation>
+ </message>
+ <message>
+ <source>Player Purple:</source>
+ <translation>Spieler Lila:</translation>
+ </message>
+ <message>
+ <source>Player Green:</source>
+ <translation>Spieler Grün:</translation>
+ </message>
+ <message>
+ <source>Player Blue:</source>
+ <translation>Spieler Blau:</translation>
+ </message>
+ <message>
+ <source>Player Yellow/Green:</source>
+ <translation>Spieler Gelb/Grün:</translation>
+ </message>
+ <message>
+ <source>Player Orange:</source>
+ <translation>Spieler Orange:</translation>
+ </message>
+ <message>
+ <source>Player Yellow:</source>
+ <translation>Spieler Gelb:</translation>
+ </message>
+ <message>
+ <source>Player Red:</source>
+ <translation>Spieler Rot:</translation>
+ </message>
+ <message>
+ <source>Date:</source>
+ <translation>Datum:</translation>
+ </message>
+ <message>
+ <source>Time:</source>
+ <translation>Bedenkzeit:</translation>
+ </message>
+ <message>
+ <source>Event:</source>
+ <translation>Veranstaltung:</translation>
+ </message>
+ <message>
+ <source>Round:</source>
+ <translation>Runde:</translation>
+ </message>
+</context>
+<context>
+ <name>GameModel</name>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Blau/Rot</translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation>Lila</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Grün</translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation>Blau</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Gelb/Grün</translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation>Orange</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Gelb</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rot</translation>
+ </message>
+ <message>
+ <source>Purple wins with 1 point.</source>
+ <translation>Lila gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Purple wins with %L1 points.</source>
+ <translation>Lila gewinnt mit %L1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Orange wins with 1 point.</source>
+ <translation>Orange gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Orange wins with %L1 points.</source>
+ <translation>Orange gewinnt mit %L1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie.</source>
+ <translation>Spiel endet unentschieden.</translation>
+ </message>
+ <message>
+ <source>Green wins with 1 point.</source>
+ <translation>Grün gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Green wins with %L1 points.</source>
+ <translation>Grün gewinnt mit %L1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Blue wins with 1 point.</source>
+ <translation>Blau gewinnt mit 1 Punkt.</translation>
+ </message>
+ <message>
+ <source>Blue wins with %L1 points.</source>
+ <translation>Blau gewinnt mit %L1 Punkten.</translation>
+ </message>
+ <message>
+ <source>Green wins (tie resolved).</source>
+ <translation>Grün gewinnt (Unentschieden aufgelöst).</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 %L1 points.</source>
+ <translation>Blau/Rot gewinnt mit %L1 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 %L1 points.</source>
+ <translation>Gelb/Grün gewinnt mit %L1 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 unentschieden zwischen Blau und Gelb.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Red.</source>
+ <translation>Spiel endet unentschieden zwischen Blau und Rot.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow and Red.</source>
+ <translation>Spiel endet unentschieden zwischen Gelb und Rot.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between all players.</source>
+ <translation>Spiel endet 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 unentschieden zwischen Blau, Gelb und Rot.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Green.</source>
+ <translation>Spiel endet 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 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 unentschieden zwischen Gelb, Rot und Grün.</translation>
+ </message>
+ <message>
+ <source>Invalid Blokus SGF file. (%1)</source>
+ <translation>Ungültige Blokus-SGF-Datei. (%1)</translation>
+ </message>
+ <message>
+ <source>Clipboard is empty.</source>
+ <translation>Zwischenablage ist leer.</translation>
+ </message>
+ <message>
+ <source>Untitled</source>
+ <translation>Unbenannt</translation>
+ </message>
+ <message>
+ <source>Untitled %1</source>
+ <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+ <translation>Unbenannt %1</translation>
+ </message>
+ <message>
+ <source>New Folder</source>
+ <translation>Neuer Ordner</translation>
+ </message>
+ <message>
+ <source>New Folder %1</source>
+ <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+ <translation>Neuer Ordner %1</translation>
+ </message>
+ <message>
+ <source>(Setup)</source>
+ <translation>(Stellung)</translation>
+ </message>
+ <message>
+ <source>(No moves)</source>
+ <translation>(Keine Züge)</translation>
+ </message>
+ <message>
+ <source>Move %1</source>
+ <extracomment>The argument is the current move number.</extracomment>
+ <translation>Zug %1</translation>
+ </message>
+ <message>
+ <source>Unsupported character set</source>
+ <translation>Zeichensatz nicht unterstützt</translation>
+ </message>
+</context>
+<context>
+ <name>GameVariantDialog</name>
+ <message>
+ <source>Classic</source>
+ <translation>Klassisch</translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation>Duo</translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation>Junior</translation>
+ </message>
+ <message>
+ <source>Trigon</source>
+ <translation>Trigon</translation>
+ </message>
+ <message>
+ <source>Nexos</source>
+ <translation>Nexos</translation>
+ </message>
+ <message>
+ <source>GembloQ</source>
+ <translation>GembloQ</translation>
+ </message>
+ <message>
+ <source>Callisto</source>
+ <translation>Callisto</translation>
+ </message>
+ <message>
+ <source>Players:</source>
+ <translation>Spieler:</translation>
+ </message>
+ <message>
+ <source>Colors:</source>
+ <translation>Farben:</translation>
+ </message>
+</context>
+<context>
+ <name>GotoMoveDialog</name>
+ <message>
+ <source>Move number:</source>
+ <translation>Zugnummer:</translation>
+ </message>
+</context>
+<context>
+ <name>HelpWindow</name>
+ <message>
+ <source>Pentobi Help</source>
+ <translation>Pentobi-Hilfe</translation>
+ </message>
+</context>
+<context>
+ <name>ImageSaveDialog</name>
+ <message>
+ <source>Save Image</source>
+ <translation>Grafik speichern</translation>
+ </message>
+ <message>
+ <source>PNG image files</source>
+ <translation>PNG-Bilddateien</translation>
+ </message>
+ <message>
+ <source>JPEG image files</source>
+ <translation>JPEG-Bilddateien</translation>
+ </message>
+</context>
+<context>
+ <name>InitialRatingDialog</name>
+ <message>
+ <source>Initialize your rating for this game variant.</source>
+ <translation>Initialisieren Sie Ihre Wertung für diese Spielvariante.</translation>
+ </message>
+ <message>
+ <source>Initial rating:</source>
+ <translation>Anfangswertung:</translation>
+ </message>
+ <message>
+ <source>Beginner</source>
+ <translation>Anfänger</translation>
+ </message>
+ <message>
+ <source>Expert</source>
+ <translation>Experte</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <source>Pentobi</source>
+ <extracomment>Window title if no file is loaded.</extracomment>
+ <translation>Pentobi</translation>
+ </message>
+ <message>
+ <source>Game analysis is only possible in main variation.</source>
+ <translation>Spielanalyse ist nur in Hauptvariante möglich.</translation>
+ </message>
+ <message>
+ <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+ <translation>Automatisch gespeichertes Spiel wurde von einer anderen Instanz von Pentobi geändert. Überschreiben?</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 has decreased from %1 to %2.</source>
+ <translation>Ihre Wertung hat sich von %1 auf %2 verringert.</translation>
+ </message>
+ <message>
+ <source>Your rating stays at %1.</source>
+ <translation>Ihre Wertung bleibt bei %1.</translation>
+ </message>
+ <message>
+ <source>No permission to access storage</source>
+ <translation>Keine Berechtigung zu Zugriff auf Speicher</translation>
+ </message>
+ <message>
+ <source>Delete all rating information for the current game variant?</source>
+ <translation>Alle Wertungsinformationen für die gegenwärtige Spielvariante löschen?</translation>
+ </message>
+ <message>
+ <source>Delete all variations?</source>
+ <translation>Alle Varianten löschen?</translation>
+ </message>
+ <message>
+ <source>Save failed.</source>
+ <translation>Speichern fehlgeschlagen.</translation>
+ </message>
+ <message>
+ <source>End of tree was reached. Continue search from start of the tree?</source>
+ <translation>Ende des Spielbaums erreicht. Suche vom Start des Spielbaums fortsetzen?</translation>
+ </message>
+ <message>
+ <source>No comment found</source>
+ <translation>Kein Kommentar gefunden</translation>
+ </message>
+ <message>
+ <source>%1 (modified)</source>
+ <translation>%1 (geändert)</translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Reload?</source>
+ <translation>Datei wurde von einer anderen Anwendung bearbeitet. Neu laden?</translation>
+ </message>
+ <message>
+ <source>Continue computer move?</source>
+ <translation>Computer-Zug fortsetzen?</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>Open failed.</source>
+ <translation>Öffnen fehlgeschlagen.</translation>
+ </message>
+ <message>
+ <source>Start rated game with Purple against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Lila gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Green against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Grün gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Blau/Rot gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Blau gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Orange against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Orange gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Gelb/Grün gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Gelb gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Red against Pentobi level %1?</source>
+ <translation>Gewertetes Spiel mit Rot gegen Pentobi Stufe %1 beginnen?</translation>
+ </message>
+ <message>
+ <source>You have not yet played rated games in this game variant.</source>
+ <translation>Sie haben noch keine gewerteten Spiele in dieser Spielvariante gespielt.</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>Discard game?</source>
+ <translation>Spiel verwerfen?</translation>
+ </message>
+ <message>
+ <source>Pentobi %1 (level %2)</source>
+ <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+ <translation>Pentobi %1 (Stufe %2)</translation>
+ </message>
+ <message>
+ <source>Human</source>
+ <extracomment>Player name for game info in rated game.</extracomment>
+ <translation>Mensch</translation>
+ </message>
+ <message>
+ <source>Rated game</source>
+ <translation>Gewertetes Spiel</translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Overwrite?</source>
+ <translation>Datei wurde von einer anderen Anwendung bearbeitet. Überschreiben?</translation>
+ </message>
+ <message>
+ <source>%1 - Pentobi</source>
+ <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+ <translation>%1 - Pentobi</translation>
+ </message>
+ <message>
+ <source>Not enough memory</source>
+ <translation>Nicht genügend Speicher</translation>
+ </message>
+ <message>
+ <source>Game analysis aborted</source>
+ <translation>Spielanalyse abgebrochen</translation>
+ </message>
+ <message>
+ <source>Computer move aborted</source>
+ <translation>Computer-Zug abgebrochen</translation>
+ </message>
+ <message>
+ <source>Rating information deleted</source>
+ <translation>Wertungsinformationen gelöscht</translation>
+ </message>
+ <message>
+ <source>Variations deleted</source>
+ <translation>Varianten gelöscht</translation>
+ </message>
+ <message>
+ <source>File saved</source>
+ <translation>Datei gespeichert</translation>
+ </message>
+ <message>
+ <source>Saving image failed or unsupported image format</source>
+ <translation>Grafik konnte nicht gespeichert werden oder Bildformat nicht unterstützt</translation>
+ </message>
+ <message>
+ <source>Image saved</source>
+ <translation>Grafik gespeichert</translation>
+ </message>
+ <message>
+ <source>Creating image failed</source>
+ <translation>Grafik konnte nicht erzeugt werden</translation>
+ </message>
+ <message>
+ <source>Continuing rated game</source>
+ <translation>Gewertetes Spiel wird fortgesetzt</translation>
+ </message>
+ <message>
+ <source>Kept only position</source>
+ <translation>Nur Brettstellung behalten</translation>
+ </message>
+ <message>
+ <source>Kept only subtree</source>
+ <translation>Nur Teilbaum behalten</translation>
+ </message>
+ <message>
+ <source>Variation is now %1</source>
+ <translation>Variante ist jetzt %1</translation>
+ </message>
+ <message>
+ <source>Children truncated</source>
+ <translation>Kindknoten abgeschnitten</translation>
+ </message>
+ <message>
+ <source>Setup</source>
+ <extracomment>Small-screen label for setup mode (short for "Setup Mode").</extracomment>
+ <translation>Aufbau</translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation>Stellungsaufbau</translation>
+ </message>
+ <message>
+ <source>Rated</source>
+ <extracomment>Label for ongoing rated game</extracomment>
+ <translation>Gewertet</translation>
+ </message>
+ <message>
+ <source>Rated %1</source>
+ <extracomment>Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number.</extracomment>
+ <translation>Gewertet %1</translation>
+ </message>
+ <message>
+ <source>Rated Game %1</source>
+ <extracomment>Label for rated game. The argument is the game number.</extracomment>
+ <translation>Gewertetes Spiel %1</translation>
+ </message>
+ <message>
+ <source>Main Variation</source>
+ <translation>Hauptvariante</translation>
+ </message>
+ <message>
+ <source>Beginning of Branch</source>
+ <translation>Verzweigungsanfang</translation>
+ </message>
+ <message>
+ <source>Comment</source>
+ <translation>Kommentar</translation>
+ </message>
+ <message>
+ <source>Settings</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation>Einstellungen</translation>
+ </message>
+ <message>
+ <source>Find Move</source>
+ <translation>Zug finden</translation>
+ </message>
+ <message>
+ <source>Next Comment</source>
+ <translation>Nächster Kommentar</translation>
+ </message>
+ <message>
+ <source>Fullscreen</source>
+ <translation>Vollbild</translation>
+ </message>
+ <message>
+ <source>Game Info</source>
+ <translation>Spielinformation</translation>
+ </message>
+ <message>
+ <source>Move Number…</source>
+ <translation>Zugnummer …</translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation>Pentobi-Hilfe</translation>
+ </message>
+ <message>
+ <source>New</source>
+ <translation>Neu</translation>
+ </message>
+ <message>
+ <source>Rated Game</source>
+ <translation>Gewertetes Spiel</translation>
+ </message>
+ <message>
+ <source>Open…</source>
+ <translation>Öffnen …</translation>
+ </message>
+ <message>
+ <source>Play</source>
+ <translation>Spielen</translation>
+ </message>
+ <message>
+ <source>Play Move</source>
+ <extracomment>Play a single move</extracomment>
+ <translation>Zug spielen</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>Stop</source>
+ <translation>Stopp</translation>
+ </message>
+ <message>
+ <source>Undo Move</source>
+ <translation>Zug rückgängig</translation>
+ </message>
+</context>
+<context>
+ <name>MenuComputer</name>
+ <message>
+ <source>Computer</source>
+ <translation>Computer</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+ <translation>Z</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+</context>
+<context>
+ <name>MenuEdit</name>
+ <message>
+ <source>Edit</source>
+ <translation>Bearbeiten</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation>H</translation>
+ </message>
+ <message>
+ <source>Make Main Variation</source>
+ <translation>Zu Hauptvariante machen</translation>
+ </message>
+ <message>
+ <source>Variation Up</source>
+ <extracomment>Short for Move Variation Up</extracomment>
+ <translation>Variante nach oben</translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>Variation Down</source>
+ <extracomment>Short for Move Variation Down</extracomment>
+ <translation>Variante nach unten</translation>
+ </message>
+ <message>
+ <source>W</source>
+ <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <source>Delete Variations</source>
+ <translation>Varianten löschen</translation>
+ </message>
+ <message>
+ <source>D</source>
+ <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+ <translation>V</translation>
+ </message>
+ <message>
+ <source>Truncate</source>
+ <translation>Abschneiden</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Truncate Children</source>
+ <translation>Kindknoten abschneiden</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+ <translation>K</translation>
+ </message>
+ <message>
+ <source>Keep Position</source>
+ <translation>Brettstellung behalten</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+ <message>
+ <source>Keep Subtree</source>
+ <translation>Teilbaum behalten</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+ <translation>T</translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation>Stellungsaufbau</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>Next Color</source>
+ <translation>Nächste Farbe</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>Annotation…</source>
+ <translation>Annotierung …</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>Made main variation</source>
+ <translation>Zu Hauptvariante gemacht</translation>
+ </message>
+</context>
+<context>
+ <name>MenuExport</name>
+ <message>
+ <source>Export</source>
+ <translation>Exportieren</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+ <translation>G</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Image…</source>
+ <translation>Grafik …</translation>
+ </message>
+ <message>
+ <source>ASCII Art…</source>
+ <translation>ASCII-Art …</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGame</name>
+ <message>
+ <source>Game</source>
+ <translation>Spiel</translation>
+ </message>
+ <message>
+ <source>G</source>
+ <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+ <translation>W</translation>
+ </message>
+ <message>
+ <source>Game Variant…</source>
+ <translation>Spielvariante …</translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+ <translation>V</translation>
+ </message>
+ <message>
+ <source>I</source>
+ <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+ <translation>R</translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+ <translation>Z</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+ <translation>f</translation>
+ </message>
+ <message>
+ <source>Open Clipboard</source>
+ <translation>Zwischenablage öffnen</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <source>Q</source>
+ <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGo</name>
+ <message>
+ <source>Go</source>
+ <translation>Gehe zu</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+ <translation>G</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation>H</translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+ <translation>V</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+ <translation>K</translation>
+ </message>
+</context>
+<context>
+ <name>MenuHelp</name>
+ <message>
+ <source>Help</source>
+ <translation>Hilfe</translation>
+ </message>
+ <message>
+ <source>H</source>
+ <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+ <translation>H</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>About Pentobi</source>
+ <translation>Über Pentobi</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+ <message>
+ <source>Report Bug</source>
+ <translation>Fehler melden</translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+</context>
+<context>
+ <name>MenuItem</name>
+ <message>
+ <source>Ctrl</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation>Strg</translation>
+ </message>
+ <message>
+ <source>Shift</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation>Umschalt</translation>
+ </message>
+</context>
+<context>
+ <name>MenuRecentFiles</name>
+ <message>
+ <source>Open Recent</source>
+ <translation>Zuletzt benutzt</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+ <translation>T</translation>
+ </message>
+ <message>
+ <source>%1. %2</source>
+ <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+ <translation>%1. %2</translation>
+ </message>
+ <message>
+ <source>Clear List</source>
+ <extracomment>Menu item for clearing the recent files list</extracomment>
+ <translation>Liste leeren</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+ <translation>L</translation>
+ </message>
+</context>
+<context>
+ <name>MenuTools</name>
+ <message>
+ <source>Tools</source>
+ <translation>Extras</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+ <translation>X</translation>
+ </message>
+ <message>
+ <source>Rating</source>
+ <translation>Wertung</translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+ <translation>W</translation>
+ </message>
+ <message>
+ <source>Clear Rating</source>
+ <translation>Wertung löschen</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+ <translation>L</translation>
+ </message>
+ <message>
+ <source>Analyze Game</source>
+ <translation>Spiel analysieren</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Clear Analysis</source>
+ <translation>Analyse löschen</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+ <message>
+ <source>Analyze Game…</source>
+ <translation>Spiel analysieren …</translation>
+ </message>
+</context>
+<context>
+ <name>MenuView</name>
+ <message>
+ <source>View</source>
+ <translation>Ansicht</translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Appearance…</source>
+ <translation type="vanished">Erscheinungsbild …</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+ <translation>V</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+ <translation>K</translation>
+ </message>
+ <message>
+ <source>Appearance</source>
+ <translation>Erscheinungsbild</translation>
+ </message>
+ <message>
+ <source>Toolbar</source>
+ <translation>Werkzeugleiste</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+ <translation>W</translation>
+ </message>
+</context>
+<context>
+ <name>MoveAnnotationDialog</name>
+ <message>
+ <source>Move %1</source>
+ <translation>Zug %1</translation>
+ </message>
+ <message>
+ <source>Very good</source>
+ <translation>Sehr gut</translation>
+ </message>
+ <message>
+ <source>Good</source>
+ <translation>Gut</translation>
+ </message>
+ <message>
+ <source>Interesting</source>
+ <translation>Interessant</translation>
+ </message>
+ <message>
+ <source>Doubtful</source>
+ <translation>Zweifelhaft</translation>
+ </message>
+ <message>
+ <source>Bad</source>
+ <translation>Schlecht</translation>
+ </message>
+ <message>
+ <source>Very Bad</source>
+ <translation>Sehr schlecht</translation>
+ </message>
+ <message>
+ <source>No annotation</source>
+ <translation>Keine Annotierung</translation>
+ </message>
+</context>
+<context>
+ <name>NewFolderDialog</name>
+ <message>
+ <source>Folder name:</source>
+ <translation>Name des Ordners:</translation>
+ </message>
+</context>
+<context>
+ <name>OpenDialog</name>
+ <message>
+ <source>Open</source>
+ <translation>Öffnen</translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation>Blokus-Partien</translation>
+ </message>
+</context>
+<context>
+ <name>RatingDialog</name>
+ <message>
+ <source>Your rating:</source>
+ <translation>Ihre Wertung:</translation>
+ </message>
+ <message>
+ <source>Game variant:</source>
+ <translation>Spielvariante:</translation>
+ </message>
+ <message>
+ <source>Classic (2)</source>
+ <extracomment>Short for Classic (2 players)</extracomment>
+ <translation>Klassisch (2)</translation>
+ </message>
+ <message>
+ <source>Classic (3)</source>
+ <extracomment>Short for Classic (3 players)</extracomment>
+ <translation>Klassisch (3)</translation>
+ </message>
+ <message>
+ <source>Classic (4)</source>
+ <extracomment>Short for Classic (4 players)</extracomment>
+ <translation>Klassisch (4)</translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation>Duo</translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation>Junior</translation>
+ </message>
+ <message>
+ <source>Trigon (2)</source>
+ <extracomment>Short for Trigon (2 players)</extracomment>
+ <translation>Trigon (2)</translation>
+ </message>
+ <message>
+ <source>Trigon (3)</source>
+ <extracomment>Short for Trigon (3 players)</extracomment>
+ <translation>Trigon (3)</translation>
+ </message>
+ <message>
+ <source>Trigon (4)</source>
+ <extracomment>Short for Trigon (4 players)</extracomment>
+ <translation>Trigon (4)</translation>
+ </message>
+ <message>
+ <source>Nexos (2)</source>
+ <extracomment>Short for Nexos (2 players)</extracomment>
+ <translation>Nexos (2)</translation>
+ </message>
+ <message>
+ <source>Nexos (4)</source>
+ <extracomment>Short for Nexos (4 players)</extracomment>
+ <translation>Nexos (4)</translation>
+ </message>
+ <message>
+ <source>Callisto (2)</source>
+ <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+ <translation>Callisto (2)</translation>
+ </message>
+ <message>
+ <source>Callisto (2/4)</source>
+ <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+ <translation>Callisto (2/4)</translation>
+ </message>
+ <message>
+ <source>Callisto (3)</source>
+ <extracomment>Short for Callisto (3 players)</extracomment>
+ <translation>Callisto (3)</translation>
+ </message>
+ <message>
+ <source>Callisto (4)</source>
+ <extracomment>Short for Callisto (4 players)</extracomment>
+ <translation>Callisto (4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (4)</source>
+ <extracomment>Short for GembloQ (4 players)</extracomment>
+ <translation>GembloQ (4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (2)</source>
+ <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+ <translation>GembloQ (2)</translation>
+ </message>
+ <message>
+ <source>GembloQ (2/4)</source>
+ <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+ <translation>GembloQ (2/4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (3)</source>
+ <extracomment>Short for GembloQ (3 players)</extracomment>
+ <translation>GembloQ (3)</translation>
+ </message>
+ <message>
+ <source>Rated games:</source>
+ <translation>Gewertete Spiele:</translation>
+ </message>
+ <message>
+ <source>Best previous rating:</source>
+ <translation>Beste frühere Wertung:</translation>
+ </message>
+ <message>
+ <source>Recent development:</source>
+ <translation>Aktuelle Entwicklung:</translation>
+ </message>
+ <message>
+ <source>Game</source>
+ <translation>Spiel</translation>
+ </message>
+ <message>
+ <source>Result</source>
+ <translation>Ergebnis</translation>
+ </message>
+ <message>
+ <source>Win</source>
+ <extracomment>Result of rated game is a win</extracomment>
+ <translation>Gewinn</translation>
+ </message>
+ <message>
+ <source>Loss</source>
+ <extracomment>Result of rated game is a loss</extracomment>
+ <translation>Verlust</translation>
+ </message>
+ <message>
+ <source>Tie</source>
+ <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+ <translation>Unentsch.</translation>
+ </message>
+ <message>
+ <source>Level</source>
+ <translation>Stufe</translation>
+ </message>
+ <message>
+ <source>Your Color</source>
+ <translation>Ihre Farbe</translation>
+ </message>
+ <message>
+ <source>Date</source>
+ <translation>Datum</translation>
+ </message>
+ <message>
+ <source>Open Game %1</source>
+ <translation>Spiel %1 öffnen</translation>
+ </message>
+</context>
+<context>
+ <name>SaveDialog</name>
+ <message>
+ <source>Save</source>
+ <translation>Speichern</translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation>Blokus-Partien</translation>
+ </message>
+</context>
+<context>
+ <name>ToolBar</name>
+ <message>
+ <source>Start a new game</source>
+ <translation>Ein neues Spiel beginnen</translation>
+ </message>
+ <message>
+ <source>Start a rated game</source>
+ <translation>Ein gewertetes Spiel beginnen</translation>
+ </message>
+ <message>
+ <source>Set the colors played by the computer</source>
+ <translation>Die vom Computer gespielten Farben festlegen</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>Go to beginning of game</source>
+ <translation>Zum Anfang des Spiels gehen</translation>
+ </message>
+ <message>
+ <source>Go ten moves backward</source>
+ <translation>Zehn Züge zurück gehen</translation>
+ </message>
+ <message>
+ <source>Go one move backward</source>
+ <translation>Einen Zug zurück gehen</translation>
+ </message>
+ <message>
+ <source>Go one move forward</source>
+ <translation>Einen Zug vorwärts gehen</translation>
+ </message>
+ <message>
+ <source>Go ten moves forward</source>
+ <translation>Zehn Züge vorwärts gehen</translation>
+ </message>
+ <message>
+ <source>Go to end of moves</source>
+ <translation>Zum Ende der Züge gehen</translation>
+ </message>
+ <message>
+ <source>Go to previous variation</source>
+ <translation>Zur vorherigen Variante gehen</translation>
+ </message>
+ <message>
+ <source>Go to next variation</source>
+ <translation>Zur nächsten Variante gehen</translation>
+ </message>
+ <message>
+ <source>Abort game analysis</source>
+ <translation>Spielanalyse abbrechen</translation>
+ </message>
+ <message>
+ <source>Abort computer move</source>
+ <translation>Computer-Zug abbrechen</translation>
+ </message>
+ <message>
+ <source>Undo move</source>
+ <extracomment>Tooltip for Undo button</extracomment>
+ <translation>Zug rückgängig</translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <source>Copyright © 2011–%1 Markus Enzenberger</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Computer opponent for the board game Blokus</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Pentobi %1</source>
+ <extracomment>The argument is the application version.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeDialog</name>
+ <message>
+ <source>Analysis speed:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Fast</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Normal</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Slow</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeGame</name>
+ <message>
+ <source>(No analysis)</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>AppearanceDialog</name>
+ <message>
+ <source>Coordinates</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Show variations</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Light</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Dark</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Colorblind light</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Colorblind dark</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>System</source>
+ <extracomment>Name of theme using default system colors</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Move marking:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Last with dot</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Last with number</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>All with number</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>None</source>
+ <extracomment>Move marking/None</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Animations</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Show comment:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Always</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>As needed</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Never</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Color theme:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Move number</source>
+ <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>AsciiArtSaveDialog</name>
+ <message>
+ <source>Export ASCII Art</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Text files</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>BoardContextMenu</name>
+ <message>
+ <source>Go to Move %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Move Annotation</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Move Annotation (%1)</source>
+ <extracomment>The argument is the annotation symbol for the current move</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ButtonApply</name>
+ <message>
+ <source>Apply</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ButtonCancel</name>
+ <message>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ButtonClose</name>
+ <message>
+ <source>Close</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ButtonOk</name>
+ <message>
+ <source>OK</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ComputerDialog</name>
+ <message>
+ <source>Computer plays:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue/Red</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Level %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ExportImageDialog</name>
+ <message>
+ <source>Image width:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FileDialog</name>
+ <message>
+ <source>Overwrite file?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Open</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>All files</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>GameDisplayDesktop</name>
+ <message>
+ <source>Computer is thinking…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Running game analysis…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Computer is thinking… (up to %1 seconds remaining)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Computer is thinking… (up to %1 minutes remaining)</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>GameInfoDialog</name>
+ <message>
+ <source>Player Blue/Red:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Player Purple:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Player Green:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Player Blue:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Player Yellow/Green:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Player Orange:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Player Yellow:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Player Red:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Date:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Time:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Event:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Round:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>GameModel</name>
+ <message>
+ <source>Blue/Red</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Purple wins with 1 point.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Purple wins with %L1 points.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Orange wins with 1 point.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Orange wins with %L1 points.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Green wins with 1 point.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Green wins with %L1 points.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue wins with 1 point.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue wins with %L1 points.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Green wins (tie resolved).</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with 1 point.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with %L1 points.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with 1 point.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with %L1 points.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins (tie resolved).</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blue wins.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow wins.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Red wins.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Red wins (tie resolved).</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Yellow wins (tie resolved).</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Yellow.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Red.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow and Red.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between all players.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Green wins.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Red.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Green.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Red and Green.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow, Red and Green.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Invalid Blokus SGF file. (%1)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clipboard is empty.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Untitled</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Untitled %1</source>
+ <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>New Folder</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>New Folder %1</source>
+ <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>(Setup)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>(No moves)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Move %1</source>
+ <extracomment>The argument is the current move number.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Unsupported character set</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>GameVariantDialog</name>
+ <message>
+ <source>Classic</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Trigon</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Nexos</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>GembloQ</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Callisto</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Players:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Colors:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>GotoMoveDialog</name>
+ <message>
+ <source>Move number:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>HelpWindow</name>
+ <message>
+ <source>Pentobi Help</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ImageSaveDialog</name>
+ <message>
+ <source>Save Image</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>PNG image files</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>JPEG image files</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>InitialRatingDialog</name>
+ <message>
+ <source>Initialize your rating for this game variant.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Initial rating:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Beginner</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Expert</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <source>Pentobi</source>
+ <extracomment>Window title if no file is loaded.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game analysis is only possible in main variation.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Your rating has increased from %1 to %2.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Your rating has decreased from %1 to %2.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Your rating stays at %1.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>No permission to access storage</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Delete all rating information for the current game variant?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Delete all variations?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Save failed.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>End of tree was reached. Continue search from start of the tree?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>No comment found</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 (modified)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Reload?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Continue computer move?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Keep only position?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Keep only subtree?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Open failed.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Purple against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Green against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Orange against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start rated game with Red against Pentobi level %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>You have not yet played rated games in this game variant.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Truncate this subtree?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Truncate children?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Discard game?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Pentobi %1 (level %2)</source>
+ <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Human</source>
+ <extracomment>Player name for game info in rated game.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rated game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Overwrite?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1 - Pentobi</source>
+ <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Not enough memory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game analysis aborted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Computer move aborted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rating information deleted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Variations deleted</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>File saved</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Saving image failed or unsupported image format</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Image saved</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Creating image failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Continuing rated game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Kept only position</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Kept only subtree</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Variation is now %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Children truncated</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Setup</source>
+ <extracomment>Small-screen label for setup mode (short for "Setup Mode").</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rated</source>
+ <extracomment>Label for ongoing rated game</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rated %1</source>
+ <extracomment>Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rated Game %1</source>
+ <extracomment>Label for rated game. The argument is the game number.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Main Variation</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Beginning of Branch</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Comment</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Settings</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Find Move</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Next Comment</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Fullscreen</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game Info</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Move Number…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>New</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rated Game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Open…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Play</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Play Move</source>
+ <extracomment>Play a single move</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Quit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Save As…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Stop</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Undo Move</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuComputer</name>
+ <message>
+ <source>Computer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuEdit</name>
+ <message>
+ <source>Edit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Make Main Variation</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Variation Up</source>
+ <extracomment>Short for Move Variation Up</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Variation Down</source>
+ <extracomment>Short for Move Variation Down</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>W</source>
+ <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Delete Variations</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>D</source>
+ <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Truncate</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Truncate Children</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Keep Position</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Keep Subtree</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Next Color</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Annotation…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Made main variation</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuExport</name>
+ <message>
+ <source>Export</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Image…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>ASCII Art…</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuGame</name>
+ <message>
+ <source>Game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>G</source>
+ <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game Variant…</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>I</source>
+ <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Open Clipboard</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Q</source>
+ <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuGo</name>
+ <message>
+ <source>Go</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuHelp</name>
+ <message>
+ <source>Help</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>H</source>
+ <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>About Pentobi</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Report Bug</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuItem</name>
+ <message>
+ <source>Ctrl</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Shift</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuRecentFiles</name>
+ <message>
+ <source>Open Recent</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>%1. %2</source>
+ <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clear List</source>
+ <extracomment>Menu item for clearing the recent files list</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuTools</name>
+ <message>
+ <source>Tools</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rating</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clear Rating</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Analyze Game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Clear Analysis</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Analyze Game…</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MenuView</name>
+ <message>
+ <source>View</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Appearance</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Toolbar</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MoveAnnotationDialog</name>
+ <message>
+ <source>Move %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Very good</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Good</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Interesting</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Doubtful</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Bad</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Very Bad</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>No annotation</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>NewFolderDialog</name>
+ <message>
+ <source>Folder name:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>OpenDialog</name>
+ <message>
+ <source>Open</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>RatingDialog</name>
+ <message>
+ <source>Your rating:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game variant:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Classic (2)</source>
+ <extracomment>Short for Classic (2 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Classic (3)</source>
+ <extracomment>Short for Classic (3 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Classic (4)</source>
+ <extracomment>Short for Classic (4 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Trigon (2)</source>
+ <extracomment>Short for Trigon (2 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Trigon (3)</source>
+ <extracomment>Short for Trigon (3 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Trigon (4)</source>
+ <extracomment>Short for Trigon (4 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Nexos (2)</source>
+ <extracomment>Short for Nexos (2 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Nexos (4)</source>
+ <extracomment>Short for Nexos (4 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Callisto (2)</source>
+ <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Callisto (2/4)</source>
+ <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Callisto (3)</source>
+ <extracomment>Short for Callisto (3 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Callisto (4)</source>
+ <extracomment>Short for Callisto (4 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>GembloQ (4)</source>
+ <extracomment>Short for GembloQ (4 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>GembloQ (2)</source>
+ <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>GembloQ (2/4)</source>
+ <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>GembloQ (3)</source>
+ <extracomment>Short for GembloQ (3 players)</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Rated games:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Best previous rating:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Recent development:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Result</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Win</source>
+ <extracomment>Result of rated game is a win</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Loss</source>
+ <extracomment>Result of rated game is a loss</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Tie</source>
+ <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Level</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Your Color</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Date</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Open Game %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>SaveDialog</name>
+ <message>
+ <source>Save</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ToolBar</name>
+ <message>
+ <source>Start a new game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Start a rated game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Set the colors played by the computer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Make the computer continue to play the current color</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Make the computer play the current color</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go to beginning of game</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go ten moves backward</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go one move backward</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go one move forward</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go ten moves forward</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go to end of moves</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go to previous variation</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Go to next variation</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Abort game analysis</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Abort computer move</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <source>Undo move</source>
+ <extracomment>Tooltip for Undo button</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fr">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <source>Copyright © 2011–%1 Markus Enzenberger</source>
+ <translation>Copyright © 2011–%1 Markus Enzenberger</translation>
+ </message>
+ <message>
+ <source>Computer opponent for the board game Blokus</source>
+ <translation>Un adversaire d’ordinateur pour le jeu Blokus</translation>
+ </message>
+ <message>
+ <source>Pentobi %1</source>
+ <extracomment>The argument is the application version.</extracomment>
+ <translation>Pentobi %1</translation>
+ </message>
+</context>
+<context>
+ <name>Actions</name>
+ <message>
+ <source>Main Variation</source>
+ <translation type="vanished">Variation principale</translation>
+ </message>
+ <message>
+ <source>Beginning of Branch</source>
+ <translation type="vanished">Au début de la branche</translation>
+ </message>
+ <message>
+ <source>Settings…</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation type="vanished">Configuration…</translation>
+ </message>
+ <message>
+ <source>Find Move</source>
+ <translation type="vanished">Trouver un coup</translation>
+ </message>
+ <message>
+ <source>Next Comment</source>
+ <translation type="vanished">Commentaire suivant</translation>
+ </message>
+ <message>
+ <source>Fullscreen</source>
+ <translation type="vanished">Plein écran</translation>
+ </message>
+ <message>
+ <source>Move Number…</source>
+ <translation type="vanished">Numéro de coup…</translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation type="vanished">Aide de Pentobi</translation>
+ </message>
+ <message>
+ <source>New</source>
+ <translation type="vanished">Nouveau</translation>
+ </message>
+ <message>
+ <source>Rated Game</source>
+ <translation type="vanished">Partie classée</translation>
+ </message>
+ <message>
+ <source>Open…</source>
+ <translation type="vanished">Ouvrir…</translation>
+ </message>
+ <message>
+ <source>Play</source>
+ <translation type="vanished">Jouer</translation>
+ </message>
+ <message>
+ <source>Play Move</source>
+ <extracomment>Play a single move</extracomment>
+ <translation type="vanished">Jouer un coup</translation>
+ </message>
+ <message>
+ <source>Quit</source>
+ <translation type="vanished">Quitter</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation type="vanished">Enregistrer</translation>
+ </message>
+ <message>
+ <source>Save As…</source>
+ <translation type="vanished">Enregistrer sous…</translation>
+ </message>
+ <message>
+ <source>Stop</source>
+ <translation type="vanished">Arrêter</translation>
+ </message>
+ <message>
+ <source>Undo Move</source>
+ <translation type="vanished">Annuler le coup</translation>
+ </message>
+ <message>
+ <source>Game Info</source>
+ <translation type="vanished">Info sur la partie</translation>
+ </message>
+ <message>
+ <source>Comment</source>
+ <translation type="vanished">Commentaire</translation>
+ </message>
+ <message>
+ <source>Settings</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation type="vanished">Configuration</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeDialog</name>
+ <message>
+ <source>Analysis speed:</source>
+ <translation>Vitesse d’analyse :</translation>
+ </message>
+ <message>
+ <source>Fast</source>
+ <translation>Rapide</translation>
+ </message>
+ <message>
+ <source>Normal</source>
+ <translation>Normal</translation>
+ </message>
+ <message>
+ <source>Slow</source>
+ <translation>Lente</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeGame</name>
+ <message>
+ <source>(No analysis)</source>
+ <translation>(Pas d’analyse)</translation>
+ </message>
+</context>
+<context>
+ <name>AppearanceDialog</name>
+ <message>
+ <source>Coordinates</source>
+ <translation>Coordonnées</translation>
+ </message>
+ <message>
+ <source>Show variations</source>
+ <translation>Afficher les variations</translation>
+ </message>
+ <message>
+ <source>Light</source>
+ <translation>Clair</translation>
+ </message>
+ <message>
+ <source>Dark</source>
+ <translation>Noir</translation>
+ </message>
+ <message>
+ <source>Colorblind light</source>
+ <translation>Daltonien clair</translation>
+ </message>
+ <message>
+ <source>Colorblind dark</source>
+ <translation>Daltonien noir</translation>
+ </message>
+ <message>
+ <source>System</source>
+ <extracomment>Name of theme using default system colors</extracomment>
+ <translation>Système</translation>
+ </message>
+ <message>
+ <source>Move marking:</source>
+ <translation>Marquage de coups :</translation>
+ </message>
+ <message>
+ <source>Last with dot</source>
+ <translation>Dernier avec point</translation>
+ </message>
+ <message>
+ <source>Last with number</source>
+ <translation>Dernier avec numéro</translation>
+ </message>
+ <message>
+ <source>All with number</source>
+ <translation>Tous avec numéro</translation>
+ </message>
+ <message>
+ <source>None</source>
+ <extracomment>Move marking/None</extracomment>
+ <translation>Aucune</translation>
+ </message>
+ <message>
+ <source>Animations</source>
+ <translation>Animations</translation>
+ </message>
+ <message>
+ <source>Show comment:</source>
+ <translation>Afficher le commentaire :</translation>
+ </message>
+ <message>
+ <source>Always</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Toujours</translation>
+ </message>
+ <message>
+ <source>As needed</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Comme requis</translation>
+ </message>
+ <message>
+ <source>Never</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Jamais</translation>
+ </message>
+ <message>
+ <source>Color theme:</source>
+ <translation>Thème de couleur :</translation>
+ </message>
+ <message>
+ <source>Move number</source>
+ <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+ <translation>Numéro de coup</translation>
+ </message>
+</context>
+<context>
+ <name>AsciiArtSaveDialog</name>
+ <message>
+ <source>Export ASCII Art</source>
+ <translation>Exporter art ASCII</translation>
+ </message>
+ <message>
+ <source>Text files</source>
+ <translation>Fichiers texte</translation>
+ </message>
+</context>
+<context>
+ <name>BoardContextMenu</name>
+ <message>
+ <source>Go to Move %1</source>
+ <translation>Aller au coup %1</translation>
+ </message>
+ <message>
+ <source>Move Annotation</source>
+ <translation>Annotation courante</translation>
+ </message>
+ <message>
+ <source>Move Annotation (%1)</source>
+ <extracomment>The argument is the annotation symbol for the current move</extracomment>
+ <translation>Annotation courante (%1)</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonApply</name>
+ <message>
+ <source>Apply</source>
+ <translation>Appliquer</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonCancel</name>
+ <message>
+ <source>Cancel</source>
+ <translation>Annuler</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonClose</name>
+ <message>
+ <source>Close</source>
+ <translation>Fermer</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonOk</name>
+ <message>
+ <source>OK</source>
+ <translation>OK</translation>
+ </message>
+</context>
+<context>
+ <name>ComputerDialog</name>
+ <message>
+ <source>Computer plays:</source>
+ <translation>L’ordinateur joue :</translation>
+ </message>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Bleu/rouge</translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation>Violet</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Vert</translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation>Bleu</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Jaune/vert</translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation>Orange</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Jaune</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rouge</translation>
+ </message>
+ <message>
+ <source>Level %1</source>
+ <translation>Niveau %1</translation>
+ </message>
+</context>
+<context>
+ <name>ExportImageDialog</name>
+ <message>
+ <source>Image width:</source>
+ <translation>Largeur de l’image :</translation>
+ </message>
+</context>
+<context>
+ <name>FileDialog</name>
+ <message>
+ <source>Overwrite file?</source>
+ <translation>Remplacer le fichier ?</translation>
+ </message>
+ <message>
+ <source>Open</source>
+ <translation>Ouvrir</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation>Enregistrer</translation>
+ </message>
+ <message>
+ <source>All files</source>
+ <translation>Tous les fichiers</translation>
+ </message>
+</context>
+<context>
+ <name>GameDisplayDesktop</name>
+ <message>
+ <source>Computer is thinking…</source>
+ <translation>L’ordinateur pense…</translation>
+ </message>
+ <message>
+ <source>Running game analysis…</source>
+ <translation>Exécution de l’analyse de la partie…</translation>
+ </message>
+ <message>
+ <source>Computer is thinking… (up to %1 seconds remaining)</source>
+ <translation>L’ordinateur pense… (jusqu’à %1 secondes restantes)</translation>
+ </message>
+ <message>
+ <source>Computer is thinking… (up to %1 minutes remaining)</source>
+ <translation>L’ordinateur pense… (jusqu’à %1 minutes restantes)</translation>
+ </message>
+</context>
+<context>
+ <name>GameInfoDialog</name>
+ <message>
+ <source>Player Blue/Red:</source>
+ <translation>Joueur bleu/rouge :</translation>
+ </message>
+ <message>
+ <source>Player Purple:</source>
+ <translation>Joueur violet :</translation>
+ </message>
+ <message>
+ <source>Player Green:</source>
+ <translation>Joueur vert :</translation>
+ </message>
+ <message>
+ <source>Player Blue:</source>
+ <translation>Joueur bleu :</translation>
+ </message>
+ <message>
+ <source>Player Yellow/Green:</source>
+ <translation>Joueur jaune/vert :</translation>
+ </message>
+ <message>
+ <source>Player Orange:</source>
+ <translation>Joueur orange :</translation>
+ </message>
+ <message>
+ <source>Player Yellow:</source>
+ <translation>Joueur jaune :</translation>
+ </message>
+ <message>
+ <source>Player Red:</source>
+ <translation>Joueur rouge :</translation>
+ </message>
+ <message>
+ <source>Date:</source>
+ <translation>Date :</translation>
+ </message>
+ <message>
+ <source>Time:</source>
+ <translation>Temps :</translation>
+ </message>
+ <message>
+ <source>Event:</source>
+ <translation>Événement :</translation>
+ </message>
+ <message>
+ <source>Round:</source>
+ <translation>Round :</translation>
+ </message>
+</context>
+<context>
+ <name>GameModel</name>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Bleu/rouge</translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation>Violet</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Vert</translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation>Bleu</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Jaune/vert</translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation>Orange</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Jaune</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rouge</translation>
+ </message>
+ <message>
+ <source>Purple wins with 1 point.</source>
+ <translation>Violet gagne avec 1 point.</translation>
+ </message>
+ <message>
+ <source>Purple wins with %L1 points.</source>
+ <translation>Violet gagne avec %L1 points.</translation>
+ </message>
+ <message>
+ <source>Orange wins with 1 point.</source>
+ <translation>Orange gagne avec 1 point.</translation>
+ </message>
+ <message>
+ <source>Orange wins with %L1 points.</source>
+ <translation>Orange gagne avec %L1 points.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie.</source>
+ <translation>La partie se termine par une égalité.</translation>
+ </message>
+ <message>
+ <source>Green wins with 1 point.</source>
+ <translation>Vert gagne avec 1 point.</translation>
+ </message>
+ <message>
+ <source>Green wins with %L1 points.</source>
+ <translation>Vert gagne avec %L1 points.</translation>
+ </message>
+ <message>
+ <source>Blue wins with 1 point.</source>
+ <translation>Bleu gagne avec 1 point.</translation>
+ </message>
+ <message>
+ <source>Blue wins with %L1 points.</source>
+ <translation>Bleu gagne avec %L1 points.</translation>
+ </message>
+ <message>
+ <source>Green wins (tie resolved).</source>
+ <translation>Vert gagne (égalité résolue).</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with 1 point.</source>
+ <translation>Bleu/rouge gagne avec 1 point.</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with %L1 points.</source>
+ <translation>Bleu/rouge gagne avec %L1 points.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with 1 point.</source>
+ <translation>Jaune/vert gagne avec 1 point.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with %L1 points.</source>
+ <translation>Jaune/vert gagne avec %L1 points.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins (tie resolved).</source>
+ <translation>Jaune/vert gagne (égalité résolue).</translation>
+ </message>
+ <message>
+ <source>Blue wins.</source>
+ <translation>Bleu gagne.</translation>
+ </message>
+ <message>
+ <source>Yellow wins.</source>
+ <translation>Jaune gagne.</translation>
+ </message>
+ <message>
+ <source>Red wins.</source>
+ <translation>Rouge gagne.</translation>
+ </message>
+ <message>
+ <source>Red wins (tie resolved).</source>
+ <translation>Rouge gagne (égalité résolue).</translation>
+ </message>
+ <message>
+ <source>Yellow wins (tie resolved).</source>
+ <translation>Jaune gagne (égalité résolue).</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Yellow.</source>
+ <translation>La partie se termine par une égalité entre bleu et jaune.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Red.</source>
+ <translation>La partie se termine par une égalité entre bleu et rouge.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow and Red.</source>
+ <translation>La partie se termine par une égalité entre jaune et rouge.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between all players.</source>
+ <translation>La partie se termine par une égalité entre tous les joueurs.</translation>
+ </message>
+ <message>
+ <source>Green wins.</source>
+ <translation>Vert gagne.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Red.</source>
+ <translation>La partie se termine par une égalité entre bleu, jaune et rouge.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Green.</source>
+ <translation>La partie se termine par une égalité entre bleu, jaune et vert.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Red and Green.</source>
+ <translation>La partie se termine par une égalité entre bleu, rouge et vert.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow, Red and Green.</source>
+ <translation>La partie se termine par une égalité entre jaune, rouge et vert.</translation>
+ </message>
+ <message>
+ <source>Invalid Blokus SGF file. (%1)</source>
+ <translation>Le fichier n’est pas un fichier Blokus SGF valable. (%1)</translation>
+ </message>
+ <message>
+ <source>Clipboard is empty.</source>
+ <translation>Le presse-papiers est vide.</translation>
+ </message>
+ <message>
+ <source>Untitled</source>
+ <translation>Sans nom</translation>
+ </message>
+ <message>
+ <source>Untitled %1</source>
+ <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+ <translation>Sans nom %1</translation>
+ </message>
+ <message>
+ <source>New Folder</source>
+ <translation>Nouveau dossier</translation>
+ </message>
+ <message>
+ <source>New Folder %1</source>
+ <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+ <translation>Nouveau dossier %1</translation>
+ </message>
+ <message>
+ <source>(Setup)</source>
+ <translation>(Position)</translation>
+ </message>
+ <message>
+ <source>(No moves)</source>
+ <translation>(Pas de coups)</translation>
+ </message>
+ <message>
+ <source>Move %1</source>
+ <extracomment>The argument is the current move number.</extracomment>
+ <translation>Coup %1</translation>
+ </message>
+ <message>
+ <source>Unsupported character set</source>
+ <translation>Codage des caractères non supporté</translation>
+ </message>
+</context>
+<context>
+ <name>GameVariantDialog</name>
+ <message>
+ <source>Classic</source>
+ <translation>Classique</translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation>Duo</translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation>Junior</translation>
+ </message>
+ <message>
+ <source>Trigon</source>
+ <translation>Trigon</translation>
+ </message>
+ <message>
+ <source>Nexos</source>
+ <translation>Nexos</translation>
+ </message>
+ <message>
+ <source>GembloQ</source>
+ <translation>GembloQ</translation>
+ </message>
+ <message>
+ <source>Callisto</source>
+ <translation>Callisto</translation>
+ </message>
+ <message>
+ <source>Players:</source>
+ <translation>Joueurs :</translation>
+ </message>
+ <message>
+ <source>Colors:</source>
+ <translation>Couleurs :</translation>
+ </message>
+</context>
+<context>
+ <name>GotoMoveDialog</name>
+ <message>
+ <source>Move number:</source>
+ <translation>Numéro de coup :</translation>
+ </message>
+</context>
+<context>
+ <name>HelpWindow</name>
+ <message>
+ <source>Pentobi Help</source>
+ <translation>Aide de Pentobi</translation>
+ </message>
+</context>
+<context>
+ <name>ImageSaveDialog</name>
+ <message>
+ <source>Save Image</source>
+ <translation>Enregistrer l’image</translation>
+ </message>
+ <message>
+ <source>PNG image files</source>
+ <translation>Image PNG</translation>
+ </message>
+ <message>
+ <source>JPEG image files</source>
+ <translation>Image JPEG</translation>
+ </message>
+</context>
+<context>
+ <name>InitialRatingDialog</name>
+ <message>
+ <source>Initialize your rating for this game variant.</source>
+ <translation>Initialisez votre classement pour cette variante du jeu.</translation>
+ </message>
+ <message>
+ <source>Initial rating:</source>
+ <translation>Classement initial :</translation>
+ </message>
+ <message>
+ <source>Beginner</source>
+ <translation>Débutant</translation>
+ </message>
+ <message>
+ <source>Expert</source>
+ <translation>Expert</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <source>Pentobi</source>
+ <extracomment>Window title if no file is loaded.</extracomment>
+ <translation>Pentobi</translation>
+ </message>
+ <message>
+ <source>Game analysis is only possible in main variation.</source>
+ <translation>L’analyse de la partie n’est possible que dans la variation principale.</translation>
+ </message>
+ <message>
+ <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+ <translation>Jeu sauvé automatiquement a été changé par une autre instance de Pentobi. Remplacer ?</translation>
+ </message>
+ <message>
+ <source>Your rating has increased from %1 to %2.</source>
+ <translation>Votre classement a augmenté de %1 à %2.</translation>
+ </message>
+ <message>
+ <source>Your rating has decreased from %1 to %2.</source>
+ <translation>Votre classement a diminué de %1 à %2.</translation>
+ </message>
+ <message>
+ <source>Your rating stays at %1.</source>
+ <translation>Votre classement reste à %1.</translation>
+ </message>
+ <message>
+ <source>No permission to access storage</source>
+ <translation>Aucune autorisation d’accès au stockage</translation>
+ </message>
+ <message>
+ <source>Delete all rating information for the current game variant?</source>
+ <translation>Supprimer toutes les informations de classement de l’actuelle variante du jeu ?</translation>
+ </message>
+ <message>
+ <source>Delete all variations?</source>
+ <translation>Détruire toutes les variations ?</translation>
+ </message>
+ <message>
+ <source>Save failed.</source>
+ <translation>Échec de l’enregistrement.</translation>
+ </message>
+ <message>
+ <source>End of tree was reached. Continue search from start of the tree?</source>
+ <translation>La fin de l’arbre est atteinte. Continuer la recherche depuis la racine ?</translation>
+ </message>
+ <message>
+ <source>No comment found</source>
+ <translation>Aucun commentaire trouvé</translation>
+ </message>
+ <message>
+ <source>%1 (modified)</source>
+ <translation>%1 (modifié)</translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Reload?</source>
+ <translation>Le fichier a été modifié par une autre application. Recharger ?</translation>
+ </message>
+ <message>
+ <source>Continue computer move?</source>
+ <translation>Coninuer le coup de l’ordinateur ?</translation>
+ </message>
+ <message>
+ <source>Keep only position?</source>
+ <translation>Garder seulement la position ?</translation>
+ </message>
+ <message>
+ <source>Keep only subtree?</source>
+ <translation>Garder seulement le sous-arbre ?</translation>
+ </message>
+ <message>
+ <source>Open failed.</source>
+ <translation>Échec de l’ouverture.</translation>
+ </message>
+ <message>
+ <source>Start rated game with Purple against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec violet contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Green against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec vert contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec bleu/rouge contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec bleu contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Orange against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec orange contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec jaune/vert contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec jaune contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Red against Pentobi level %1?</source>
+ <translation>Commencer une partie classée avec rouge contre Pentobi niveau %1 ?</translation>
+ </message>
+ <message>
+ <source>You have not yet played rated games in this game variant.</source>
+ <translation>Vous n’avez pas encore joué des parties classées dans cette variante du jeu.</translation>
+ </message>
+ <message>
+ <source>Truncate this subtree?</source>
+ <translation>Élaguer la branche actuelle ?</translation>
+ </message>
+ <message>
+ <source>Truncate children?</source>
+ <translation>Élaguer les branches filles ?</translation>
+ </message>
+ <message>
+ <source>Discard game?</source>
+ <translation>Abandonner la partie ?</translation>
+ </message>
+ <message>
+ <source>Pentobi %1 (level %2)</source>
+ <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+ <translation>Pentobi %1 (niveau %2)</translation>
+ </message>
+ <message>
+ <source>Human</source>
+ <extracomment>Player name for game info in rated game.</extracomment>
+ <translation>Personne</translation>
+ </message>
+ <message>
+ <source>Rated game</source>
+ <translation>Partie classée</translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Overwrite?</source>
+ <translation>Le fichier a été modifié par une autre application. Remplacer ?</translation>
+ </message>
+ <message>
+ <source>%1 - Pentobi</source>
+ <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+ <translation>%1 - Pentobi</translation>
+ </message>
+ <message>
+ <source>Not enough memory</source>
+ <translation>Mémoire insuffisante</translation>
+ </message>
+ <message>
+ <source>Game analysis aborted</source>
+ <translation>Analyse de la partie abandonnée</translation>
+ </message>
+ <message>
+ <source>Computer move aborted</source>
+ <translation>Coup de l'ordinateur abandonné</translation>
+ </message>
+ <message>
+ <source>Rating information deleted</source>
+ <translation>Informations de classement supprimées</translation>
+ </message>
+ <message>
+ <source>Variations deleted</source>
+ <translation>Variations supprimées</translation>
+ </message>
+ <message>
+ <source>File saved</source>
+ <translation>Fichier enregistré</translation>
+ </message>
+ <message>
+ <source>Saving image failed or unsupported image format</source>
+ <translation>Impossible d’enregistrer l’image ou format de l’image non pris en charge</translation>
+ </message>
+ <message>
+ <source>Image saved</source>
+ <translation>Image enregistrée</translation>
+ </message>
+ <message>
+ <source>Creating image failed</source>
+ <translation>La création d’image a échoué</translation>
+ </message>
+ <message>
+ <source>Continuing rated game</source>
+ <translation>Partie classée est continuée</translation>
+ </message>
+ <message>
+ <source>Kept only position</source>
+ <translation>Gardé seulement la position</translation>
+ </message>
+ <message>
+ <source>Kept only subtree</source>
+ <translation>Gardé seulement le sous-arbre</translation>
+ </message>
+ <message>
+ <source>Variation is now %1</source>
+ <translation>La variation est maintenant %1</translation>
+ </message>
+ <message>
+ <source>Children truncated</source>
+ <translation>Nœuds fils coupé</translation>
+ </message>
+ <message>
+ <source>Setup</source>
+ <extracomment>Small-screen label for setup mode (short for "Setup Mode").</extracomment>
+ <translation>Position</translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation>Position</translation>
+ </message>
+ <message>
+ <source>Rated</source>
+ <extracomment>Label for ongoing rated game</extracomment>
+ <translation>Classée</translation>
+ </message>
+ <message>
+ <source>Rated %1</source>
+ <extracomment>Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number.</extracomment>
+ <translation>Classée %1</translation>
+ </message>
+ <message>
+ <source>Rated Game %1</source>
+ <extracomment>Label for rated game. The argument is the game number.</extracomment>
+ <translation>Partie classée %1</translation>
+ </message>
+ <message>
+ <source>Main Variation</source>
+ <translation>Variation principale</translation>
+ </message>
+ <message>
+ <source>Beginning of Branch</source>
+ <translation>Au début de la branche</translation>
+ </message>
+ <message>
+ <source>Comment</source>
+ <translation>Commentaire</translation>
+ </message>
+ <message>
+ <source>Settings</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation>Configuration</translation>
+ </message>
+ <message>
+ <source>Find Move</source>
+ <translation>Trouver un coup</translation>
+ </message>
+ <message>
+ <source>Next Comment</source>
+ <translation>Commentaire suivant</translation>
+ </message>
+ <message>
+ <source>Fullscreen</source>
+ <translation>Plein écran</translation>
+ </message>
+ <message>
+ <source>Game Info</source>
+ <translation>Info sur la partie</translation>
+ </message>
+ <message>
+ <source>Move Number…</source>
+ <translation>Numéro de coup…</translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation>Aide de Pentobi</translation>
+ </message>
+ <message>
+ <source>New</source>
+ <translation>Nouveau</translation>
+ </message>
+ <message>
+ <source>Rated Game</source>
+ <translation>Partie classée</translation>
+ </message>
+ <message>
+ <source>Open…</source>
+ <translation>Ouvrir…</translation>
+ </message>
+ <message>
+ <source>Play</source>
+ <translation>Jouer</translation>
+ </message>
+ <message>
+ <source>Play Move</source>
+ <extracomment>Play a single move</extracomment>
+ <translation>Jouer un coup</translation>
+ </message>
+ <message>
+ <source>Quit</source>
+ <translation>Quitter</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation>Enregistrer</translation>
+ </message>
+ <message>
+ <source>Save As…</source>
+ <translation>Enregistrer sous…</translation>
+ </message>
+ <message>
+ <source>Stop</source>
+ <translation>Arrêter</translation>
+ </message>
+ <message>
+ <source>Undo Move</source>
+ <translation>Annuler le coup</translation>
+ </message>
+</context>
+<context>
+ <name>MenuComputer</name>
+ <message>
+ <source>Computer</source>
+ <translation>Ordinateur</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+ <translation>J</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+</context>
+<context>
+ <name>MenuEdit</name>
+ <message>
+ <source>Edit</source>
+ <translation>Édition</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>Make Main Variation</source>
+ <translation>Choisir comme variation principale</translation>
+ </message>
+ <message>
+ <source>Variation Up</source>
+ <extracomment>Short for Move Variation Up</extracomment>
+ <translation>Variation vers le haut</translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+ <translation>H</translation>
+ </message>
+ <message>
+ <source>Variation Down</source>
+ <extracomment>Short for Move Variation Down</extracomment>
+ <translation>Variation vers le bas</translation>
+ </message>
+ <message>
+ <source>W</source>
+ <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+ <message>
+ <source>Delete Variations</source>
+ <translation>Détruire les variations</translation>
+ </message>
+ <message>
+ <source>D</source>
+ <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+ <translation>D</translation>
+ </message>
+ <message>
+ <source>Truncate</source>
+ <translation>Couper</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+ <message>
+ <source>Truncate Children</source>
+ <translation>Couper les branches filles</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>Keep Position</source>
+ <translation>Garder la position</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+ <translation>G</translation>
+ </message>
+ <message>
+ <source>Keep Subtree</source>
+ <translation>Garder le sous-arbre</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation>Position</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>Next Color</source>
+ <translation>Couleur suivante</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>Annotation…</source>
+ <translation>Annotation…</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Made main variation</source>
+ <translation>Choisi comme variation principale</translation>
+ </message>
+</context>
+<context>
+ <name>MenuExport</name>
+ <message>
+ <source>Export</source>
+ <translation>Exporter</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+ <translation>I</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Image…</source>
+ <translation>Image…</translation>
+ </message>
+ <message>
+ <source>ASCII Art…</source>
+ <translation>Art ASCII…</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGame</name>
+ <message>
+ <source>Game</source>
+ <translation>Partie</translation>
+ </message>
+ <message>
+ <source>G</source>
+ <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+ <message>
+ <source>Game Variant…</source>
+ <translation>Variante du jeu…</translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+ <translation>V</translation>
+ </message>
+ <message>
+ <source>I</source>
+ <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+ <translation>I</translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+ <translation>T</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>Open Clipboard</source>
+ <translation>Ouvrir le presse-papiers</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+ <translation>R</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>Q</source>
+ <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+ <translation>Q</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGo</name>
+ <message>
+ <source>Go</source>
+ <translation>Déplacement</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+ <translation>D</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+ <translation>D</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+</context>
+<context>
+ <name>MenuHelp</name>
+ <message>
+ <source>Help</source>
+ <translation>Aide</translation>
+ </message>
+ <message>
+ <source>H</source>
+ <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>About Pentobi</source>
+ <translation>À propos de Pentobi</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>Report Bug</source>
+ <translation>Rapportez une erreur</translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+ <translation>R</translation>
+ </message>
+</context>
+<context>
+ <name>MenuItem</name>
+ <message>
+ <source>Ctrl</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation>Ctrl</translation>
+ </message>
+ <message>
+ <source>Shift</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation>Maj</translation>
+ </message>
+</context>
+<context>
+ <name>MenuRecentFiles</name>
+ <message>
+ <source>Open Recent</source>
+ <translation>Fichiers récents</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>%1. %2</source>
+ <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+ <translation>%1. %2</translation>
+ </message>
+ <message>
+ <source>Clear List</source>
+ <extracomment>Menu item for clearing the recent files list</extracomment>
+ <translation>Effacer la liste</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+</context>
+<context>
+ <name>MenuTools</name>
+ <message>
+ <source>Tools</source>
+ <translation>Outils</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <source>Rating</source>
+ <translation>Classement</translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+ <message>
+ <source>Clear Rating</source>
+ <translation>Effacer le classement</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>Analyze Game</source>
+ <translation>Analyser la partie</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Clear Analysis</source>
+ <translation>Effacer l’analyse</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>Analyze Game…</source>
+ <translation>Analyser la partie…</translation>
+ </message>
+</context>
+<context>
+ <name>MenuView</name>
+ <message>
+ <source>View</source>
+ <translation>Affichage</translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Appearance…</source>
+ <translation type="vanished">Apparence…</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+ <translation>C</translation>
+ </message>
+ <message>
+ <source>Appearance</source>
+ <translation>Apparence</translation>
+ </message>
+ <message>
+ <source>Toolbar</source>
+ <translation>Barre d’outils</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+</context>
+<context>
+ <name>MoveAnnotationDialog</name>
+ <message>
+ <source>Move %1</source>
+ <translation>Coup %1</translation>
+ </message>
+ <message>
+ <source>Very good</source>
+ <translation>Très bon</translation>
+ </message>
+ <message>
+ <source>Good</source>
+ <translation>Bon</translation>
+ </message>
+ <message>
+ <source>Interesting</source>
+ <translation>Intéressant</translation>
+ </message>
+ <message>
+ <source>Doubtful</source>
+ <translation>Douteux</translation>
+ </message>
+ <message>
+ <source>Bad</source>
+ <translation>Mauvais</translation>
+ </message>
+ <message>
+ <source>Very Bad</source>
+ <translation>Très mauvais</translation>
+ </message>
+ <message>
+ <source>No annotation</source>
+ <translation>Aucune annotation</translation>
+ </message>
+</context>
+<context>
+ <name>NewFolderDialog</name>
+ <message>
+ <source>Folder name:</source>
+ <translation>Nom de dossier :</translation>
+ </message>
+</context>
+<context>
+ <name>OpenDialog</name>
+ <message>
+ <source>Open</source>
+ <translation>Ouvrir</translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation>Parties de Blokus</translation>
+ </message>
+</context>
+<context>
+ <name>RatingDialog</name>
+ <message>
+ <source>Your rating:</source>
+ <translation>Votre classement :</translation>
+ </message>
+ <message>
+ <source>Game variant:</source>
+ <translation>Variante du jeu :</translation>
+ </message>
+ <message>
+ <source>Classic (2)</source>
+ <extracomment>Short for Classic (2 players)</extracomment>
+ <translation>Classique (2)</translation>
+ </message>
+ <message>
+ <source>Classic (3)</source>
+ <extracomment>Short for Classic (3 players)</extracomment>
+ <translation>Classique (3)</translation>
+ </message>
+ <message>
+ <source>Classic (4)</source>
+ <extracomment>Short for Classic (4 players)</extracomment>
+ <translation>Classique (4)</translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation>Duo</translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation>Junior</translation>
+ </message>
+ <message>
+ <source>Trigon (2)</source>
+ <extracomment>Short for Trigon (2 players)</extracomment>
+ <translation>Trigon (2)</translation>
+ </message>
+ <message>
+ <source>Trigon (3)</source>
+ <extracomment>Short for Trigon (3 players)</extracomment>
+ <translation>Trigon (3)</translation>
+ </message>
+ <message>
+ <source>Trigon (4)</source>
+ <extracomment>Short for Trigon (4 players)</extracomment>
+ <translation>Trigon (4)</translation>
+ </message>
+ <message>
+ <source>Nexos (2)</source>
+ <extracomment>Short for Nexos (2 players)</extracomment>
+ <translation>Nexos (2)</translation>
+ </message>
+ <message>
+ <source>Nexos (4)</source>
+ <extracomment>Short for Nexos (4 players)</extracomment>
+ <translation>Nexos (4)</translation>
+ </message>
+ <message>
+ <source>Callisto (2)</source>
+ <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+ <translation>Callisto (2)</translation>
+ </message>
+ <message>
+ <source>Callisto (2/4)</source>
+ <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+ <translation>Callisto (2/4)</translation>
+ </message>
+ <message>
+ <source>Callisto (3)</source>
+ <extracomment>Short for Callisto (3 players)</extracomment>
+ <translation>Callisto (3)</translation>
+ </message>
+ <message>
+ <source>Callisto (4)</source>
+ <extracomment>Short for Callisto (4 players)</extracomment>
+ <translation>Callisto (4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (4)</source>
+ <extracomment>Short for GembloQ (4 players)</extracomment>
+ <translation>GembloQ (4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (2)</source>
+ <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+ <translation>GembloQ (2)</translation>
+ </message>
+ <message>
+ <source>GembloQ (2/4)</source>
+ <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+ <translation>GembloQ (2/4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (3)</source>
+ <extracomment>Short for GembloQ (3 players)</extracomment>
+ <translation>GembloQ (3)</translation>
+ </message>
+ <message>
+ <source>Rated games:</source>
+ <translation>Parties classées :</translation>
+ </message>
+ <message>
+ <source>Best previous rating:</source>
+ <translation>Meilleur classement précédent :</translation>
+ </message>
+ <message>
+ <source>Recent development:</source>
+ <translation>Développement récent :</translation>
+ </message>
+ <message>
+ <source>Game</source>
+ <translation>Partie</translation>
+ </message>
+ <message>
+ <source>Result</source>
+ <translation>Résultat</translation>
+ </message>
+ <message>
+ <source>Win</source>
+ <extracomment>Result of rated game is a win</extracomment>
+ <translation>Victoire</translation>
+ </message>
+ <message>
+ <source>Loss</source>
+ <extracomment>Result of rated game is a loss</extracomment>
+ <translation>Perte</translation>
+ </message>
+ <message>
+ <source>Tie</source>
+ <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+ <translation>Egalité</translation>
+ </message>
+ <message>
+ <source>Level</source>
+ <translation>Niveau</translation>
+ </message>
+ <message>
+ <source>Your Color</source>
+ <translation>Votre couleur</translation>
+ </message>
+ <message>
+ <source>Date</source>
+ <translation>Date</translation>
+ </message>
+ <message>
+ <source>Open Game %1</source>
+ <translation>Ouvrir la partie %1</translation>
+ </message>
+</context>
+<context>
+ <name>SaveDialog</name>
+ <message>
+ <source>Save</source>
+ <translation>Enregistrer</translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation>Parties de Blokus</translation>
+ </message>
+</context>
+<context>
+ <name>ToolBar</name>
+ <message>
+ <source>Start a new game</source>
+ <translation>Commencer une nouvelle partie</translation>
+ </message>
+ <message>
+ <source>Start a rated game</source>
+ <translation>Commence une partie classée</translation>
+ </message>
+ <message>
+ <source>Set the colors played by the computer</source>
+ <translation>Définir les couleurs joué par l’ordinateur</translation>
+ </message>
+ <message>
+ <source>Make the computer continue to play the current color</source>
+ <translation>Faire que l’ordinateur continue à jouer la couleur actuelle</translation>
+ </message>
+ <message>
+ <source>Make the computer play the current color</source>
+ <translation>Faire que l’ordinateur joue la couleur actuelle</translation>
+ </message>
+ <message>
+ <source>Go to beginning of game</source>
+ <translation>Aller en début de partie</translation>
+ </message>
+ <message>
+ <source>Go ten moves backward</source>
+ <translation>Revenir dix coups en arrière</translation>
+ </message>
+ <message>
+ <source>Go one move backward</source>
+ <translation>Revenir un coup en arrière</translation>
+ </message>
+ <message>
+ <source>Go one move forward</source>
+ <translation>Avancer d’un coup</translation>
+ </message>
+ <message>
+ <source>Go ten moves forward</source>
+ <translation>Avancer de dix coups</translation>
+ </message>
+ <message>
+ <source>Go to end of moves</source>
+ <translation>Aller à la fin de coups</translation>
+ </message>
+ <message>
+ <source>Go to previous variation</source>
+ <translation>Aller à la variation précédente</translation>
+ </message>
+ <message>
+ <source>Go to next variation</source>
+ <translation>Aller à la variation suivante</translation>
+ </message>
+ <message>
+ <source>Abort game analysis</source>
+ <translation>Abandonner l'analyse</translation>
+ </message>
+ <message>
+ <source>Abort computer move</source>
+ <translation>Abandonner le coup de l'ordinateur</translation>
+ </message>
+ <message>
+ <source>Undo move</source>
+ <extracomment>Tooltip for Undo button</extracomment>
+ <translation>Annuler le coup</translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="nb_NO">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <source>Copyright © 2011–%1 Markus Enzenberger</source>
+ <translation>Copyright © 2011–%1 Markus Enzenberger</translation>
+ </message>
+ <message>
+ <source>Computer opponent for the board game Blokus</source>
+ <translation>Datamaskinmotstander for brettspillet Blokus</translation>
+ </message>
+ <message>
+ <source>Pentobi %1</source>
+ <extracomment>The argument is the application version.</extracomment>
+ <translation>Pentobi %1</translation>
+ </message>
+</context>
+<context>
+ <name>Actions</name>
+ <message>
+ <source>Main Variation</source>
+ <translation type="vanished">Hovedvariasjon</translation>
+ </message>
+ <message>
+ <source>Beginning of Branch</source>
+ <translation type="vanished">Begynnelse av forgreining</translation>
+ </message>
+ <message>
+ <source>Settings…</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation type="vanished">Innstillinger</translation>
+ </message>
+ <message>
+ <source>Find Move</source>
+ <translation type="vanished">Finn trekk</translation>
+ </message>
+ <message>
+ <source>Next Comment</source>
+ <translation type="vanished">Neste kommentar</translation>
+ </message>
+ <message>
+ <source>Fullscreen</source>
+ <translation type="vanished">Fullskjermsvisning</translation>
+ </message>
+ <message>
+ <source>Move Number…</source>
+ <translation type="vanished">Trekknummer…</translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation type="vanished">Pentobi-hjelp</translation>
+ </message>
+ <message>
+ <source>New</source>
+ <translation type="vanished">Ny</translation>
+ </message>
+ <message>
+ <source>Rated Game</source>
+ <translation type="vanished">Vurdert spill</translation>
+ </message>
+ <message>
+ <source>Open…</source>
+ <translation type="vanished">Åpne…</translation>
+ </message>
+ <message>
+ <source>Play</source>
+ <translation type="vanished">Spill</translation>
+ </message>
+ <message>
+ <source>Play Move</source>
+ <extracomment>Play a single move</extracomment>
+ <translation type="vanished">Spill enkelt trekk</translation>
+ </message>
+ <message>
+ <source>Quit</source>
+ <translation type="vanished">Avslutt</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation type="vanished">Lagre</translation>
+ </message>
+ <message>
+ <source>Save As…</source>
+ <translation type="vanished">Lagre som…</translation>
+ </message>
+ <message>
+ <source>Stop</source>
+ <translation type="vanished">Stopp</translation>
+ </message>
+ <message>
+ <source>Undo Move</source>
+ <translation type="vanished">Angre trekk</translation>
+ </message>
+ <message>
+ <source>Game Info</source>
+ <translation type="vanished">Spillinfo</translation>
+ </message>
+ <message>
+ <source>Comment</source>
+ <translation type="vanished">Kommentar</translation>
+ </message>
+ <message>
+ <source>Settings</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation type="vanished">Innstillinger</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeDialog</name>
+ <message>
+ <source>Analysis speed:</source>
+ <translation>Analysehastighet:</translation>
+ </message>
+ <message>
+ <source>Fast</source>
+ <translation>Rask</translation>
+ </message>
+ <message>
+ <source>Normal</source>
+ <translation>Normal</translation>
+ </message>
+ <message>
+ <source>Slow</source>
+ <translation>Treg</translation>
+ </message>
+</context>
+<context>
+ <name>AnalyzeGame</name>
+ <message>
+ <source>(No analysis)</source>
+ <translation>(Ingen analyse)</translation>
+ </message>
+</context>
+<context>
+ <name>AppearanceDialog</name>
+ <message>
+ <source>Coordinates</source>
+ <translation>Koordinater</translation>
+ </message>
+ <message>
+ <source>Show variations</source>
+ <translation>Vis variasjoner</translation>
+ </message>
+ <message>
+ <source>Light</source>
+ <translation>Lys</translation>
+ </message>
+ <message>
+ <source>Dark</source>
+ <translation>Mørk</translation>
+ </message>
+ <message>
+ <source>Colorblind light</source>
+ <translation>Fargeblind lys</translation>
+ </message>
+ <message>
+ <source>Colorblind dark</source>
+ <translation>Fargeblind mørk</translation>
+ </message>
+ <message>
+ <source>System</source>
+ <extracomment>Name of theme using default system colors</extracomment>
+ <translation>System</translation>
+ </message>
+ <message>
+ <source>Move marking:</source>
+ <translation>Flytt markering:</translation>
+ </message>
+ <message>
+ <source>Last with dot</source>
+ <translation>Siste med punkt</translation>
+ </message>
+ <message>
+ <source>Last with number</source>
+ <translation>Siste med nummer</translation>
+ </message>
+ <message>
+ <source>All with number</source>
+ <translation>Alle med nummer</translation>
+ </message>
+ <message>
+ <source>None</source>
+ <extracomment>Move marking/None</extracomment>
+ <translation>Ingen</translation>
+ </message>
+ <message>
+ <source>Animations</source>
+ <translation>Animasjoner</translation>
+ </message>
+ <message>
+ <source>Show comment:</source>
+ <translation>Vis kommentar:</translation>
+ </message>
+ <message>
+ <source>Always</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Alltid</translation>
+ </message>
+ <message>
+ <source>As needed</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Ved behov</translation>
+ </message>
+ <message>
+ <source>Never</source>
+ <extracomment>Show-comment mode</extracomment>
+ <translation>Aldri</translation>
+ </message>
+ <message>
+ <source>Color theme:</source>
+ <translation>Fargepalett:</translation>
+ </message>
+ <message>
+ <source>Move number</source>
+ <extracomment>Check box in appearance dialog whether to show the move number in the status bar.</extracomment>
+ <translation>Trekknummer</translation>
+ </message>
+</context>
+<context>
+ <name>AsciiArtSaveDialog</name>
+ <message>
+ <source>Export ASCII Art</source>
+ <translation>Eksporter ASCII-kunst</translation>
+ </message>
+ <message>
+ <source>Text files</source>
+ <translation>Tekstfiler</translation>
+ </message>
+</context>
+<context>
+ <name>BoardContextMenu</name>
+ <message>
+ <source>Go to Move %1</source>
+ <translation>Gå til trekk %1</translation>
+ </message>
+ <message>
+ <source>Move Annotation</source>
+ <translation>Flytt anmerkning</translation>
+ </message>
+ <message>
+ <source>Move Annotation (%1)</source>
+ <extracomment>The argument is the annotation symbol for the current move</extracomment>
+ <translation>Flytt anmerkning (%1)</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonApply</name>
+ <message>
+ <source>Apply</source>
+ <translation>Bruk</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonCancel</name>
+ <message>
+ <source>Cancel</source>
+ <translation>Avbryt</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonClose</name>
+ <message>
+ <source>Close</source>
+ <translation>Lukk</translation>
+ </message>
+</context>
+<context>
+ <name>ButtonOk</name>
+ <message>
+ <source>OK</source>
+ <translation>OK</translation>
+ </message>
+</context>
+<context>
+ <name>ComputerDialog</name>
+ <message>
+ <source>Computer plays:</source>
+ <translation>Datamaskinen spiller:</translation>
+ </message>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Blå/rød</translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation>Lilla</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Grønn</translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation>Blå</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Gul/grønn</translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation>Oransje</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Gul</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rød</translation>
+ </message>
+ <message>
+ <source>Level %1</source>
+ <translation>Nivå %1</translation>
+ </message>
+</context>
+<context>
+ <name>ExportImageDialog</name>
+ <message>
+ <source>Image width:</source>
+ <translation>Bildebredde:</translation>
+ </message>
+</context>
+<context>
+ <name>FileDialog</name>
+ <message>
+ <source>Overwrite file?</source>
+ <translation>Overskriv fil?</translation>
+ </message>
+ <message>
+ <source>Open</source>
+ <translation>Åpne</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation>Lagre</translation>
+ </message>
+ <message>
+ <source>All files</source>
+ <translation>Alle filer</translation>
+ </message>
+</context>
+<context>
+ <name>GameDisplayDesktop</name>
+ <message>
+ <source>Computer is thinking…</source>
+ <translation>Datamaskinen tenker…</translation>
+ </message>
+ <message>
+ <source>Running game analysis…</source>
+ <translation>Kjører spillanalyse…</translation>
+ </message>
+ <message>
+ <source>Computer is thinking… (up to %1 seconds remaining)</source>
+ <translation>Datamaskinen tenker… (opptil %1 sekunder gjenstår)</translation>
+ </message>
+ <message>
+ <source>Computer is thinking… (up to %1 minutes remaining)</source>
+ <translation>Datamaskinen tenker… (opptil %1 minutter gjenstår)</translation>
+ </message>
+</context>
+<context>
+ <name>GameInfoDialog</name>
+ <message>
+ <source>Player Blue/Red:</source>
+ <translation>Spiller blå/rød:</translation>
+ </message>
+ <message>
+ <source>Player Purple:</source>
+ <translation>Spiller lilla:</translation>
+ </message>
+ <message>
+ <source>Player Green:</source>
+ <translation>Spiller grønn:</translation>
+ </message>
+ <message>
+ <source>Player Blue:</source>
+ <translation>Spiller blå:</translation>
+ </message>
+ <message>
+ <source>Player Yellow/Green:</source>
+ <translation>Spiller gul/grønn:</translation>
+ </message>
+ <message>
+ <source>Player Orange:</source>
+ <translation>Spiller oransje:</translation>
+ </message>
+ <message>
+ <source>Player Yellow:</source>
+ <translation>Spiller gul:</translation>
+ </message>
+ <message>
+ <source>Player Red:</source>
+ <translation>Spiller rød:</translation>
+ </message>
+ <message>
+ <source>Date:</source>
+ <translation>Dato:</translation>
+ </message>
+ <message>
+ <source>Time:</source>
+ <translation>Tid:</translation>
+ </message>
+ <message>
+ <source>Event:</source>
+ <translation>Hendelse:</translation>
+ </message>
+ <message>
+ <source>Round:</source>
+ <translation>Runde:</translation>
+ </message>
+</context>
+<context>
+ <name>GameModel</name>
+ <message>
+ <source>Blue/Red</source>
+ <translation>Blå/rød</translation>
+ </message>
+ <message>
+ <source>Purple</source>
+ <translation>Lilla</translation>
+ </message>
+ <message>
+ <source>Green</source>
+ <translation>Grønn</translation>
+ </message>
+ <message>
+ <source>Blue</source>
+ <translation>Blå</translation>
+ </message>
+ <message>
+ <source>Yellow/Green</source>
+ <translation>Gul/grønn</translation>
+ </message>
+ <message>
+ <source>Orange</source>
+ <translation>Oransje</translation>
+ </message>
+ <message>
+ <source>Yellow</source>
+ <translation>Gul</translation>
+ </message>
+ <message>
+ <source>Red</source>
+ <translation>Rød</translation>
+ </message>
+ <message>
+ <source>Purple wins with 1 point.</source>
+ <translation>Lilla vinner med ett poeng.</translation>
+ </message>
+ <message>
+ <source>Purple wins with %L1 points.</source>
+ <translation>Lilla vinner med %L1 poeng.</translation>
+ </message>
+ <message>
+ <source>Orange wins with 1 point.</source>
+ <translation>Oransje vinner med ett poeng.</translation>
+ </message>
+ <message>
+ <source>Orange wins with %L1 points.</source>
+ <translation>Oransje vinner med %L1 poeng.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie.</source>
+ <translation>Spillet slutter uavgjort.</translation>
+ </message>
+ <message>
+ <source>Green wins with 1 point.</source>
+ <translation>Grønn vinner med ett poeng.</translation>
+ </message>
+ <message>
+ <source>Green wins with %L1 points.</source>
+ <translation>Grønn vinner med %L1 poeng.</translation>
+ </message>
+ <message>
+ <source>Blue wins with 1 point.</source>
+ <translation>Blå vinner med ett poeng.</translation>
+ </message>
+ <message>
+ <source>Blue wins with %L1 points.</source>
+ <translation>Blå vinner med %L1 poeng.</translation>
+ </message>
+ <message>
+ <source>Green wins (tie resolved).</source>
+ <translation>Grønn vinner (uavgjort tilstand løst).</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with 1 point.</source>
+ <translation>Blå/rød vinner med ett poeng.</translation>
+ </message>
+ <message>
+ <source>Blue/Red wins with %L1 points.</source>
+ <translation>Blå/rød vinner med %L1 poeng.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with 1 point.</source>
+ <translation>Gul/grønn vinnner med ett poeng.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins with %L1 points.</source>
+ <translation>Gul/grønn vinner med %L1 poeng.</translation>
+ </message>
+ <message>
+ <source>Yellow/Green wins (tie resolved).</source>
+ <translation>Gul/grønn vinner (uavgjort tilstand løst).</translation>
+ </message>
+ <message>
+ <source>Blue wins.</source>
+ <translation>Blå vinner.</translation>
+ </message>
+ <message>
+ <source>Yellow wins.</source>
+ <translation>Gul vinner.</translation>
+ </message>
+ <message>
+ <source>Red wins.</source>
+ <translation>Rød vinner.</translation>
+ </message>
+ <message>
+ <source>Red wins (tie resolved).</source>
+ <translation>Rød vinner (uavgjort tilstand løst).</translation>
+ </message>
+ <message>
+ <source>Yellow wins (tie resolved).</source>
+ <translation>Gul vinner (uavgjirt tilstand løst).</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Yellow.</source>
+ <translation>Spillet slutter uavgjort mellom blå og gul.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue and Red.</source>
+ <translation>Spillet slutter uavgjort mellom blå og rød.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow and Red.</source>
+ <translation>Spillet slutter uavgjort mellom gul og rød.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between all players.</source>
+ <translation>Spillet slutter uavgjort mellom alle spillere.</translation>
+ </message>
+ <message>
+ <source>Green wins.</source>
+ <translation>Grønn vinner.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Red.</source>
+ <translation>Spillet slutter uavgjort mellom blå, gul og rød.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Yellow and Green.</source>
+ <translation>Spillet slutter uavgjort mellom blå, gul og grønn.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Blue, Red and Green.</source>
+ <translation>Spillet slutter uavgjort mellom blå, rød og grønn.</translation>
+ </message>
+ <message>
+ <source>Game ends in a tie between Yellow, Red and Green.</source>
+ <translation>Spillet slutter uavgjort mellom gul, rød og grønn.</translation>
+ </message>
+ <message>
+ <source>Invalid Blokus SGF file. (%1)</source>
+ <translation>Ugyldig Blokus SGF-fil. (%1)</translation>
+ </message>
+ <message>
+ <source>Clipboard is empty.</source>
+ <translation>Utklippstavlen er tom.</translation>
+ </message>
+ <message>
+ <source>Untitled</source>
+ <translation>Uten tittel</translation>
+ </message>
+ <message>
+ <source>Untitled %1</source>
+ <extracomment>The argument is a number, which will be increased if a file with the same name already exists</extracomment>
+ <translation>Uten tittel %1</translation>
+ </message>
+ <message>
+ <source>New Folder</source>
+ <translation>Ny mappe</translation>
+ </message>
+ <message>
+ <source>New Folder %1</source>
+ <extracomment>The argument is a number, which will be increased if a folder with the same name already exists</extracomment>
+ <translation>Ny mappe %1</translation>
+ </message>
+ <message>
+ <source>(Setup)</source>
+ <translation>(Oppsett)</translation>
+ </message>
+ <message>
+ <source>(No moves)</source>
+ <translation>(Ingen trekk)</translation>
+ </message>
+ <message>
+ <source>Move %1</source>
+ <extracomment>The argument is the current move number.</extracomment>
+ <translation>Trekk %1</translation>
+ </message>
+ <message>
+ <source>Unsupported character set</source>
+ <translation>Ustøttet tegnsett</translation>
+ </message>
+</context>
+<context>
+ <name>GameVariantDialog</name>
+ <message>
+ <source>Classic</source>
+ <translation>Klassisk</translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation>Duo</translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation>Junior</translation>
+ </message>
+ <message>
+ <source>Trigon</source>
+ <translation>Trigon</translation>
+ </message>
+ <message>
+ <source>Nexos</source>
+ <translation>Nexos</translation>
+ </message>
+ <message>
+ <source>GembloQ</source>
+ <translation>GembloQ</translation>
+ </message>
+ <message>
+ <source>Callisto</source>
+ <translation>Callisto</translation>
+ </message>
+ <message>
+ <source>Players:</source>
+ <translation>Spillere:</translation>
+ </message>
+ <message>
+ <source>Colors:</source>
+ <translation>Farger:</translation>
+ </message>
+</context>
+<context>
+ <name>GotoMoveDialog</name>
+ <message>
+ <source>Move number:</source>
+ <translation>Trekknummer:</translation>
+ </message>
+</context>
+<context>
+ <name>HelpWindow</name>
+ <message>
+ <source>Pentobi Help</source>
+ <translation>Pentobi-hjelp</translation>
+ </message>
+</context>
+<context>
+ <name>ImageSaveDialog</name>
+ <message>
+ <source>Save Image</source>
+ <translation>Lagre bilde</translation>
+ </message>
+ <message>
+ <source>PNG image files</source>
+ <translation>PNG-bildefiler</translation>
+ </message>
+ <message>
+ <source>JPEG image files</source>
+ <translation>JPEG-bildefiler</translation>
+ </message>
+</context>
+<context>
+ <name>InitialRatingDialog</name>
+ <message>
+ <source>Initialize your rating for this game variant.</source>
+ <translation>Hent din vurdering for denne spillvarianten.</translation>
+ </message>
+ <message>
+ <source>Initial rating:</source>
+ <translation>Startsvurdering:</translation>
+ </message>
+ <message>
+ <source>Beginner</source>
+ <translation>Begynner</translation>
+ </message>
+ <message>
+ <source>Expert</source>
+ <translation>Ekspert</translation>
+ </message>
+</context>
+<context>
+ <name>Main</name>
+ <message>
+ <source>Pentobi</source>
+ <extracomment>Window title if no file is loaded.</extracomment>
+ <translation>Pentobi</translation>
+ </message>
+ <message>
+ <source>Game analysis is only possible in main variation.</source>
+ <translation>Spillanalyse er kun tilgjengelig i hovedvariasjonen.</translation>
+ </message>
+ <message>
+ <source>Autosaved game was changed by another instance of Pentobi. Overwrite?</source>
+ <translation>Automatisk lagret spill ble endret av en annen kjørende utgave av Pentobi. Overskriv?</translation>
+ </message>
+ <message>
+ <source>Your rating has increased from %1 to %2.</source>
+ <translation>Din vurdering har økt fra %1 til %2.</translation>
+ </message>
+ <message>
+ <source>Your rating has decreased from %1 to %2.</source>
+ <translation>Din vurdering har sunket fra %1 til %2.</translation>
+ </message>
+ <message>
+ <source>Your rating stays at %1.</source>
+ <translation>Din vurdering forblir %1.</translation>
+ </message>
+ <message>
+ <source>No permission to access storage</source>
+ <translation>Mangler lagringstilgang</translation>
+ </message>
+ <message>
+ <source>Delete all rating information for the current game variant?</source>
+ <translation>Slett all vurderingsinformasjon fra nåværende spillvariant?</translation>
+ </message>
+ <message>
+ <source>Delete all variations?</source>
+ <translation>Slett alle variasjoner?</translation>
+ </message>
+ <message>
+ <source>Save failed.</source>
+ <translation>Lagring mislyktes.</translation>
+ </message>
+ <message>
+ <source>End of tree was reached. Continue search from start of the tree?</source>
+ <translation>Nådde slutten av treet. Fortsett søket fra starten av treet?</translation>
+ </message>
+ <message>
+ <source>No comment found</source>
+ <translation>Ingen kommentar funnet</translation>
+ </message>
+ <message>
+ <source>%1 (modified)</source>
+ <translation>%1 (endret)</translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Reload?</source>
+ <translation>Filen har blitt endret av et annet program. Last inn på nytt?</translation>
+ </message>
+ <message>
+ <source>Continue computer move?</source>
+ <translation>Fortsett datamaskintrekk?</translation>
+ </message>
+ <message>
+ <source>Keep only position?</source>
+ <translation>Kun behold posisjonen?</translation>
+ </message>
+ <message>
+ <source>Keep only subtree?</source>
+ <translation>Kun behold undertreet?</translation>
+ </message>
+ <message>
+ <source>Open failed.</source>
+ <translation>Åpning mislyktes.</translation>
+ </message>
+ <message>
+ <source>Start rated game with Purple against Pentobi level %1?</source>
+ <translation>Start vurdert spill med lilla mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Green against Pentobi level %1?</source>
+ <translation>Start vurdert spill med grønn mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue/Red against Pentobi level %1?</source>
+ <translation>Start vurdert spill med blå/rød mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Blue against Pentobi level %1?</source>
+ <translation>Start vurdert spill med blå mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Orange against Pentobi level %1?</source>
+ <translation>Start vurdert spill med oransje mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow/Green against Pentobi level %1?</source>
+ <translation>Start vurdert spill med gul/grønn mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Yellow against Pentobi level %1?</source>
+ <translation>Start vurdert spill med gul mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>Start rated game with Red against Pentobi level %1?</source>
+ <translation>Start vurdert spill med rød mot Pentobi nivå %1?</translation>
+ </message>
+ <message>
+ <source>You have not yet played rated games in this game variant.</source>
+ <translation>Du har ikke spillt noen vurderte spill i denne varianten enda.</translation>
+ </message>
+ <message>
+ <source>Truncate this subtree?</source>
+ <translation>Forkort dette undertreet?</translation>
+ </message>
+ <message>
+ <source>Truncate children?</source>
+ <translation>Forkort underprosess?</translation>
+ </message>
+ <message>
+ <source>Discard game?</source>
+ <translation>Forkast spill?</translation>
+ </message>
+ <message>
+ <source>Pentobi %1 (level %2)</source>
+ <extracomment>Player name for game info in rated game. First argument is version of Pentobi, second argument is level.</extracomment>
+ <translation>Pentobi %1 (nivå %2)</translation>
+ </message>
+ <message>
+ <source>Human</source>
+ <extracomment>Player name for game info in rated game.</extracomment>
+ <translation>Menneske</translation>
+ </message>
+ <message>
+ <source>Rated game</source>
+ <translation>Vurdert spill</translation>
+ </message>
+ <message>
+ <source>File has been modified by another application. Overwrite?</source>
+ <translation>Filen har blitt endret av et annet program. Overskriv?</translation>
+ </message>
+ <message>
+ <source>%1 - Pentobi</source>
+ <extracomment>Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified.</extracomment>
+ <translation>%1 - Pentobi</translation>
+ </message>
+ <message>
+ <source>Not enough memory</source>
+ <translation>Ikke nok minne</translation>
+ </message>
+ <message>
+ <source>Game analysis aborted</source>
+ <translation>Spillanalyse avbrutt</translation>
+ </message>
+ <message>
+ <source>Computer move aborted</source>
+ <translation>Datamaskintrekk avbrutt</translation>
+ </message>
+ <message>
+ <source>Rating information deleted</source>
+ <translation>Vurderingsinformasjon slettet</translation>
+ </message>
+ <message>
+ <source>Variations deleted</source>
+ <translation>Variasjoner slettet</translation>
+ </message>
+ <message>
+ <source>File saved</source>
+ <translation>Fil lagret</translation>
+ </message>
+ <message>
+ <source>Saving image failed or unsupported image format</source>
+ <translation>Lagring av bilde mislyktes, eller ustøttet bildeformat</translation>
+ </message>
+ <message>
+ <source>Image saved</source>
+ <translation>Bilde lagret</translation>
+ </message>
+ <message>
+ <source>Creating image failed</source>
+ <translation>Oppretting av bilde mislyktes</translation>
+ </message>
+ <message>
+ <source>Continuing rated game</source>
+ <translation>Fortsetter vurdert spill</translation>
+ </message>
+ <message>
+ <source>Kept only position</source>
+ <translation>Beholdt kun posisjonen</translation>
+ </message>
+ <message>
+ <source>Kept only subtree</source>
+ <translation>Beholdte kun undertre</translation>
+ </message>
+ <message>
+ <source>Variation is now %1</source>
+ <translation>Varianten er nå %1</translation>
+ </message>
+ <message>
+ <source>Children truncated</source>
+ <translation>Underprosess forkortet</translation>
+ </message>
+ <message>
+ <source>Setup</source>
+ <extracomment>Small-screen label for setup mode (short for "Setup Mode").</extracomment>
+ <translation>Oppsett</translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation>Oppsettsmodus</translation>
+ </message>
+ <message>
+ <source>Rated</source>
+ <extracomment>Label for ongoing rated game</extracomment>
+ <translation>Vurdert</translation>
+ </message>
+ <message>
+ <source>Rated %1</source>
+ <extracomment>Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number.</extracomment>
+ <translation>Vurdert %1</translation>
+ </message>
+ <message>
+ <source>Rated Game %1</source>
+ <extracomment>Label for rated game. The argument is the game number.</extracomment>
+ <translation>Vurdert spill %1</translation>
+ </message>
+ <message>
+ <source>Main Variation</source>
+ <translation>Hovedvariasjon</translation>
+ </message>
+ <message>
+ <source>Beginning of Branch</source>
+ <translation>Begynnelse av forgreining</translation>
+ </message>
+ <message>
+ <source>Comment</source>
+ <translation>Kommentar</translation>
+ </message>
+ <message>
+ <source>Settings</source>
+ <extracomment>Menu item Computer/Settings</extracomment>
+ <translation>Innstillinger</translation>
+ </message>
+ <message>
+ <source>Find Move</source>
+ <translation>Finn trekk</translation>
+ </message>
+ <message>
+ <source>Next Comment</source>
+ <translation>Neste kommentar</translation>
+ </message>
+ <message>
+ <source>Fullscreen</source>
+ <translation>Fullskjermsvisning</translation>
+ </message>
+ <message>
+ <source>Game Info</source>
+ <translation>Spillinfo</translation>
+ </message>
+ <message>
+ <source>Move Number…</source>
+ <translation>Trekknummer…</translation>
+ </message>
+ <message>
+ <source>Pentobi Help</source>
+ <translation>Pentobi-hjelp</translation>
+ </message>
+ <message>
+ <source>New</source>
+ <translation>Ny</translation>
+ </message>
+ <message>
+ <source>Rated Game</source>
+ <translation>Vurdert spill</translation>
+ </message>
+ <message>
+ <source>Open…</source>
+ <translation>Åpne…</translation>
+ </message>
+ <message>
+ <source>Play</source>
+ <translation>Spill</translation>
+ </message>
+ <message>
+ <source>Play Move</source>
+ <extracomment>Play a single move</extracomment>
+ <translation>Spill enkelt trekk</translation>
+ </message>
+ <message>
+ <source>Quit</source>
+ <translation>Avslutt</translation>
+ </message>
+ <message>
+ <source>Save</source>
+ <translation>Lagre</translation>
+ </message>
+ <message>
+ <source>Save As…</source>
+ <translation>Lagre som…</translation>
+ </message>
+ <message>
+ <source>Stop</source>
+ <translation>Stopp</translation>
+ </message>
+ <message>
+ <source>Undo Move</source>
+ <translation>Angre trekk</translation>
+ </message>
+</context>
+<context>
+ <name>MenuComputer</name>
+ <message>
+ <source>Computer</source>
+ <translation>Datamaskin</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu Computer. Leave empty for no mnemonic.</extracomment>
+ <translation>D</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Computer Settings. Leave empty for no mnemonic.</extracomment>
+ <translation>L</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Play. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Play Move. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Stop. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+</context>
+<context>
+ <name>MenuEdit</name>
+ <message>
+ <source>Edit</source>
+ <translation>Rediger</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Edit. Leave empty for no mnemonic.</extracomment>
+ <translation>R</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation>H</translation>
+ </message>
+ <message>
+ <source>Make Main Variation</source>
+ <translation>Gjør til hovedvariasjon</translation>
+ </message>
+ <message>
+ <source>Variation Up</source>
+ <extracomment>Short for Move Variation Up</extracomment>
+ <translation>Variasjon oppover</translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Variation Up. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>Variation Down</source>
+ <extracomment>Short for Move Variation Down</extracomment>
+ <translation>Variasjon nedover</translation>
+ </message>
+ <message>
+ <source>W</source>
+ <extracomment>Mnemonic for menu item Variation Down. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>Delete Variations</source>
+ <translation>Slett variasjoner</translation>
+ </message>
+ <message>
+ <source>D</source>
+ <extracomment>Mnemonic for menu item Delete Variations. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>Truncate</source>
+ <translation>Forkort</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item Truncate. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>Truncate Children</source>
+ <translation>Forkort underprosess</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Truncate Children. Leave empty for no mnemonic.</extracomment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <source>Keep Position</source>
+ <translation>Behold posisjon</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Keep Position. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+ <message>
+ <source>Keep Subtree</source>
+ <translation>Behold undertreet</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic.</extracomment>
+ <translation>T</translation>
+ </message>
+ <message>
+ <source>Setup Mode</source>
+ <translation>Oppsettsmodus</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Setup Mode. Leave empty for no mnemonic.</extracomment>
+ <translation>M</translation>
+ </message>
+ <message>
+ <source>Next Color</source>
+ <translation>Neste farge</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Next Color. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>Annotation…</source>
+ <translation>Anmerkning…</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Annotation. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Made main variation</source>
+ <translation>Gjort til hovedvariasjon</translation>
+ </message>
+</context>
+<context>
+ <name>MenuExport</name>
+ <message>
+ <source>Export</source>
+ <translation>Eksporter</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu Export. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Image. Leave empty for no mnemonic.</extracomment>
+ <translation>B</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item ASCII Art. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Image…</source>
+ <translation>Bilde…</translation>
+ </message>
+ <message>
+ <source>ASCII Art…</source>
+ <translation>ASCII-kunst…</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGame</name>
+ <message>
+ <source>Game</source>
+ <translation>Spil</translation>
+ </message>
+ <message>
+ <source>G</source>
+ <extracomment>Mnemonic for menu Game. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item New. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rated Game. Leave empty for no mnemonic.</extracomment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <source>Game Variant…</source>
+ <translation>Spillvariant…</translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu item Game Variant. Leave empty for no mnemonic.</extracomment>
+ <translation>V</translation>
+ </message>
+ <message>
+ <source>I</source>
+ <extracomment>Mnemonic for menu item Game Info. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>U</source>
+ <extracomment>Mnemonic for menu item Undo. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Find Move. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu item Open. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>Open Clipboard</source>
+ <translation>Åpne utklippstavle</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic.</extracomment>
+ <translation>K</translation>
+ </message>
+ <message>
+ <source>S</source>
+ <extracomment>Mnemonic for menu item Save. Leave empty for no mnemonic.</extracomment>
+ <translation>L</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Save As. Leave empty for no mnemonic.</extracomment>
+ <translation>S</translation>
+ </message>
+ <message>
+ <source>Q</source>
+ <extracomment>Mnemonic for menu item Quit. Leave empty for no mnemonic.</extracomment>
+ <translation>T</translation>
+ </message>
+</context>
+<context>
+ <name>MenuGo</name>
+ <message>
+ <source>Go</source>
+ <translation>Gå</translation>
+ </message>
+ <message>
+ <source>O</source>
+ <extracomment>Mnemonic for menu Go. Leave empty for no mnemonic.</extracomment>
+ <translation>G</translation>
+ </message>
+ <message>
+ <source>N</source>
+ <extracomment>Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>M</source>
+ <extracomment>Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic.</extracomment>
+ <translation>H</translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Next Comment. Leave empty for no mnemonic.</extracomment>
+ <translation>K</translation>
+ </message>
+</context>
+<context>
+ <name>MenuHelp</name>
+ <message>
+ <source>Help</source>
+ <translation>Hjelp</translation>
+ </message>
+ <message>
+ <source>H</source>
+ <extracomment>Mnemonic for menu Help. Leave empty for no mnemonic.</extracomment>
+ <translation>H</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic.</extracomment>
+ <translation>P</translation>
+ </message>
+ <message>
+ <source>About Pentobi</source>
+ <translation>Om Pentobi</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item About Pentobi. Leave empty for no mnemonic.</extracomment>
+ <translation>O</translation>
+ </message>
+ <message>
+ <source>Report Bug</source>
+ <translation>Innrapporter feil</translation>
+ </message>
+ <message>
+ <source>B</source>
+ <extracomment>Mnemonic for menu item Report Bug. Leave empty for no mnemonic.</extracomment>
+ <translation>R</translation>
+ </message>
+</context>
+<context>
+ <name>MenuItem</name>
+ <message>
+ <source>Ctrl</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation>Ctrl</translation>
+ </message>
+ <message>
+ <source>Shift</source>
+ <extracomment>Shortcut modifier key as displayed in menu item text (abbreviate if long)</extracomment>
+ <translation>Shift</translation>
+ </message>
+</context>
+<context>
+ <name>MenuRecentFiles</name>
+ <message>
+ <source>Open Recent</source>
+ <translation>Åpne nylige</translation>
+ </message>
+ <message>
+ <source>P</source>
+ <extracomment>Mnemonic for menu Open Recent. Leave empty for no mnemonic.</extracomment>
+ <translation>Y</translation>
+ </message>
+ <message>
+ <source>%1. %2</source>
+ <extracomment>Format in recent files menu. First argument is the file number, second argument the file name.</extracomment>
+ <translation>%1. %2</translation>
+ </message>
+ <message>
+ <source>Clear List</source>
+ <extracomment>Menu item for clearing the recent files list</extracomment>
+ <translation>Tøm liste</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic.</extracomment>
+ <translation>T</translation>
+ </message>
+</context>
+<context>
+ <name>MenuTools</name>
+ <message>
+ <source>Tools</source>
+ <translation>Verktøy</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu Tools. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <source>Rating</source>
+ <translation>Vurdering</translation>
+ </message>
+ <message>
+ <source>R</source>
+ <extracomment>Mnemonic for menu item Rating. Leave empty for no mnemonic.</extracomment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <source>Clear Rating</source>
+ <translation>Fjern vurdering</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item Clear Rating. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>Analyze Game</source>
+ <translation>Analyser spill</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu item Analyze Game. Leave empty for no mnemonic.</extracomment>
+ <translation>A</translation>
+ </message>
+ <message>
+ <source>Clear Analysis</source>
+ <translation>Tøm analyse</translation>
+ </message>
+ <message>
+ <source>E</source>
+ <extracomment>Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic.</extracomment>
+ <translation>N</translation>
+ </message>
+ <message>
+ <source>Analyze Game…</source>
+ <translation>Analyser spill…</translation>
+ </message>
+</context>
+<context>
+ <name>MenuView</name>
+ <message>
+ <source>View</source>
+ <translation>Vis</translation>
+ </message>
+ <message>
+ <source>V</source>
+ <extracomment>Mnemonic for menu View. Leave empty for no mnemonic.</extracomment>
+ <translation>V</translation>
+ </message>
+ <message>
+ <source>Appearance…</source>
+ <translation type="vanished">Utseende…</translation>
+ </message>
+ <message>
+ <source>A</source>
+ <extracomment>Mnemonic for menu Appearance. Leave empty for no mnemonic.</extracomment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <source>F</source>
+ <extracomment>Mnemonic for menu item Fullscreen. Leave empty for no mnemonic.</extracomment>
+ <translation>F</translation>
+ </message>
+ <message>
+ <source>C</source>
+ <extracomment>Mnemonic for menu item View/Comment. Leave empty for no mnemonic.</extracomment>
+ <translation>K</translation>
+ </message>
+ <message>
+ <source>Appearance</source>
+ <translation>Utseende</translation>
+ </message>
+ <message>
+ <source>Toolbar</source>
+ <translation>Verktøyslinje</translation>
+ </message>
+ <message>
+ <source>T</source>
+ <extracomment>Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic.</extracomment>
+ <translation>E</translation>
+ </message>
+</context>
+<context>
+ <name>MoveAnnotationDialog</name>
+ <message>
+ <source>Move %1</source>
+ <translation>Trekk %1</translation>
+ </message>
+ <message>
+ <source>Very good</source>
+ <translation>Veldig god</translation>
+ </message>
+ <message>
+ <source>Good</source>
+ <translation>God</translation>
+ </message>
+ <message>
+ <source>Interesting</source>
+ <translation>Interessant</translation>
+ </message>
+ <message>
+ <source>Doubtful</source>
+ <translation>Tvilsom</translation>
+ </message>
+ <message>
+ <source>Bad</source>
+ <translation>Dårlig</translation>
+ </message>
+ <message>
+ <source>Very Bad</source>
+ <translation>Veldig dårlig</translation>
+ </message>
+ <message>
+ <source>No annotation</source>
+ <translation>Ingen tilknytning</translation>
+ </message>
+</context>
+<context>
+ <name>NewFolderDialog</name>
+ <message>
+ <source>Folder name:</source>
+ <translation>Mappenavn:</translation>
+ </message>
+</context>
+<context>
+ <name>OpenDialog</name>
+ <message>
+ <source>Open</source>
+ <translation>Åpne</translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation>Blokus-spill</translation>
+ </message>
+</context>
+<context>
+ <name>RatingDialog</name>
+ <message>
+ <source>Your rating:</source>
+ <translation>Din vurdering:</translation>
+ </message>
+ <message>
+ <source>Game variant:</source>
+ <translation>Spillvariant:</translation>
+ </message>
+ <message>
+ <source>Classic (2)</source>
+ <extracomment>Short for Classic (2 players)</extracomment>
+ <translation>Klassisk (2)</translation>
+ </message>
+ <message>
+ <source>Classic (3)</source>
+ <extracomment>Short for Classic (3 players)</extracomment>
+ <translation>Klassisk (3)</translation>
+ </message>
+ <message>
+ <source>Classic (4)</source>
+ <extracomment>Short for Classic (4 players)</extracomment>
+ <translation>Klassisk (4)</translation>
+ </message>
+ <message>
+ <source>Duo</source>
+ <translation>Duo</translation>
+ </message>
+ <message>
+ <source>Junior</source>
+ <translation>Junior</translation>
+ </message>
+ <message>
+ <source>Trigon (2)</source>
+ <extracomment>Short for Trigon (2 players)</extracomment>
+ <translation>Trigon (2)</translation>
+ </message>
+ <message>
+ <source>Trigon (3)</source>
+ <extracomment>Short for Trigon (3 players)</extracomment>
+ <translation>Trigon (3)</translation>
+ </message>
+ <message>
+ <source>Trigon (4)</source>
+ <extracomment>Short for Trigon (4 players)</extracomment>
+ <translation>Trigon (4)</translation>
+ </message>
+ <message>
+ <source>Nexos (2)</source>
+ <extracomment>Short for Nexos (2 players)</extracomment>
+ <translation>Nexos (2)</translation>
+ </message>
+ <message>
+ <source>Nexos (4)</source>
+ <extracomment>Short for Nexos (4 players)</extracomment>
+ <translation>Nexos (4)</translation>
+ </message>
+ <message>
+ <source>Callisto (2)</source>
+ <extracomment>Short for Callisto (2 players, 2 colors)</extracomment>
+ <translation>Callisto (2)</translation>
+ </message>
+ <message>
+ <source>Callisto (2/4)</source>
+ <extracomment>Short for Callisto (2 players, 4 colors)</extracomment>
+ <translation>Callisto (2/4)</translation>
+ </message>
+ <message>
+ <source>Callisto (3)</source>
+ <extracomment>Short for Callisto (3 players)</extracomment>
+ <translation>Callisto (3)</translation>
+ </message>
+ <message>
+ <source>Callisto (4)</source>
+ <extracomment>Short for Callisto (4 players)</extracomment>
+ <translation>Callisto (4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (4)</source>
+ <extracomment>Short for GembloQ (4 players)</extracomment>
+ <translation>GembloQ (4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (2)</source>
+ <extracomment>Short for GembloQ (2 players, 2 colors)</extracomment>
+ <translation>GembloQ (2)</translation>
+ </message>
+ <message>
+ <source>GembloQ (2/4)</source>
+ <extracomment>Short for GembloQ (2 players, 4 colors)</extracomment>
+ <translation>GembloQ (2/4)</translation>
+ </message>
+ <message>
+ <source>GembloQ (3)</source>
+ <extracomment>Short for GembloQ (3 players)</extracomment>
+ <translation>GembloQ (3)</translation>
+ </message>
+ <message>
+ <source>Rated games:</source>
+ <translation>Vurderte spill:</translation>
+ </message>
+ <message>
+ <source>Best previous rating:</source>
+ <translation>Beste tidligere vurdering:</translation>
+ </message>
+ <message>
+ <source>Recent development:</source>
+ <translation>Nylig utvikling:</translation>
+ </message>
+ <message>
+ <source>Game</source>
+ <translation>Spil</translation>
+ </message>
+ <message>
+ <source>Result</source>
+ <translation>Resultat</translation>
+ </message>
+ <message>
+ <source>Win</source>
+ <extracomment>Result of rated game is a win</extracomment>
+ <translation>Vunnet</translation>
+ </message>
+ <message>
+ <source>Loss</source>
+ <extracomment>Result of rated game is a loss</extracomment>
+ <translation>Tapt</translation>
+ </message>
+ <message>
+ <source>Tie</source>
+ <extracomment>Result of rated game is a tie. Abbreviate long translations to ensure that all columns of rated games list are visible on mobile devices with small screens.</extracomment>
+ <translation>Uavgjort</translation>
+ </message>
+ <message>
+ <source>Level</source>
+ <translation>Nivå</translation>
+ </message>
+ <message>
+ <source>Your Color</source>
+ <translation>Din farge</translation>
+ </message>
+ <message>
+ <source>Date</source>
+ <translation>Dato</translation>
+ </message>
+ <message>
+ <source>Open Game %1</source>
+ <translation>Åpne spill %1</translation>
+ </message>
+</context>
+<context>
+ <name>SaveDialog</name>
+ <message>
+ <source>Save</source>
+ <translation>Lagre</translation>
+ </message>
+ <message>
+ <source>Blokus games</source>
+ <translation>Blokus-spill</translation>
+ </message>
+</context>
+<context>
+ <name>ToolBar</name>
+ <message>
+ <source>Start a new game</source>
+ <translation>Start nytt spill</translation>
+ </message>
+ <message>
+ <source>Start a rated game</source>
+ <translation>Start vurdert spill</translation>
+ </message>
+ <message>
+ <source>Set the colors played by the computer</source>
+ <translation>Sett fargene spilt av datamaskinen</translation>
+ </message>
+ <message>
+ <source>Make the computer continue to play the current color</source>
+ <translation>Få datamaskinen til å fortsette å spille gjeldende farge</translation>
+ </message>
+ <message>
+ <source>Make the computer play the current color</source>
+ <translation>Få datamaskinen til å spille gjeldende farge</translation>
+ </message>
+ <message>
+ <source>Go to beginning of game</source>
+ <translation>Gå til begynnelsen av spillet</translation>
+ </message>
+ <message>
+ <source>Go ten moves backward</source>
+ <translation>Gå ti trekk tilbake</translation>
+ </message>
+ <message>
+ <source>Go one move backward</source>
+ <translation>Gå ett steg bakover</translation>
+ </message>
+ <message>
+ <source>Go one move forward</source>
+ <translation>Gå ett steg forover</translation>
+ </message>
+ <message>
+ <source>Go ten moves forward</source>
+ <translation>Gå ti trekk forover</translation>
+ </message>
+ <message>
+ <source>Go to end of moves</source>
+ <translation>Gå til trekkslutt</translation>
+ </message>
+ <message>
+ <source>Go to previous variation</source>
+ <translation>Gå til forrige variasjon</translation>
+ </message>
+ <message>
+ <source>Go to next variation</source>
+ <translation>Gå til neste variasjon</translation>
+ </message>
+ <message>
+ <source>Abort game analysis</source>
+ <translation>Avbryt spillanalyse</translation>
+ </message>
+ <message>
+ <source>Abort computer move</source>
+ <translation>Avbryt datamaskintrekk</translation>
+ </message>
+ <message>
+ <source>Undo move</source>
+ <extracomment>Tooltip for Undo button</extracomment>
+ <translation>Angre trekk</translation>
+ </message>
+</context>
+</TS>
--- /dev/null
+<RCC>
+ <qresource prefix="/qml/i18n">
+ <file>qml_de.qm</file>
+ <file>qml_fr.qm</file>
+ <file>qml_nb_NO.qm</file>
+ </qresource>
+</RCC>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="SVGRoot" width="16px" height="16px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="m1.5 4.5h13v-2h-9v-1h-4z" fill="#fff" stroke="#404040" stroke-linejoin="round" stroke-width="1px"/>
+ <path d="m0.5 4.5h15v10h-15z" fill="#bfbfbf" stroke="#404040" stroke-linejoin="round" stroke-width="1px"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="SVGRoot" width="16px" height="16px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <rect transform="rotate(90)" x="7" y="-14" width="2" height="12" ry="0" style="paint-order:fill markers stroke"/>
+ <rect transform="scale(-1)" x="-9" y="-14" width="2" height="12" ry="0" style="paint-order:fill markers stroke"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="SVGRoot" width="16px" height="16px" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+ <path d="m10.944 12.942v-9.8981l-6.9007 4.9491 6.9007 4.9491" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.0871"/>
+</svg>
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/colorblind-dark/Theme.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import "../colorblind-light" as ColorblindLight
+import "../dark" as Dark
+
+Dark.Theme {
+ property var colorBlue: [ "#008f9d", "#006069", "#00bcce", "#ffffff" ]
+ property var colorGreen: [ "#72a074", "#4e7450", "#9cbc9e", "#ffffff" ]
+ property var colorOrange: colorRed
+ property var colorPurple: colorBlue
+ property var colorRed: [ "#984326", "#692e19", "#ca5a30", "#ffffff" ]
+ property var colorYellow: [ "#bb7031", "#8c5525", "#d28b4f", "#ffffff" ]
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/colorblind-light/Theme.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import "../light" as Light
+
+Light.Theme {
+ property var colorBlue: [ "#008f9d", "#006069", "#00bcce", "#ffffff" ]
+ property var colorGreen: [ "#72a074", "#4e7450", "#9cbc9e", "#ffffff" ]
+ property var colorOrange: colorRed
+ property var colorPurple: colorBlue
+ property var colorRed: [ "#984326", "#692e19", "#ca5a30", "#ffffff" ]
+ property var colorYellow: [ "#bb7031", "#8c5525", "#d28b4f", "#ffffff" ]
+}
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/dark/Theme.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import "../light" as Light
+
+// See themes/light/Theme.qml for comments
+Light.Theme {
+ property var colorBoard: [ "#494347", "#3b3639", "#6d686b",
+ "#696267", "#5a5458", "#797276" ]
+
+ property color colorBackground: "#131313"
+ property color colorButtonPressed: Qt.lighter(colorBackground, 4)
+ property color colorButtonHovered: Qt.lighter(colorBackground, 2)
+ property color colorButtonBorder: Qt.lighter(colorBackground, 5)
+ property color colorCommentBase: "#1e2028"
+ property color colorCommentBorder: "#5a5756"
+ property color colorCommentFocus: "#4799cc"
+ property color colorCommentText: "#C8C1BE"
+ property color colorMessageText: "#C8C1BE"
+ property color colorMessageBase: "#333333"
+ property color colorSelectedText: colorBackground
+ property color colorSelection: "#4799cc"
+ property color colorStartingPoint: "#82777E"
+ property color colorText: "#e6d5e1"
+
+ function getImage(name) {
+ if (name === "pentobi-rated-game"
+ || name.startsWith("piece-manipulator"))
+ return "themes/dark/" + name + ".svg"
+ return light.getImage(name)
+ }
+
+ Light.Theme { id: light }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15.5 8a7.4998 7.4999 0 0 1-7.4972 7.4999 7.4998 7.4999 0 0 1-7.5025-7.4945 7.4998 7.4999 0 0 1 7.4917-7.5052 7.4998 7.4999 0 0 1 7.5079 7.4892" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.0003" style="paint-order:fill markers stroke"/>
+ <path d="m14 8a6 6 0 0 1-5.9979 6 6 6 0 0 1-6.0021-5.9957 6 6 0 0 1 5.9936-6.0043 6 6 0 0 1 6.0064 5.9915" style="paint-order:fill markers stroke"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#777" fill-opacity=".59"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <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="#666"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#bbb" fill-opacity=".75"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#777" fill-opacity=".75"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <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="#666"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#555" fill-opacity=".75"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#777" fill-opacity=".59"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <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="#666"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#bbb" fill-opacity=".75"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#777" fill-opacity=".75"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <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="#666"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#c8c8c8"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#666"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#555" fill-opacity=".75"/>
+</svg>
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/light/Theme.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+
+/** Theme using light colors. */
+Item {
+ /** @name Colors for board and piece elements.
+ Each color has several versions to paint raised or sunken borders. The
+ first color is the base color, the second a darker version, the third
+ a lighter version. The board has a second set of three colors for
+ painting the center section in Callisto, the pieces have a fourth color
+ for painting markup. */
+ /// @{
+ property var colorBlue: [ "#0073cf", "#004881", "#1499ff", "#ffffff" ]
+ property var colorBoard: [ "#aea7ac", "#868084", "#c7bfc5",
+ "#918b8f", "#7c777b", "#a09a9f"]
+ property var colorGreen: [ "#00c000", "#007800", "#00fa00", "#333333" ]
+ property var colorOrange: [ "#f09217", "#9d5e0b", "#ffbb67", "#333333" ]
+ property var colorPurple: [ "#a12ccf", "#6d2787", "#be70dc", "#ffffff" ]
+ property var colorRed: [ "#e63e2c", "#90261b", "#ff655a", "#ffffff" ]
+ property var colorYellow: [ "#f5c320", "#aa8516", "#ffdb58", "#333333" ]
+ /// @}
+
+ property color colorBackground: "#e8e8e8"
+ property color colorButtonPressed: Qt.darker(colorBackground, 1.1)
+ property color colorButtonHovered: Qt.lighter(colorBackground, 3)
+ property color colorButtonBorder: Qt.darker(colorBackground, 2)
+ property color colorCommentBase: "#ffffff"
+ property color colorCommentBorder: "#b4b3b3"
+ property color colorCommentFocus: "#4799cc"
+ property color colorCommentText: colorText
+ property color colorMessageText: "black"
+ property color colorMessageBase: "#cac9c9"
+ property color colorSelectedText: colorBackground
+ property color colorSelection: "#4799cc"
+ property color colorStartingPoint: "#767074"
+ property color colorText: "#111111"
+
+ function getImage(name) { return "themes/light/" + name + ".svg" }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path id="a" d="M 10,2 A 2,2 0 0 1 8.0007,4 2,2 0 0 1 6,2.0014 2,2 0 0 1 7.9979,0 2,2 0 0 1 10,1.9971" style="paint-order:fill markers stroke"/>
+ <use transform="translate(0,6.0029)" width="100%" height="100%" xlink:href="#a"/>
+ <use transform="translate(0 12.003)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m10 1v4h6v6h-6v4l-10-6.9965z" stroke-width=".94336"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path id="a" d="m10 1v14l-10-6.9965z" stroke-width=".94336"/>
+ <use transform="translate(6)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <rect x="2" y="2" width="3" height="12" stroke-width=".96077" style="paint-order:fill markers stroke"/>
+ <path id="a" d="m14 1v14l-10-6.9965z" stroke-width=".94336"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect id="a" x="2" y="2" width="6" height="6" style="paint-order:fill markers stroke"/>
+ <rect x=".50013" y=".50013" width="15" height="15" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.0003" style="paint-order:fill markers stroke"/>
+ <use transform="translate(6,6)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <rect transform="scale(-1,1)" x="-14" y="2" width="3" height="12" stroke-width=".96077" style="paint-order:fill markers stroke"/>
+ <path id="a" d="m2 1v14l10-6.9965z" stroke-width=".94336"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m6 1v4h-6v6h6v4l10-6.9965z" stroke-width=".94336"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path id="a" d="m6 1v14l10-6.9965z" stroke-width=".94336"/>
+ <use transform="translate(-6)" width="100%" height="100%" xlink:href="#a"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m12.523 12.908-4.6934-4.5523 0.21582 6.3642-0.66919-6.5042-3.9255 5.014 3.6682-5.4126-6.23 1.3177 6.2892-1.7885-5.6194-2.9952 5.9674 2.6726-2.3795-5.9065 2.8534 5.883 1.9739-6.0542-1.5957 6.3408 5.4036-3.369-5.2982 3.8316 6.3049 0.89259-6.5216-0.47042z" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7305" style="paint-order:fill markers stroke"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15 6h-4v-5h-6v5h-4l6.9965 10z" stroke-width=".94336"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m2.5 0.5v15l12.75-7.5z" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width="1px"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15 10h-4v5h-6v-5h-4l6.9965-10z" stroke-width=".94336"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m15 8a7.0001 7.0001 0 0 1-6.9976 7.0001 7.0001 7.0001 0 0 1-7.0026-6.9951 7.0001 7.0001 0 0 1 6.9925-7.0051 7.0001 7.0001 0 0 1 7.0076 6.9901" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.9998" style="paint-order:fill markers stroke"/>
+ <path d="m12.5 8a4.5001 4.5001 0 0 1-4.4985 4.5001 4.5001 4.5001 0 0 1-4.5017-4.4969 4.5001 4.5001 0 0 1 4.4953-4.5033 4.5001 4.5001 0 0 1 4.5049 4.4937" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".99978" style="paint-order:fill markers stroke"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m3 3 10 10" fill="none" stroke="#000" stroke-linecap="round" stroke-width="3.4904"/>
+ <path d="m13 3-10 10" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.461"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <path d="m6 12.544a5.2183 5.1494 0 0 1-3.6178-5.3032 5.2183 5.1494 0 0 1 4.401-4.6913 5.2183 5.1494 0 0 1 5.6368 3.151" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" style="paint-order:fill markers stroke"/>
+ <path d="m7.2557 7.9583-4.6751 7.6593 8.9512 0.08219z" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width=".60041px"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#666" fill-opacity=".59"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <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="#686868"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#fff" fill-opacity=".75"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-0.75976 25.988a74 74 0 0 1 71.84 73.969 74 74 0 0 1-148 0 74 74 0 0 1 76.16-73.969z" fill="#666" fill-opacity=".75"/>
+ <g id="b" transform="matrix(-.65 0 0 .65 113 -52)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <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="#686868"/>
+ </g>
+ <g id="a" transform="matrix(0 .65 -.65 0 304 35)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,113,87)" width="100%" height="100%" xlink:href="#b"/>
+ <use transform="rotate(90 100 100)" width="100%" height="100%" xlink:href="#a"/>
+ <path d="m174 100a74 74 0 0 1-73.974 74 74 74 0 0 1-74.026-73.947 74 74 0 0 1 73.921-74.053 74 74 0 0 1 74.079 73.894" fill="#aaa" fill-opacity=".75"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#666" fill-opacity=".59"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <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="#686868"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#fff" fill-opacity=".75"/>
+</svg>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="m102.92 0.042969a100 100 0 0 0-102.92 99.957 100 100 0 0 0 200 0 100 100 0 0 0-97.08-99.957zm-1.0508 35.984a64 64 0 0 1 62.131 63.973 64 64 0 0 1-128 0 64 64 0 0 1 65.869-63.973z" fill="#666" fill-opacity=".75"/>
+ <g id="c" transform="matrix(.9 0 0 .9 0 10)">
+ <path d="m40 100a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <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="#686868"/>
+ </g>
+ <g id="d" transform="matrix(.9 0 0 .9 10 20)">
+ <path d="m120 180a20 20 0 0 1-40 0 20 20 0 1 1 40 0z" fill="#eee"/>
+ <path d="m95 170-10 10 10 9.9375v-6.9375h10v7l10-10-10-9.9375v6.9375h-10z" fill="#686868"/>
+ </g>
+ <use transform="matrix(-1,0,0,1,118,-82)" width="100%" height="100%" xlink:href="#c"/>
+ <use transform="matrix(0,1,-1,0,364,0)" width="100%" height="100%" xlink:href="#d"/>
+ <path d="m164 100a64 64 0 0 1-63.977 64 64 64 0 0 1-64.023-63.954 64 64 0 0 1 63.931-64.046 64 64 0 0 1 64.069 63.908" fill="#aaa" fill-opacity=".75"/>
+</svg>
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi/qml/themes/system/Theme.qml
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+import QtQuick 2.11
+import "../dark" as Dark
+import "../light" as Light
+
+// See themes/light/Theme.qml for comments
+Item {
+ property color colorBackground: {
+ // If the contrast to yellow is too bad, we use a slightly modified
+ // system background color
+ var c = _contrast(palette.window, colorYellow)
+ if (c > 0 && c < 0.12)
+ return Qt.lighter(palette.window, 1.1)
+ if (c < 0 && c > -0.12)
+ return Qt.darker(palette.window, 1.5)
+ return palette.window
+ }
+
+ property var colorBlue: _isDark ? dark.colorBlue : light.colorBlue
+ property var colorBoard: _isDark ? dark.colorBoard : light.colorBoard
+ property var colorGreen: _isDark ? dark.colorGreen : light.colorGreen
+ property var colorOrange: _isDark ? dark.colorOrange : light.colorOrange
+ property var colorPurple: _isDark ? dark.colorPurple : light.colorPurple
+ property var colorRed: _isDark ? dark.colorRed : light.colorRed
+ property var colorYellow: _isDark ? dark.colorYellow : light.colorYellow
+
+ property color colorButtonPressed: palette.mid
+ property color colorButtonHovered: palette.window
+ property color colorButtonBorder: palette.dark
+ property color colorCommentBase: palette.base
+ property color colorCommentBorder: palette.mid
+ property color colorCommentFocus: palette.highlight
+ property color colorCommentText: colorText
+ property color colorMessageText: colorText
+ property color colorMessageBase: palette.base
+ property color colorSelectedText: palette.highlightedText
+ property color colorSelection: palette.highlight
+ property color colorStartingPoint:
+ _isDark ? dark.colorStartingPoint : light.colorStartingPoint
+ property color colorText: palette.text
+
+ property bool _isDark: palette.window.hslLightness < 0.5
+
+ function getImage(name) {
+ return _isDark ? dark.getImage(name) : light.getImage(name)
+ }
+
+ function _contrast(color1, color2) {
+ return 0.30 * (color1.r - color2.r) + 0.59 * (color1.g - color2.g)
+ + 0.11 * (color1.b - color2.b)
+ }
+
+ SystemPalette { id: palette }
+ Dark.Theme { id: dark }
+ Light.Theme { id: light }
+}
--- /dev/null
+<RCC>
+<qresource prefix="/qml/themes">
+<file>colorblind-dark/Theme.qml</file>
+<file>colorblind-light/Theme.qml</file>
+<file>dark/pentobi-rated-game.svg</file>
+<file>dark/piece-manipulator-desktop.svg</file>
+<file>dark/piece-manipulator-desktop-legal.svg</file>
+<file>dark/piece-manipulator.svg</file>
+<file>dark/piece-manipulator-legal.svg</file>
+<file>dark/Theme.qml</file>
+<file>light/menu.svg</file>
+<file>light/pentobi-backward.svg</file>
+<file>light/pentobi-backward10.svg</file>
+<file>light/pentobi-beginning.svg</file>
+<file>light/pentobi-computer-colors.svg</file>
+<file>light/pentobi-end.svg</file>
+<file>light/pentobi-forward.svg</file>
+<file>light/pentobi-forward10.svg</file>
+<file>light/pentobi-newgame.svg</file>
+<file>light/pentobi-next-variation.svg</file>
+<file>light/pentobi-play.svg</file>
+<file>light/pentobi-previous-variation.svg</file>
+<file>light/pentobi-rated-game.svg</file>
+<file>light/pentobi-stop.svg</file>
+<file>light/pentobi-undo.svg</file>
+<file>light/piece-manipulator-desktop.svg</file>
+<file>light/piece-manipulator-desktop-legal.svg</file>
+<file>light/piece-manipulator.svg</file>
+<file>light/piece-manipulator-legal.svg</file>
+<file>light/Theme.qml</file>
+<file>system/Theme.qml</file>
+</qresource>
+</RCC>
--- /dev/null
+<RCC>
+ <qresource prefix="/">
+ <file>qml/icons/filedialog-folder.svg</file>
+ <file>qml/icons/filedialog-newfolder.svg</file>
+ <file>qml/icons/filedialog-parent.svg</file>
+ <file>qml/AboutDialog.qml</file>
+ <file>qml/AnalyzeGame.qml</file>
+ <file>qml/AppearanceDialog.qml</file>
+ <file>qml/AsciiArtSaveDialog.qml</file>
+ <file>qml/Board.qml</file>
+ <file>qml/BoardContextMenu.qml</file>
+ <file>qml/BusyIndicator.qml</file>
+ <file>qml/Button.qml</file>
+ <file>qml/ButtonApply.qml</file>
+ <file>qml/ButtonCancel.qml</file>
+ <file>qml/ButtonClose.qml</file>
+ <file>qml/ButtonOk.qml</file>
+ <file>qml/ButtonToolTip.qml</file>
+ <file>qml/ComputerDialog.qml</file>
+ <file>qml/Comment.qml</file>
+ <file>qml/Controls.js</file>
+ <file>qml/Dialog.qml</file>
+ <file>qml/DialogButtonBox.qml</file>
+ <file>qml/DialogButtonBoxOkCancel.qml</file>
+ <file>qml/DialogLoader.qml</file>
+ <file>qml/ExportImageDialog.qml</file>
+ <file>qml/FatalMessage.qml</file>
+ <file>qml/FileDialog.qml</file>
+ <file>qml/GameDisplayMobile.qml</file>
+ <file>qml/GameInfoDialog.qml</file>
+ <file>qml/GameVariantDialog.qml</file>
+ <file>qml/GotoMoveDialog.qml</file>
+ <file>qml/HelpWindow.qml</file>
+ <file>qml/ImageSaveDialog.qml</file>
+ <file>qml/InitialRatingDialog.qml</file>
+ <file>qml/LineSegment.qml</file>
+ <file>qml/Main.qml</file>
+ <file>qml/MessageDialog.qml</file>
+ <file>qml/Menu.qml</file>
+ <file>qml/MenuComputer.qml</file>
+ <file>qml/MenuEdit.qml</file>
+ <file>qml/MenuExport.qml</file>
+ <file>qml/MenuGame.qml</file>
+ <file>qml/MenuGo.qml</file>
+ <file>qml/MenuHelp.qml</file>
+ <file>qml/MenuItem.qml</file>
+ <file>qml/MenuRecentFiles.qml</file>
+ <file>qml/MenuSeparator.qml</file>
+ <file>qml/MenuTools.qml</file>
+ <file>qml/MenuView.qml</file>
+ <file>qml/MoveAnnotationDialog.qml</file>
+ <file>qml/NavigationButtons.qml</file>
+ <file>qml/NavigationPanel.qml</file>
+ <file>qml/NewFolderDialog.qml</file>
+ <file>qml/OpenDialog.qml</file>
+ <file>qml/PieceCallisto.qml</file>
+ <file>qml/PieceClassic.qml</file>
+ <file>qml/PieceFlipAnimation.qml</file>
+ <file>qml/PieceGembloQ.qml</file>
+ <file>qml/PieceList.qml</file>
+ <file>qml/PieceManipulator.qml</file>
+ <file>qml/PieceNexos.qml</file>
+ <file>qml/PieceRotationAnimation.qml</file>
+ <file>qml/PieceSelectorMobile.qml</file>
+ <file>qml/PieceSwitchedFlipAnimation.qml</file>
+ <file>qml/PieceTrigon.qml</file>
+ <file>qml/QuarterSquare.qml</file>
+ <file>qml/QuestionDialog.qml</file>
+ <file>qml/RatingDialog.qml</file>
+ <file>qml/RatingGraph.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/AnalyzeDialog.qml</file>
+ <file>qml/GameDisplayDesktop.qml</file>
+ <file>qml/PieceSelectorDesktop.qml</file>
+ </qresource>
+</RCC>
--- /dev/null
+add_executable(pentobi-gtp
+ Engine.h
+ Engine.cpp
+ Main.cpp
+)
+
+target_compile_definitions(pentobi-gtp PRIVATE VERSION="${PENTOBI_VERSION}")
+
+target_link_libraries(pentobi-gtp pentobi_mcts)
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file pentobi_gtp/Engine.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "Engine.h"
+
+#include <fstream>
+#include "libboardgame_sgf/Writer.h"
+#include "libpentobi_mcts/Util.h"
+
+namespace pentobi_gtp {
+
+using libboardgame_gtp::Failure;
+using libboardgame_sgf::Writer;
+using libpentobi_base::Board;
+using libpentobi_base::get_color_id;
+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("selfplay", &Engine::cmd_selfplay);
+ add("version", &Engine::cmd_version);
+}
+
+Engine::~Engine() = default; // Non-inline to avoid GCC -Winline warning
+
+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;
+ children.reserve(tree.get_root().get_nu_children());
+ for (auto& i : tree.get_root_children())
+ children.push_back(&i);
+ sort(children.begin(), children.end(), libpentobi_mcts::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(Arguments args)
+{
+ auto& search = get_search();
+ if (! search.get_last_history().is_valid())
+ throw Failure("no search tree");
+ ofstream out(args.get());
+ libpentobi_mcts::dump_tree(out, search);
+}
+
+/** Let the engine play a number of games against itself.
+ This is more efficient than using twogtp if selfplay games are needed
+ because it has lower memory requirements (only one engine needed), process
+ switches between the engines are avoided and parts of the search tree can
+ be reused between moves of different players. */
+void Engine::cmd_selfplay(Arguments args)
+{
+ args.check_size(2);
+ auto nu_games = args.parse<int>(0);
+ ofstream out(args.get(1));
+ auto variant = get_board().get_variant();
+ auto variant_str = to_string(variant);
+ Board bd(variant);
+ auto& player = get_mcts_player();
+ ostringstream s;
+ for (int i = 0; i < nu_games; ++i)
+ {
+ s.str("");
+ Writer writer(s);
+ writer.set_indent(-1);
+ bd.init();
+ writer.begin_tree();
+ writer.begin_node();
+ writer.write_property("GM", variant_str);
+ writer.end_node();
+ while (! bd.is_game_over())
+ {
+ auto c = bd.get_effective_to_play();
+ auto mv = player.genmove(bd, c);
+ bd.play(c, mv);
+ writer.begin_node();
+ writer.write_property(get_color_id(variant, c),
+ bd.to_string(mv, false));
+ writer.end_node();
+ }
+ writer.end_tree();
+ out << s.str() << '\n';
+ }
+}
+
+void Engine::cmd_param(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'
+ << "exploration_constant " << s.get_exploration_constant() << '\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 == "exploration_constant")
+ s.set_exploration_constant(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;
+#else
+ version = "unknown";
+#endif
+#ifdef 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 = make_unique<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:
+ explicit Engine(Variant variant, unsigned level = 5,
+ bool use_book = true, const string& books_dir = "",
+ unsigned nu_threads = 0);
+
+ ~Engine() override;
+
+ void cmd_param(Arguments args, Response& response);
+ void cmd_get_value(Response& response);
+ void cmd_move_values(Response& response);
+ void cmd_name(Response& response);
+ void cmd_selfplay(Arguments args);
+ void cmd_save_tree(Arguments args);
+ void cmd_version(Response& 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 */
+//-----------------------------------------------------------------------------
+
+#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 == nullptr || argv[0] == nullptr)
+ 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)
+{
+ libboardgame_util::LogInitializer log_initializer;
+ 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 unknown version";
+#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"));
+ const 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 ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH})
+
+include(KDEInstallDirs)
+include(KDECompilerSettings)
+include(KDECMakeSettings)
+
+find_package(KF5 REQUIRED COMPONENTS KIO)
+
+add_library(pentobi-thumbnail MODULE
+ PentobiThumbCreator.h
+ PentobiThumbCreator.cpp
+)
+
+target_include_directories(pentobi-thumbnail PRIVATE "${CMAKE_SOURCE_DIR}/src")
+
+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; }
+
+}
+
+//-----------------------------------------------------------------------------
+
+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 <kio/thumbcreator.h>
+
+//-----------------------------------------------------------------------------
+
+class PentobiThumbCreator
+ : public ThumbCreator
+{
+public:
+ 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
+ServiceTypes=ThumbCreator
+MimeType=application/x-blokus-sgf;
+X-KDE-Library=pentobi-thumbnail
+
+
+# Translations
+Name[de]=Blokus-Partien
+Name[fr]=Parties de Blokus
--- /dev/null
+add_executable(pentobi-thumbnailer Main.cpp)
+
+target_link_libraries(pentobi-thumbnailer pentobi_thumbnail)
+
+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 "libboardgame_util/Log.h"
+#include "libpentobi_thumbnail/CreateThumbnail.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char* argv[])
+{
+ libboardgame_util::LogInitializer log_initializer;
+ QCoreApplication app(argc, argv);
+ try
+ {
+ QCommandLineParser parser;
+ QCommandLineOption optionSize(
+ QStringList() << QStringLiteral("s")
+ << QStringLiteral("size"),
+ QStringLiteral(
+ "Generate image with height and width <size>."),
+ QStringLiteral("size"), QStringLiteral("128"));
+ parser.addOption(optionSize);
+ parser.addHelpOption();
+ parser.addPositionalArgument(QStringLiteral("input.blksgf"),
+ QStringLiteral("Blokus SGF input file"));
+ parser.addPositionalArgument(QStringLiteral("output.png"),
+ QStringLiteral("PNG image output file"));
+ 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 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("Not a valid Blokus SGF file");
+ 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 */
+//-----------------------------------------------------------------------------
+
+#include "Analyze.h"
+
+#include <fstream>
+#include <map>
+#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)
+{
+ FmtSaver saver(cout);
+ ifstream in(file);
+ Statistics<> stat_result;
+ map<unsigned, Statistics<>> stat_result_player;
+ map<double, 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;
+ double result;
+ unsigned length;
+ unsigned player;
+ double cpu_b;
+ double 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';
+}
+
+//-----------------------------------------------------------------------------
--- /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);
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_ANALYZE_H
--- /dev/null
+find_package(Threads)
+
+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
+ Threads::Threads
+ )
+
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/FdStream.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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.begin()) + m_buf.size();
+ setg(end, end, end);
+}
+
+FdInBuf::~FdInBuf() = default; // Non-inline to avoid GCC -Winline warning
+
+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; // Non-inline to avoid GCC -Winline warning
+
+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:
+ explicit FdInBuf(int fd, size_t buf_size = 1024);
+
+ ~FdInBuf() override;
+
+protected:
+ int_type underflow() override;
+
+private:
+ int m_fd;
+
+ vector<char_type> m_buf;
+};
+
+//-----------------------------------------------------------------------------
+
+/** Input stream from a file descriptor. */
+class FdInStream final
+ : 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() override;
+
+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 final
+ : 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 */
+//-----------------------------------------------------------------------------
+
+#include "GtpConnection.h"
+
+#include <cstring>
+#include <vector>
+#include <unistd.h>
+#include "FdStream.h"
+#include "libboardgame_util/Log.h"
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+[[noreturn]] void terminate_child(const string& message)
+{
+ LIBBOARDGAME_LOG(message);
+ exit(1);
+}
+
+vector<string> split_args(const 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) != 0) && ! 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)
+{
+ auto args = split_args(command);
+ if (args.empty())
+ 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");
+ if (pid > 0) // Parent
+ {
+ close(fd1[0]);
+ close(fd2[1]);
+ m_in = make_unique<FdInStream>(fd2[0]);
+ m_out = make_unique<FdOutStream>(fd1[1]);
+ return;
+ }
+ // 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");
+ }
+ vector<char*> argv;
+ argv.reserve(args.size() + 1);
+ for (auto& a : args)
+ argv.push_back(const_cast<char*>(a.c_str()));
+ argv.push_back(nullptr);
+ execvp(args[0].c_str(), &(*argv.begin()));
+ terminate_child("Could not execute '" + command + "': " + strerror(errno));
+}
+
+GtpConnection::~GtpConnection() = default; // Non-inline to avoid GCC -Winline warning
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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::Options;
+using libpentobi_base::Variant;
+
+//-----------------------------------------------------------------------------
+
+int main(int argc, char** argv)
+{
+ libboardgame_util::LogInitializer log_initializer;
+ atomic<int> result(0);
+ try
+ {
+ vector<string> specs = {
+ "analyze:",
+ "black|b:",
+ "fastopen",
+ "file|f:",
+ "game|g:",
+ "nugames|n:",
+ "quiet",
+ "saveinterval:",
+ "threads:",
+ "tree",
+ "white|w:",
+ };
+ Options opt(argc, argv, specs);
+ if (opt.contains("analyze"))
+ {
+ analyze(opt.get("analyze"));
+ 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");
+ auto save_interval = opt.get<double>("saveinterval", 60);
+ 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>> twogtps;
+ twogtps.reserve(nu_threads);
+ for (unsigned i = 0; i < nu_threads; ++i)
+ {
+ string log_prefix;
+ if (nu_threads > 1)
+ log_prefix = to_string(i + 1);
+ auto twogtp = make_shared<TwoGtp>(black, white, variant,
+ nu_games, output, quiet,
+ log_prefix, fast_open);
+ twogtp->set_save_interval(save_interval);
+ twogtps.push_back(twogtp);
+ }
+ vector<thread> threads;
+ threads.reserve(nu_threads);
+ for (auto& i : twogtps)
+ threads.emplace_back([&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 */
+//-----------------------------------------------------------------------------
+
+#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");
+ m_timer.reset(m_time_source);
+ 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()
+{
+ save();
+ 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_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()));
+ m_sgf_buffer << sgf;
+ if (m_create_tree)
+ m_output_tree.add_game(bd, player_black, result, is_real_move);
+ }
+ if (m_timer() > m_save_interval)
+ {
+ save();
+ m_timer.reset();
+ }
+}
+
+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;
+}
+
+void Output::save()
+{
+ lock_guard<mutex> lock(m_mutex);
+ {
+ 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 << m_sgf_buffer.str();
+ m_sgf_buffer.str("");
+ }
+ if (m_create_tree)
+ m_output_tree.save(m_prefix + "-tree.blksgf");
+}
+
+//-----------------------------------------------------------------------------
--- /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"
+#include "libboardgame_util/Timer.h"
+#include "libboardgame_util/WallTimeSource.h"
+
+using libboardgame_util::Timer;
+using libboardgame_util::WallTimeSource;
+
+//-----------------------------------------------------------------------------
+
+/** Handles the output files of TwoGtp and their concurrent access. */
+class Output
+{
+public:
+ Output(Variant variant, const string& prefix, bool create_tree);
+
+ ~Output();
+
+ void set_save_interval(double seconds) { m_save_interval = seconds; }
+
+ 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_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;
+
+ ostringstream m_sgf_buffer;
+
+ WallTimeSource m_time_source;
+
+ Timer m_timer;
+
+ double m_save_interval = 60;
+
+ void save();
+};
+
+//-----------------------------------------------------------------------------
+
+#endif // TWOGTP_OUTPUT_H
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file twogtp/OutputTree.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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 libboardgame_util::ArrayList;
+using libpentobi_base::get_transforms;
+using libpentobi_base::ColorMove;
+using libpentobi_base::MovePoints;
+using libpentobi_base::get_transformed;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+void add(PentobiTree& tree, const SgfNode& node, bool is_player_black,
+ bool is_real_move, double 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_moves>& s1,
+ ArrayList<ColorMove, Board::max_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;
+ 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() = default; // Non-inline to avoid GCC -Winline warning
+
+void OutputTree::add_game(const Board& bd, unsigned player_black,
+ double result, const array<bool,
+ Board::max_moves>& is_real_move)
+{
+ if (bd.has_setup())
+ throw runtime_error("OutputTree: setup not supported");
+
+ // Find the canonical representation
+ ArrayList<ColorMove, Board::max_moves> sequence;
+ for (auto& transform : m_transforms)
+ {
+ ArrayList<ColorMove, Board::max_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 == nullptr)
+ {
+ 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 == nullptr)
+ 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;
+ }
+ auto 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 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 probability,
+ 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, double result,
+ const array<bool, Board::max_moves>& is_real_move);
+
+private:
+ using PointTransform = libboardgame_base::PointTransform<Point>;
+
+
+ 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 */
+//-----------------------------------------------------------------------------
+
+#include "TwoGtp.h"
+
+#include "libboardgame_sgf/Writer.h"
+#include "libboardgame_util/Log.h"
+#include "libpentobi_base/ScoreUtil.h"
+
+using libboardgame_sgf::Writer;
+using libpentobi_base::get_multiplayer_result;
+using libpentobi_base::Move;
+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;
+ get_multiplayer_result(nu_players, points, player_result,
+ m_bd.get_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(-1);
+ 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_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;
+ }
+ if (! m_bd.from_string(mv, response))
+ throw runtime_error("invalid move");
+ }
+ 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();
+ sgf_string << '\n';
+ 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();
+
+ void set_save_interval(double seconds) { m_output.set_save_interval(seconds); }
+
+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
+ )
+
+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 */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Marker.h"
+#include "libboardgame_base/Point.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using Point = libboardgame_base::Point<19 * 19, 19, 19, unsigned short>;
+using Marker = libboardgame_base::Marker<Point>;
+
+//-----------------------------------------------------------------------------
+
+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 */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Point.h"
+#include "libboardgame_base/PointTransform.h"
+#include "libboardgame_base/RectGeometry.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using Point = libboardgame_base::Point<19 * 19, 19, 19, unsigned short>;
+using RectGeometry = libboardgame_base::RectGeometry<Point>;
+
+//-----------------------------------------------------------------------------
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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, 1);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2585, 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, 1);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2575, 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, 1);
+ LIBBOARDGAME_CHECK_CLOSE_EPS(new_b.get(), 2580, 1);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_base/RectGeometryTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_base/Point.h"
+#include "libboardgame_base/RectGeometry.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+
+//-----------------------------------------------------------------------------
+
+using Point = libboardgame_base::Point<19 * 19, 19, 19, unsigned short>;
+using Geometry = libboardgame_base::Geometry<Point>;
+using RectGeometry = libboardgame_base::RectGeometry<Point>;
+using PointList = libboardgame_util::ArrayList<Point, Point::range_onboard>;
+
+//-----------------------------------------------------------------------------
+
+namespace {
+
+bool from_string(const string& s, const Geometry& geo, Point& p)
+{
+ return geo.from_string(s.begin(), s.end(), p);
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+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(from_string("a1", geo, p));
+ LIBBOARDGAME_CHECK(p == geo.get_point(0, 18));
+
+ LIBBOARDGAME_CHECK(from_string("a19", geo, p));
+ LIBBOARDGAME_CHECK(p == geo.get_point(0, 0));
+
+ LIBBOARDGAME_CHECK(from_string("A1", geo, p));
+ LIBBOARDGAME_CHECK(p == geo.get_point(0, 18));
+
+ LIBBOARDGAME_CHECK(! from_string("foobar", geo, p));
+ LIBBOARDGAME_CHECK(! from_string("a123", geo, p));
+ LIBBOARDGAME_CHECK(! from_string("a56", geo, p));
+ LIBBOARDGAME_CHECK(! from_string("aa1", geo, p));
+ LIBBOARDGAME_CHECK(! from_string("c3#", geo, 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 */
+//-----------------------------------------------------------------------------
+
+#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)
+{
+ return string_rep.read(s.begin(), s.end(), 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 */
+//-----------------------------------------------------------------------------
+
+#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(R"(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-4f);
+}
+
+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(R"(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_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 */
+//-----------------------------------------------------------------------------
+
+#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));
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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 libboardgame_gtp::Engine::exec_main_loop). */
+class InvalidResponseEngine
+ : public Engine
+{
+public:
+ InvalidResponseEngine();
+
+ void invalid_response(Response& r);
+
+ void invalid_response_2(Response& r);
+};
+
+InvalidResponseEngine::InvalidResponseEngine()
+{
+ add("invalid_response", &InvalidResponseEngine::invalid_response);
+ add("invalid_response_2", &InvalidResponseEngine::invalid_response_2);
+}
+
+void InvalidResponseEngine::invalid_response(Response& r)
+{
+ r << "This response is invalid\n"
+ << "\n"
+ << "because it contains an empty line";
+}
+
+void InvalidResponseEngine::invalid_response_2(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("known_command known_command\n");
+ ostringstream out;
+ Engine engine;
+ engine.exec_main_loop(in, out);
+ LIBBOARDGAME_CHECK_EQUAL(string("= true\n\n"), out.str());
+}
+
+LIBBOARDGAME_TEST_CASE(gtp_engine_command_with_id)
+{
+ istringstream in("10 known_command known_command\n");
+ ostringstream out;
+ Engine engine;
+ engine.exec_main_loop(in, out);
+ LIBBOARDGAME_CHECK_EQUAL(string("=10 true\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 */
+//-----------------------------------------------------------------------------
+
+#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_mcts
+ )
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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, 1);
+ node.add_value(5);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5.f, 1e-4f);
+ node.add_value(2);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5f, 1e-4f);
+}
+
+LIBBOARDGAME_TEST_CASE(libboardgame_mcts_node_add_value_remove_loss)
+{
+ libboardgame_mcts::Node<int, float, true> node;
+ node.init(0, 0.5, 0, 1);
+ node.add_value(5);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 5.f, 1e-4f);
+ node.add_value(0);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 2.5f, 1e-4f);
+ node.add_value_remove_loss(2);
+ LIBBOARDGAME_CHECK_CLOSE(node.get_value(), 3.5f, 1e-4f);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libboardgame_sgf
+ SgfNodeTest.cpp
+ SgfTreeTest.cpp
+ SgfUtilTest.cpp
+ TreeReaderTest.cpp
+)
+
+target_link_libraries(unittest_libboardgame_sgf
+ boardgame_test_main
+ boardgame_sgf
+ )
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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)
+{
+ auto parent = make_unique<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";
+ auto node = make_unique<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/SgfTreeTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_sgf/SgfTree.h"
+#include "libboardgame_test/Test.h"
+
+using namespace libboardgame_sgf;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_delete_all_variations)
+{
+ // root - node1 - node2 - node3
+ // \ node4
+ SgfTree tree;
+ auto& root = tree.get_root();
+ auto& node1 = tree.create_new_child(root);
+ auto& node2 = tree.create_new_child(node1);
+ auto& node3 = tree.create_new_child(node2);
+ auto& node4 = tree.create_new_child(node1);
+ LIBBOARDGAME_CHECK_EQUAL(root.get_nu_children(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(node1.get_nu_children(), 2u);
+ LIBBOARDGAME_CHECK_EQUAL(node2.get_nu_children(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(node3.get_nu_children(), 0u);
+ LIBBOARDGAME_CHECK_EQUAL(node4.get_nu_children(), 0u);
+ tree.clear_modified();
+ LIBBOARDGAME_CHECK(! tree.is_modified());
+ tree.delete_all_variations();
+ LIBBOARDGAME_CHECK(tree.is_modified());
+ LIBBOARDGAME_CHECK_EQUAL(root.get_nu_children(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(node1.get_nu_children(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(node2.get_nu_children(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(node3.get_nu_children(), 0u);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libboardgame_sgf/SgfUtilTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libboardgame_sgf/SgfUtil.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libboardgame_sgf;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(sgf_util_get_path_from_root)
+{
+ auto root = make_unique<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 */
+//-----------------------------------------------------------------------------
+
+#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");
+}
+
+LIBBOARDGAME_TEST_CASE(sgf_tree_reader_multiprop_with_whitespace)
+{
+ istringstream in("(;A [1]\n[2] [3]\t[4]\r\n[5])");
+ TreeReader reader;
+ reader.read(in);
+ auto& root = reader.get_tree();
+ auto values = root.get_multi_property("A");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 5u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "1");
+ LIBBOARDGAME_CHECK_EQUAL(values[1], "2");
+ LIBBOARDGAME_CHECK_EQUAL(values[2], "3");
+ LIBBOARDGAME_CHECK_EQUAL(values[3], "4");
+ LIBBOARDGAME_CHECK_EQUAL(values[4], "5");
+}
+
+/** 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 = "ü";
+ 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 */
+//-----------------------------------------------------------------------------
+
+#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_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_util
+ )
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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" };
+ auto 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" };
+ auto 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" };
+ auto 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" };
+ auto 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" };
+ auto 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" };
+ auto 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 */
+//-----------------------------------------------------------------------------
+
+#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 */
+//-----------------------------------------------------------------------------
+
+#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 */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/BoardConst.h"
+
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+
+//-----------------------------------------------------------------------------
+
+/** Test that from_string() handles null moves.
+ Used for example in pentobi/AnalyzeGameMode.cpp */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_const_from_string_null)
+{
+ auto& bc = BoardConst::get(Variant::duo);
+ Move mv;
+ LIBBOARDGAME_CHECK(bc.from_string(mv, "null"));
+ LIBBOARDGAME_CHECK(mv.is_null());
+}
+
+/** 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_to_string_ordered)
+{
+ auto& bc = BoardConst::get(Variant::duo);
+ Move mv;
+ LIBBOARDGAME_CHECK(bc.from_string(mv, "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);
+ Move mv;
+ LIBBOARDGAME_CHECK(bc.from_string(mv, "q9,q10,r10,q11,r11,s11"));
+ auto& info_ext_2 = bc.get_move_info_ext_2(mv);
+ LIBBOARDGAME_CHECK(! info_ext_2.breaks_symmetry);
+ LIBBOARDGAME_CHECK(bc.from_string(mv, "q8,r8,s8,r9,s9,s10"));
+ LIBBOARDGAME_CHECK_EQUAL(info_ext_2.symmetric_move.to_int(), mv.to_int());
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/BoardTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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)
+{
+ Move mv;
+ auto ok = bd.from_string(mv, s);
+ LIBBOARDGAME_UNUSED_IF_NOT_DEBUG(ok);
+ LIBBOARDGAME_ASSERT(ok);
+ bd.play(c, mv);
+}
+
+} // 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]
+ )
+ */
+ auto bd = make_unique<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)
+{
+ auto bd = make_unique<Board>(Variant::classic);
+ auto moves = make_unique<MoveList>();
+ auto marker = make_unique<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)
+{
+ auto bd = make_unique<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 */
+//-----------------------------------------------------------------------------
+
+#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::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);
+ auto bd = make_unique<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);
+ auto bd = make_unique<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);
+ auto bd = make_unique<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 of Color(0) 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);
+ auto bd = make_unique<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));
+}
+
+/** Test removing a piece of Color(1) with the AE property. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_empty_1)
+{
+ istringstream in("(;GM[Blokus Duo]"
+ " ;W[e8,e9,f9,d10,e10]"
+ " ;B[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);
+ auto bd = make_unique<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(5));
+ LIBBOARDGAME_CHECK_EQUAL(bd->get_points(Color(1)), ScoreType(0));
+}
+
+/** Test removing a piece in a game variant with multiple instances per
+ piece. */
+LIBBOARDGAME_TEST_CASE(pentobi_base_board_updater_setup_empty_multi_instance)
+{
+ istringstream in("(;GM[Blokus Junior];B[e10];W[j5];B[f9];AE[f9][e10])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ auto bd = make_unique<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(1));
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+add_executable(unittest_libpentobi_base
+ BoardConstTest.cpp
+ BoardTest.cpp
+ BoardUpdaterTest.cpp
+ GameTest.cpp
+ PentobiTreeTest.cpp
+ PentobiSgfUtilTest.cpp
+)
+
+target_link_libraries(unittest_libpentobi_base
+ boardgame_test_main
+ pentobi_base
+ )
+
+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 */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/Game.h"
+
+#include "libboardgame_sgf/TreeReader.h"
+#include "libboardgame_test/Test.h"
+
+using namespace std;
+using namespace libpentobi_base;
+using libboardgame_sgf::TreeReader;
+
+//-----------------------------------------------------------------------------
+
+/** 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/PentobiSgfUtilTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#include "libpentobi_base/PentobiSgfUtil.h"
+
+#include <cstring>
+#include "libboardgame_test/Test.h"
+
+using namespace libpentobi_base;
+
+//-----------------------------------------------------------------------------
+
+LIBBOARDGAME_TEST_CASE(pentobi_base_get_color_id)
+{
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_2, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::classic_3, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::duo, Color(0)), "B") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::duo, Color(1)), "W") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::junior, Color(0)), "B") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::junior, Color(1)), "W") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_2, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_3, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_3, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::trigon_3, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::nexos_2, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(2)), "3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto, Color(3)), "4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_2, Color(0)), "B") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_2, Color(1)), "W") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_3, Color(0)), "1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_3, Color(1)), "2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_color_id(Variant::callisto_3, Color(2)), "3") == 0);
+}
+
+LIBBOARDGAME_TEST_CASE(pentobi_base_get_setup_id)
+{
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_2, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::classic_3, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::duo, Color(0)), "AB") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::duo, Color(1)), "AW") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::junior, Color(0)), "AB") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::junior, Color(1)), "AW") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_2, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_3, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_3, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::trigon_3, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::nexos_2, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(2)), "A3") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto, Color(3)), "A4") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_2, Color(0)), "AB") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_2, Color(1)), "AW") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_3, Color(0)), "A1") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_3, Color(1)), "A2") == 0);
+ LIBBOARDGAME_CHECK(strcmp(get_setup_id(Variant::callisto_3, Color(2)), "A3") == 0);
+}
+
+//-----------------------------------------------------------------------------
--- /dev/null
+//-----------------------------------------------------------------------------
+/** @file unittest/libpentobi_base/PentobiTreeTest.cpp
+ @author Markus Enzenberger
+ @copyright GNU General Public License version 3 or later */
+//-----------------------------------------------------------------------------
+
+#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::InvalidProperty;
+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.to_int(), 0u);
+ 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.to_int(), 1u);
+ 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.to_int(), 2u);
+ 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.to_int(), 3u);
+ 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 InvalidProperty 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), InvalidProperty);
+}
+
+/** Check that keep_only_subtree() works in Callisto.
+ Regression test for a bug in Pentobi 12.0 */
+LIBBOARDGAME_TEST_CASE(pentobi_base_tree_keep_only_subtree_callisto)
+{
+
+ istringstream in("(;GM[Callisto]"
+ ";1[h12]"
+ ";2[m9]"
+ ";3[l8]"
+ ";4[i13]"
+ ";1[i8]"
+ ";2[p14])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ auto node = &tree.get_root();
+ node = &node->get_first_child();
+ node = &node->get_first_child();
+ node = &node->get_first_child();
+ node = &node->get_first_child();
+ node = &node->get_first_child();
+ LIBBOARDGAME_CHECK(! tree.is_modified());
+ tree.keep_only_subtree(*node);
+ LIBBOARDGAME_CHECK(tree.is_modified());
+
+ node = &tree.get_root();
+ LIBBOARDGAME_CHECK(node->has_single_child());
+ {
+ auto& values = node->get_multi_property("A1");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 2u);
+ auto begin = values.begin();
+ auto end = values.end();
+ LIBBOARDGAME_CHECK(find(begin, end, "h12") != end);
+ LIBBOARDGAME_CHECK(find(begin, end, "i8") != end);
+ }
+ {
+ auto& values = node->get_multi_property("A2");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "m9");
+ }
+ {
+ auto& values = node->get_multi_property("A3");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "l8");
+ }
+ {
+ auto& values = node->get_multi_property("A4");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "i13");
+ }
+ {
+ auto& value = node->get_property("PL");
+ LIBBOARDGAME_CHECK_EQUAL(value, "2");
+
+ node = &node->get_first_child();
+ LIBBOARDGAME_CHECK(! node->has_children());
+ }
+ {
+ auto& value = node->get_property("2");
+ LIBBOARDGAME_CHECK_EQUAL(value, "p14");
+ }
+}
+
+/** Check that keep_only_subtree() works in Nexos.
+ Regression test for a bug in Pentobi 12.0 */
+LIBBOARDGAME_TEST_CASE(pentobi_base_tree_keep_only_subtree_nexos)
+{
+
+ istringstream in("(;GM[Nexos Two-Player]"
+ ";1[g16,g18,f19,e20]"
+ ";2[r17,s18,t19,u20]"
+ ";3[t5,s6,s8,r9]"
+ ";4[e6,e8,f9,h9]"
+ ";1[m14,h15,j15,l15])");
+ TreeReader reader;
+ reader.read(in);
+ unique_ptr<SgfNode> root = reader.get_tree_transfer_ownership();
+ PentobiTree tree(root);
+ auto node = &tree.get_root();
+ node = &node->get_first_child();
+ node = &node->get_first_child();
+ node = &node->get_first_child();
+ node = &node->get_first_child();
+ LIBBOARDGAME_CHECK(! tree.is_modified());
+ tree.keep_only_subtree(*node);
+ LIBBOARDGAME_CHECK(tree.is_modified());
+
+ node = &tree.get_root();
+ LIBBOARDGAME_CHECK(node->has_single_child());
+ auto values = node->get_multi_property("A1");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "g16,g18,f19,e20");
+ values = node->get_multi_property("A2");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "r17,s18,t19,u20");
+ values = node->get_multi_property("A3");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "t5,s6,s8,r9");
+ values = node->get_multi_property("A4");
+ LIBBOARDGAME_CHECK_EQUAL(values.size(), 1u);
+ LIBBOARDGAME_CHECK_EQUAL(values[0], "e6,e8,f9,h9");
+ auto value = node->get_property("PL");
+ LIBBOARDGAME_CHECK_EQUAL(value, "1");
+
+ node = &node->get_first_child();
+ LIBBOARDGAME_CHECK(! node->has_children());
+ value = node->get_property("1");
+ LIBBOARDGAME_CHECK_EQUAL(value, "m14,h15,j15,l15");
+}
+
+/** 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
+ )
+
+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 */
+//-----------------------------------------------------------------------------
+
+#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::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);
+ auto bd = make_unique<Board>(tree.get_variant());
+ BoardUpdater updater;
+ updater.update(*bd, tree, get_last_node(tree.get_root()));
+ unsigned nu_threads = 1;
+ size_t memory = 10000;
+ auto search = make_unique<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);
+ auto bd = make_unique<Board>(tree.get_variant());
+ BoardUpdater updater;
+ updater.update(*bd, tree, get_last_node(tree.get_root()));
+ unsigned nu_threads = 1;
+ size_t memory = 10000;
+ auto search = make_unique<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());
+}
+
+//-----------------------------------------------------------------------------