From: Juhani Numminen Date: Thu, 17 Jan 2019 14:42:18 +0000 (+0000) Subject: Import pentobi_16.2.orig.tar.xz X-Git-Tag: archive/raspbian/17.3-1+rpi1~2^2^2~2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=50cb394ee797f9dbcd3a31eb6b7fcb788b811dbb;p=pentobi.git Import pentobi_16.2.orig.tar.xz [dgit import orig pentobi_16.2.orig.tar.xz] --- 50cb394ee797f9dbcd3a31eb6b7fcb788b811dbb diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..3919f9a --- /dev/null +++ b/AUTHORS @@ -0,0 +1,8 @@ +Main developer: + +Markus Enzenberger + +Translators: + +Allan Nordhøy (Norsk bokmål) +Markus Enzenberger (German, French) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a997ba5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,46 @@ +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) + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/CTestCustom.cmake b/CTestCustom.cmake new file mode 100644 index 0000000..fb76026 --- /dev/null +++ b/CTestCustom.cmake @@ -0,0 +1,4 @@ +# 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) diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..c2a7aa9 --- /dev/null +++ b/INSTALL @@ -0,0 +1,58 @@ +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. diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..de40122 --- /dev/null +++ b/NEWS @@ -0,0 +1,798 @@ +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. diff --git a/README b/README new file mode 100644 index 0000000..0096826 --- /dev/null +++ b/README @@ -0,0 +1,29 @@ +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 + +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 . + +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. diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..af3d669 --- /dev/null +++ b/data/CMakeLists.txt @@ -0,0 +1,68 @@ +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() diff --git a/data/application-x-blokus-sgf-16.svg b/data/application-x-blokus-sgf-16.svg new file mode 100644 index 0000000..7232a07 --- /dev/null +++ b/data/application-x-blokus-sgf-16.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf-32.svg b/data/application-x-blokus-sgf-32.svg new file mode 100644 index 0000000..35516b0 --- /dev/null +++ b/data/application-x-blokus-sgf-32.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf-64.svg b/data/application-x-blokus-sgf-64.svg new file mode 100644 index 0000000..83c234a --- /dev/null +++ b/data/application-x-blokus-sgf-64.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/application-x-blokus-sgf.svg b/data/application-x-blokus-sgf.svg new file mode 100644 index 0000000..9f93f57 --- /dev/null +++ b/data/application-x-blokus-sgf.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/io.sourceforge.pentobi.appdata.xml.in b/data/io.sourceforge.pentobi.appdata.xml.in new file mode 100644 index 0000000..f248e87 --- /dev/null +++ b/data/io.sourceforge.pentobi.appdata.xml.in @@ -0,0 +1,92 @@ + + + io.sourceforge.pentobi.desktop + CC0-1.0 + GPL-3.0+ + Pentobi + Pentobi + Computer opponent for the board game Blokus + Computer-Gegner für das Brettspiel Blokus + + +

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.

+

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.

+ +

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.

+

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.

+ +

System requirements: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2.5 GHz dual-core or + faster CPU recommended for playing level 9).

+

Systemminima: 1 GB RAM, 1 GHz CPU (4 GB RAM, 2,5 GHz + Dual-Core- oder schnellere CPU empfohlen für Spielstufe 9).

+ +

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.

+

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.

+
+ + + + + https://pentobi.sourceforge.io/pentobi-classic.png + Game variant Classic + Spielvariante Klassisch + + + + https://pentobi.sourceforge.io/pentobi-duo.png + Game variant Duo + Spielvariante Duo + + + + https://pentobi.sourceforge.io/pentobi-trigon.png + Game variant Trigon + Spielvariante Trigon + + + + https://pentobi.sourceforge.io/pentobi-nexos.png + Game variant Nexos + Spielvariante Nexos + + + + https://pentobi.sourceforge.io/pentobi-gembloq.png + Game variant GembloQ + Spielvariante GembloQ + + + + https://pentobi.sourceforge.io/ + https://sourceforge.net/p/pentobi/bugs/ + https://sourceforge.net/p/pentobi/donate/ + Markus Enzenberger + enz@users.sourceforge.net + + + pentobi + + + application/x-blokus-sgf + + pentobi + + + + +
diff --git a/data/io.sourceforge.pentobi.desktop.in b/data/io.sourceforge.pentobi.desktop.in new file mode 100755 index 0000000..1cef503 --- /dev/null +++ b/data/io.sourceforge.pentobi.desktop.in @@ -0,0 +1,13 @@ +[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 diff --git a/data/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in b/data/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in new file mode 100644 index 0000000..80c4074 --- /dev/null +++ b/data/io.sourceforge.pentobi.kde_thumbnailer.metainfo.xml.in @@ -0,0 +1,39 @@ + + + io.sourceforge.pentobi.kde_thumnailer + org.kde.dolphin.desktop + CC0-1.0 + GPL-3.0+ + Pentobi KDE Thumbnailer + Pentobi-Vorschaubilder unter KDE + Enables previews of game files written by Pentobi on KDE + Ermöglicht Vorschaubilder von Spieldateien, die von Pentobi geschrieben wurden, unter KDE + + +

Plugin that enables previews of Blokus game files as written by the + program Pentobi in the Dolphin file manager of the KDE desktop + environment.

+

Plug-in, das Vorschaubilder von Blokus-Spieldateien, + wie vom Programm Pentobi erzeugt, im Dateimanager Dolphin der + KDE-Desktop-Umgebung ermöglicht.

+
+ + + + + https://pentobi.sourceforge.io/pentobi-kde-thumbnailer.png + Game file previews in Dolphin + Vorschaubilder von Spieldateien in Dolphin + + + + https://pentobi.sourceforge.io/ + https://sourceforge.net/p/pentobi/bugs/ + https://sourceforge.net/p/pentobi/donate/ + Markus Enzenberger + enz@users.sourceforge.net + + + + +
diff --git a/data/pentobi-mime.xml b/data/pentobi-mime.xml new file mode 100644 index 0000000..8a0c9c2 --- /dev/null +++ b/data/pentobi-mime.xml @@ -0,0 +1,30 @@ + + + +Blokus game +Blokus-Partie +Partie de Blokus +Blokus-spill + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/pentobi.thumbnailer.in b/data/pentobi.thumbnailer.in new file mode 100644 index 0000000..dd95f90 --- /dev/null +++ b/data/pentobi.thumbnailer.in @@ -0,0 +1,3 @@ +[Thumbnailer Entry] +Exec=@CMAKE_INSTALL_FULL_BINDIR@/pentobi-thumbnailer --size %s %i %o +MimeType=application/x-blokus-sgf; diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 0000000..cbc7956 --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,7 @@ +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) diff --git a/doc/blksgf/Pentobi-SGF.html b/doc/blksgf/Pentobi-SGF.html new file mode 100644 index 0000000..befdf5f --- /dev/null +++ b/doc/blksgf/Pentobi-SGF.html @@ -0,0 +1,174 @@ + + + +Pentobi SGF Files + + + + +

Pentobi SGF Files

+
Author: Markus Enzenberger
+Last modified: 2017-09-16
+

This document describes the file format for Blokus game records as used by the +program Pentobi. The most recent +version of this document can be found in the source code distribution of +Pentobi in the folder pentobi/doc/blksgf.

+

Introduction

+

The file format is a derivative of the Smart Game Format (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 discussions +about the future SGF version 5.

+

Note
+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.

+

File Extension and MIME Type

+

The file extension .blksgf and the MIME type +application/x-blokus-sgf are used for Blokus SGF files.

+

Note
+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 .htaccess 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:

+
AddType application/x-blokus-sgf +blksgf
+

Character Set

+

UTF-8 should be used as the +character set. Pentobi always writes files in UTF-8 and indicates that with the +CA 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 CA property.

+

Game Property

+

Since there is no number for Blokus defined in SGF 4, a string instead of a +number is used as the value for the GM 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.

+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. +

Color and Player Properties

+

In game variants with two players and two colors, B denotes the +first player or color, W the second player or color. In game variants +with three or four players and one color per player, 1, 2, +3, 4 denote the first, second, third, and fourth player or +color. In game variants with two players and four colors, B denotes +the first player, W the second player, and 1, 2, +3, 4 denote the first, second, third, and fourth color. This +applies to move properties and properties related to a player or a color.

+

Example 1: in the game variant Blokus Two-Player PB is the name of +the first player, and 1 is a move of the first color.

+

Example 2: in the game variant Blokus Two-Player, one could either use the +BL, WL 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 +1L, 2L, 3L, 4L 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.)

+

Note
+Pentobi versions before 0.2 used the properties BLUE, YELLOW, +RED, GREEN 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.

+

Coordinate System

+

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.

+

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.

+

For Trigon, hexagonal boards are mapped to rectangular coordinates as in the +following example of a hexagon with edge size 3:

+
+       6     / \ / \ / \ / \
+       5   / \ / \ / \ / \ / \
+       4 / \ / \ / \ / \ / \ / \
+       3 \ / \ / \ / \ / \ / \ /
+       2   \ / \ / \ / \ / \ /
+       1     \ / \ / \ / \ /
+          a b c d e f g h i j k
+
+

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:

+
+       6 |   |   |
+       5 + - + - + -
+       4 |   |   |
+       3 + - + - + -
+       2 |   |   |
+       1 + - + - + -
+         a b c d e f
+
+

In GembloQ, each square field is divided into four triangles with their own +coordinates, like in this example:

+
+       4 | / | \ | / | \ | /
+       3 | \ | / | \ | / | \
+       2 | / | \ | / | \ | /
+       1 | \ | / | \ | / | \
+          a b c d e f g h i
+
+

Move Properties

+

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.

+

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.

+

Example: B[f9,e10,f10,g10,f11]

+

In Nexos, moves contain only the coordinates of line segments occupied by +the piece, no coordinates of junctions.

+

Note
+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.

+

Setup Properties

+

The setup properties AB, AW, A1, A2, +A3, A4 can be used to place several pieces simultaneously on +the board. The setup property AE 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 PL can be used +to set the color to play in a setup position.

+

Example:
+AB[e8,e9,f9,d10,e10][g6,f7,g7,h7,g8]
+AW[i4,h5,i5,j5,i6][j7,j8,j9,k9,j10]
+PL[B]

+

Note
+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.

+ + diff --git a/doc/gtp/Pentobi-GTP.html b/doc/gtp/Pentobi-GTP.html new file mode 100644 index 0000000..16fcc1b --- /dev/null +++ b/doc/gtp/Pentobi-GTP.html @@ -0,0 +1,332 @@ + + + +Pentobi GTP Interface + + + + +

Pentobi GTP Interface

+
Author: Markus Enzenberger
+

This document describes the text-based interface to the engine of the Blokus +program Pentobi. The interface is +an adaption of the Go Text +Protocol (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.

+

Go Text Protocol

+

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: = for success, ? for failure, followed by +the actual response. The response ends with two consecutive newline characters. +See the GTP +specification for details.

+

Controllers

+

To use the engine from a controller program, the controller typically +creates a child process by running pentobi-gtp 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 +java.lang.Runtime.exec().

+

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 pentobi-gtp 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 pentobi-gtp with +the command line option --quiet, but it is generally better to assume +that a GTP engine writes text to standard error.

+

An example for a controller written in C++ for Linux is included in Pentobi +since version 9.0 in src/twogtp. 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 +tools/twogtp/twogtp.py.

+

Building

+

Since the GTP engine is a developer tool, building it is not enabled by +default. To enable it, run cmake with the option +-DPENTOBI_BUILD_GTP=ON. After building, there will be an executable in +the build directory named src/pentobi_gtp/pentobi-gtp. 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 +-DPENTOBI_BUILD_GUI=OFF.

+

Options

+

The following command-line options are supported by +pentobi-gtp:

+
+
--book file
+
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 +src/books). If no opening book is specified and opening books are not +disabled, pentobi-gtp 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 src/books. If no such file is found it +will print an error message to standard error and disable the use of opening +books.
+
--config,-c file
+
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).
+
--color
+
Use ANSI escape sequences to colorize the text output of boards (for +example in the response to the showboard command or with the +--showboard command line option).
+
--cputime
+
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.
+
--game,-g variant
+
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.
+
--help,-h
+
Print a list of the command-line options and exit.
+
--level,-l n
+
Set the level of playing strength to n. Valid values are 1 to 9.
+
--seed,-r n
+
Use n 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.
+
--showboard
+
Automatically write a text representation of the current position to +standard error after each command that alters the position.
+
--nobook
+
Disable the use of opening books.
+
--noresign
+
Disable resignation. If resignation is disabled, the genmove +command will never respond with resign. Resignation can speed up the +playing of test games if only the win/loss information is wanted.
+
--quiet,-q
+
Do not print any debugging messages, errors or warnings to standard +error.
+
--threads n
+
Use n 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.
+
--version,-v
+
Print the version of Pentobi and exit.
+
+

Commands

+

Standard Commands

+

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 (B, W if +two colors; 1, 2, 3, 4 if more than two). +Moves in arguments or responses are represented as in the move property values +of blksgf files. See the specification for Pentobi SGF files for +details.

+
+
all_legal color
+
List all legal moves for a color.
+
clear_board
+
Clear the board and start a new game in the current game variant.
+
final_score
+
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. B+2 if the first player wins with two points, or +0 for a draw). In game variants with more than two players, the +response is a list of the points for each player (e.g. +64 69 70 40). If the current position is not a final +position, the response is undefined.
+
genmove color
+
Generate and play a move for a given color in the current position. If the +color has no more moves, the response is pass. If resignation is not +disabled, the response is resign if the players is very likely to +lose. Otherwise the response is the move.
+
known_command command
+
The response is true if command is a GTP command supported +by the engine, false otherwise.
+
list_commands
+
List all supported GTP commands, one command per line.
+
loadsgf file [move_number]
+
Load a board position from a blksgf file with name file. If +move_number is specified, the board position will be set to the position +in the main variation of the file before the move with the given number +was played, otherwise to the last position in the main variation.
+
name
+
Return the name of the GTP engine (Pentobi).
+
play color move
+
Play a move for a given color in the current board position.
+
quit
+
Exit the command loop and quit the engine.
+
reg_genmove color
+
Like the genmove command, but only generates a move and does not +play it on the board.
+
showboard
+
Return a text representation of the current board position.
+
undo
+
Undo the last move played.
+
version
+
Return the version of Pentobi.
+
+

Generally Useful Extension Commands

+
+
cputime
+
Return the CPU time used by the engine since the start of the program.
+
g
+
Shortcut for the genmove command with the color argument set to +the current color to play.
+
get_place color
+
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 shared is +appended to the place number.
+
get_value
+
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 +reg_genmove or genmove 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 get_value command is +used.
+
p move
+
Shortcut for the play command with the color argument set to the +current color to play.
+
param [key value]
+
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: +
+
+
avoid_symmetric_draw 0|1
+
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 1) by default.
+
fixed_simulations n
+
Use exactly n 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.
+
use_book 0|1
+
Enable or disable the opening book.
+
+
+The other parameters are only interesting for developers.
+
param_base [key value]
+
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. +
+
+
accept_illegal 0|1
+
Accept move arguments to the play command that violate the rules +of the game. If disabled, the play command will respond with an error, +otherwise it will perform the moves.
+
resign 0|1
+
Allow the engine to respond with resign to the genmove +command.
+
+
+
+
set_game variant
+
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. +Blokus Duo, see the specification for Pentobi SGF files for +details).
+
set_random_seed n
+
Set the seed of the random generator to n. See the documentation for +the command-line option --seed.
+
+

Extension Commands for Developers

+The remaining commands are only interesting for developers. See Pentobi's +source code for details. +

Example

+

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.

+
+$ ./pentobi-gtp --quiet
+name
+= Pentobi
+
+version
+= 7.1
+
+set_game Blokus Duo
+=
+
+play b e8,d9,e9,f9,e10
+=
+
+genmove w
+= i4,h5,i5,j5,i6
+
+showboard
+=
+   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
+
+quit
+=
+
+
+ + diff --git a/doc/help.qrc b/doc/help.qrc new file mode 100644 index 0000000..61cdc15 --- /dev/null +++ b/doc/help.qrc @@ -0,0 +1,54 @@ + + + + help/C/pentobi/analysis.jpg + help/C/pentobi/become_stronger.html + help/C/pentobi/board_callisto.png + help/C/pentobi/board_classic.png + help/C/pentobi/board_duo.png + help/C/pentobi/board_gembloq.png + help/C/pentobi/board_nexos.png + help/C/pentobi/board_trigon.jpg + help/C/pentobi/callisto_rules.html + help/C/pentobi/classic_rules.html + help/C/pentobi/duo_rules.html + help/C/pentobi/gembloq_rules.html + help/C/pentobi/index.html + help/C/pentobi/junior_rules.html + help/C/pentobi/license.html + help/C/pentobi/nexos_rules.html + help/C/pentobi/pieces_callisto.png + help/C/pentobi/pieces_gembloq.jpg + help/C/pentobi/pieces_junior.png + help/C/pentobi/pieces_nexos.png + help/C/pentobi/pieces.png + help/C/pentobi/pieces_trigon.jpg + help/C/pentobi/position_callisto.png + help/C/pentobi/position_classic.png + help/C/pentobi/position_duo.png + help/C/pentobi/position_gembloq.png + help/C/pentobi/position_nexos.png + help/C/pentobi/position_trigon.jpg + help/C/pentobi/rating.jpg + help/C/pentobi/shortcuts.html + help/C/pentobi/stylesheet.css + help/C/pentobi/system.html + help/C/pentobi/trigon_rules.html + help/C/pentobi/user_interface.html + help/C/pentobi/window_menu.html + help/de/pentobi/become_stronger.html + help/de/pentobi/callisto_rules.html + help/de/pentobi/classic_rules.html + help/de/pentobi/duo_rules.html + help/de/pentobi/gembloq_rules.html + help/de/pentobi/index.html + help/de/pentobi/junior_rules.html + help/de/pentobi/license.html + help/de/pentobi/nexos_rules.html + help/de/pentobi/shortcuts.html + help/de/pentobi/system.html + help/de/pentobi/trigon_rules.html + help/de/pentobi/user_interface.html + help/de/pentobi/window_menu.html + + diff --git a/doc/help/C/pentobi/analysis.jpg b/doc/help/C/pentobi/analysis.jpg new file mode 100644 index 0000000..3abdcd9 Binary files /dev/null and b/doc/help/C/pentobi/analysis.jpg differ diff --git a/doc/help/C/pentobi/become_stronger.html b/doc/help/C/pentobi/become_stronger.html new file mode 100644 index 0000000..bc58961 --- /dev/null +++ b/doc/help/C/pentobi/become_stronger.html @@ -0,0 +1,61 @@ + + + +Pentobi Help + + + + + + +

Become a Stronger Player

+

Pentobi has functionality that can help you to become a stronger Blokus +player.

+

Game Analysis

+

A game can be analyzed by selecting Analyze Game from the +Tools 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.

+
+
Analysis of a game of variant Classic (2 players)
+

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.

+

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 Play Single Move from the Computer menu.

+

Determine Your Rating

+

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.

+

A rated game is started with Rated Game from the Game 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.

+

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.

+

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.

+

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.

+
+
Window with rating graph
+

You can always see your current rating by selecting Rating from the +Tools 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.

+ + + diff --git a/doc/help/C/pentobi/board_callisto.png b/doc/help/C/pentobi/board_callisto.png new file mode 100644 index 0000000..89a2fc7 Binary files /dev/null and b/doc/help/C/pentobi/board_callisto.png differ diff --git a/doc/help/C/pentobi/board_classic.png b/doc/help/C/pentobi/board_classic.png new file mode 100644 index 0000000..9d9a543 Binary files /dev/null and b/doc/help/C/pentobi/board_classic.png differ diff --git a/doc/help/C/pentobi/board_duo.png b/doc/help/C/pentobi/board_duo.png new file mode 100644 index 0000000..b1f5259 Binary files /dev/null and b/doc/help/C/pentobi/board_duo.png differ diff --git a/doc/help/C/pentobi/board_gembloq.png b/doc/help/C/pentobi/board_gembloq.png new file mode 100644 index 0000000..a79ea02 Binary files /dev/null and b/doc/help/C/pentobi/board_gembloq.png differ diff --git a/doc/help/C/pentobi/board_nexos.png b/doc/help/C/pentobi/board_nexos.png new file mode 100644 index 0000000..4fd32c8 Binary files /dev/null and b/doc/help/C/pentobi/board_nexos.png differ diff --git a/doc/help/C/pentobi/board_trigon.jpg b/doc/help/C/pentobi/board_trigon.jpg new file mode 100644 index 0000000..886514d Binary files /dev/null and b/doc/help/C/pentobi/board_trigon.jpg differ diff --git a/doc/help/C/pentobi/callisto_rules.html b/doc/help/C/pentobi/callisto_rules.html new file mode 100644 index 0000000..22f2b5c --- /dev/null +++ b/doc/help/C/pentobi/callisto_rules.html @@ -0,0 +1,43 @@ + + + +Pentobi Help + + + + + + +

Callisto Rules

+

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.

+
+
The 21 pieces
+

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.

+
+
The board with the center having a darker color
+

All larger pieces may be placed anywhere on the board but must touch an +existing piece of the same color edge-to-edge.

+
+
An example position after a few moves
+

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.

+

Rules for two or three players

+

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.

+ + + diff --git a/doc/help/C/pentobi/classic_rules.html b/doc/help/C/pentobi/classic_rules.html new file mode 100644 index 0000000..c454db3 --- /dev/null +++ b/doc/help/C/pentobi/classic_rules.html @@ -0,0 +1,57 @@ + + + +Pentobi Help + + + + + + +

Classic Rules

+

There are four players, Blue, Yellow, Red and Green, and a board consisting +of 20×20 squares.

+

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.

+
+
The 21 pieces
+

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.

+
+
The 20×20 board with the starting squares marked with +colored dots
+

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.

+
+
An example position after a few moves
+

When the player of a color cannot place any more pieces, the player passes +and the next color continues.

+

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.

+

Rules for Two Players

+

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.

+

Rules for Three Players

+

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.

+

Colorless starting points

+

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.

+ + + diff --git a/doc/help/C/pentobi/duo_rules.html b/doc/help/C/pentobi/duo_rules.html new file mode 100644 index 0000000..feadbd7 --- /dev/null +++ b/doc/help/C/pentobi/duo_rules.html @@ -0,0 +1,26 @@ + + + +Pentobi Help + + + + + + +

Duo Rules

+

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.

+
+
The 14×14 board used in game variant Duo with the starting +squares marked with colored dots
+
+
An example position in game variant Duo
+ + + diff --git a/doc/help/C/pentobi/gembloq_rules.html b/doc/help/C/pentobi/gembloq_rules.html new file mode 100644 index 0000000..df6fd3e --- /dev/null +++ b/doc/help/C/pentobi/gembloq_rules.html @@ -0,0 +1,43 @@ + + + +Pentobi Help + + + + + + +

GembloQ Rules

+

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.

+
+
The 21 pieces
+

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.

+
+
The board for GembloQ with the starting points marked with +colored dots
+

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.

+
+
An example position after a few moves
+

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.

+

Rules for Two and Three Players

+

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.

+ + + diff --git a/doc/help/C/pentobi/index.html b/doc/help/C/pentobi/index.html new file mode 100644 index 0000000..1b8d11e --- /dev/null +++ b/doc/help/C/pentobi/index.html @@ -0,0 +1,31 @@ + + + +Pentobi Help + + + + + + +

Pentobi

+

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.

+

Classic Rules
+Duo Rules
+Trigon Rules
+Junior Rules
+Nexos Rules
+GembloQ Rules
+Callisto Rules
+How to Use Pentobi
+Become a Stronger Player
+Window Menu and Toolbar
+Keyboard Shortcuts
+System Requirements
+License

+ + + diff --git a/doc/help/C/pentobi/junior_rules.html b/doc/help/C/pentobi/junior_rules.html new file mode 100644 index 0000000..c55712a --- /dev/null +++ b/doc/help/C/pentobi/junior_rules.html @@ -0,0 +1,22 @@ + + + +Pentobi Help + + + + + + +

Junior Rules

+

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.

+
+
The 24 pieces used in Junior
+

Bonus points are not used in Junior.

+ + + diff --git a/doc/help/C/pentobi/license.html b/doc/help/C/pentobi/license.html new file mode 100644 index 0000000..c96d9df --- /dev/null +++ b/doc/help/C/pentobi/license.html @@ -0,0 +1,29 @@ + + + +Pentobi Help + + + + + + +

License

+

Copyright © 2011–2018 Markus Enzenberger

+

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.

+

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.

+ + + diff --git a/doc/help/C/pentobi/nexos_rules.html b/doc/help/C/pentobi/nexos_rules.html new file mode 100644 index 0000000..5989590 --- /dev/null +++ b/doc/help/C/pentobi/nexos_rules.html @@ -0,0 +1,42 @@ + + + +Pentobi Help + + + + + + +

Nexos Rules

+

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.

+
+
The 24 pieces
+

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.

+
+
The board for Nexos with the starting intersections marked +with colored dots
+

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.

+
+
An example position after a few moves
+

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.

+

Rules for Two Players

+

Like Blokus, Nexos can be played with two players by having one player play +Blue and Red and the other player Yellow and Green.

+ + + diff --git a/doc/help/C/pentobi/pieces.png b/doc/help/C/pentobi/pieces.png new file mode 100644 index 0000000..0bce3bf Binary files /dev/null and b/doc/help/C/pentobi/pieces.png differ diff --git a/doc/help/C/pentobi/pieces_callisto.png b/doc/help/C/pentobi/pieces_callisto.png new file mode 100644 index 0000000..2d5d01b Binary files /dev/null and b/doc/help/C/pentobi/pieces_callisto.png differ diff --git a/doc/help/C/pentobi/pieces_gembloq.jpg b/doc/help/C/pentobi/pieces_gembloq.jpg new file mode 100644 index 0000000..522d440 Binary files /dev/null and b/doc/help/C/pentobi/pieces_gembloq.jpg differ diff --git a/doc/help/C/pentobi/pieces_junior.png b/doc/help/C/pentobi/pieces_junior.png new file mode 100644 index 0000000..724bd0e Binary files /dev/null and b/doc/help/C/pentobi/pieces_junior.png differ diff --git a/doc/help/C/pentobi/pieces_nexos.png b/doc/help/C/pentobi/pieces_nexos.png new file mode 100644 index 0000000..228495f Binary files /dev/null and b/doc/help/C/pentobi/pieces_nexos.png differ diff --git a/doc/help/C/pentobi/pieces_trigon.jpg b/doc/help/C/pentobi/pieces_trigon.jpg new file mode 100644 index 0000000..a22a5ef Binary files /dev/null and b/doc/help/C/pentobi/pieces_trigon.jpg differ diff --git a/doc/help/C/pentobi/position_callisto.png b/doc/help/C/pentobi/position_callisto.png new file mode 100644 index 0000000..c4097ce Binary files /dev/null and b/doc/help/C/pentobi/position_callisto.png differ diff --git a/doc/help/C/pentobi/position_classic.png b/doc/help/C/pentobi/position_classic.png new file mode 100644 index 0000000..4bdf32b Binary files /dev/null and b/doc/help/C/pentobi/position_classic.png differ diff --git a/doc/help/C/pentobi/position_duo.png b/doc/help/C/pentobi/position_duo.png new file mode 100644 index 0000000..96e021f Binary files /dev/null and b/doc/help/C/pentobi/position_duo.png differ diff --git a/doc/help/C/pentobi/position_gembloq.png b/doc/help/C/pentobi/position_gembloq.png new file mode 100644 index 0000000..768ed05 Binary files /dev/null and b/doc/help/C/pentobi/position_gembloq.png differ diff --git a/doc/help/C/pentobi/position_nexos.png b/doc/help/C/pentobi/position_nexos.png new file mode 100644 index 0000000..be1c5bc Binary files /dev/null and b/doc/help/C/pentobi/position_nexos.png differ diff --git a/doc/help/C/pentobi/position_trigon.jpg b/doc/help/C/pentobi/position_trigon.jpg new file mode 100644 index 0000000..fa1dd82 Binary files /dev/null and b/doc/help/C/pentobi/position_trigon.jpg differ diff --git a/doc/help/C/pentobi/rating.jpg b/doc/help/C/pentobi/rating.jpg new file mode 100644 index 0000000..2bb05d4 Binary files /dev/null and b/doc/help/C/pentobi/rating.jpg differ diff --git a/doc/help/C/pentobi/shortcuts.html b/doc/help/C/pentobi/shortcuts.html new file mode 100644 index 0000000..589ffc4 --- /dev/null +++ b/doc/help/C/pentobi/shortcuts.html @@ -0,0 +1,77 @@ + + + +Pentobi Help + + + + + + +

Keyboard Shortcuts

+

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.

+
+
Plus
+
+

Select next piece

+
+
Minus
+
+

Select previous piece

+
+
Escape
+
+

Clear selected piece

+
+
Left, Right, Up, Down, Shift+Left, Shift+Right, Shift+Up, Shift+Down
+
+

Move the selected piece. The Shift key makes the piece move faster.

+
+
Space
+
+

Next orientation of the selected piece

+
+
Shift+Space
+
+

Previous orientation of the selected piece

+
+
Enter
+
+

Play the selected piece.

+
+
1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z
+
+

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").

+
+
Ctrl+Home, Ctrl+Shift+Left, Ctrl+Left, Ctrl+Right, Ctrl+Shift+Right, +Ctrl+End, Ctrl+Up, Ctrl+Down
+
+

Navigate in the game: beginning, ten moves backward, backward, forward, ten +moves forward, end, previous variation, next variation.

+
+
Ctrl+Shift+H
+
+

Like Find Move (Ctrl+H) but iterates backwards through the list of +legal moves.

+
+
Ctrl+T
+
+

Switch view between comment and game analysis.

+
+
Alt+M
+
+

Open menu.

+
+
+ + + diff --git a/doc/help/C/pentobi/stylesheet.css b/doc/help/C/pentobi/stylesheet.css new file mode 100644 index 0000000..9f1d52f --- /dev/null +++ b/doc/help/C/pentobi/stylesheet.css @@ -0,0 +1,51 @@ +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; +} diff --git a/doc/help/C/pentobi/system.html b/doc/help/C/pentobi/system.html new file mode 100644 index 0000000..1a5139c --- /dev/null +++ b/doc/help/C/pentobi/system.html @@ -0,0 +1,23 @@ + + + +Pentobi Help + + + + + + +

System Requirements

+

Minimum: 1 GB RAM, 1 GHz CPU
+Recommended for playing level 9: 4 GB RAM, 2.5 GHz dual-core or +faster CPU

+

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).

+ + + diff --git a/doc/help/C/pentobi/trigon_rules.html b/doc/help/C/pentobi/trigon_rules.html new file mode 100644 index 0000000..d04a3e4 --- /dev/null +++ b/doc/help/C/pentobi/trigon_rules.html @@ -0,0 +1,41 @@ + + + +Pentobi Help + + + + + + +

Trigon Rules

+

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.

+
+
The 22 Trigon pieces
+

The board also consists of triangles and is shaped like a hexagon with an +edge size of nine triangles.

+
+
The board with the starting fields marked with gray +dots
+

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.

+
+
An example position after a few moves
+

Rules for Two Players

+

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.

+

Rules for Three Players

+

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.

+ + + diff --git a/doc/help/C/pentobi/user_interface.html b/doc/help/C/pentobi/user_interface.html new file mode 100644 index 0000000..b762b15 --- /dev/null +++ b/doc/help/C/pentobi/user_interface.html @@ -0,0 +1,59 @@ + + + +Pentobi Help + + + + + + +

How to Use Pentobi

+

Board

+

Pieces can be selected by clicking on one of the unplayed pieces or by using +shortcut keys. 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.

+

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).

+

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.

+

Playing Against the Computer

+

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.

+

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 +Settings from the Computer menu or toolbar and select the colors +the computer should play.

+

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.

+

Selecting Play from the Computer 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.

+

Move Variations and the Game Tree

+

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 Go menu and the navigation buttons.

+

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 Forward button). The main variation is supposed to +represent the real game played. If you want a side variation to become the main +variation, select Make Main Variation from the Edit menu.

+ + + diff --git a/doc/help/C/pentobi/window_menu.html b/doc/help/C/pentobi/window_menu.html new file mode 100644 index 0000000..35113c3 --- /dev/null +++ b/doc/help/C/pentobi/window_menu.html @@ -0,0 +1,222 @@ + + + +Pentobi Help + + + + + + +

Window Menu and Toolbar

+

Navigation Buttons in Toolbar

+
+
Beginning
+
Go to the beginning of the game.
+
Backward 10
+
Go ten moves backward in the current variation. The button supports +autorepeat if pressed and held.
+
Backward
+
Go one move backward in the current variation. The button supports +autorepeat if pressed and held.
+
Forward
+
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.
+
Forward 10
+
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.
+
End
+
Go to the end of the current variation. Like Forward, this also uses +the first variation in positions with several follow-up variations.
+
Next Variation
+
Go to the next variation to the last move played (i.e. the next sibling +node of the current node in the game tree).
+
Previous Variation
+
Go to the previous variation to the last move played (i.e. the previous +sibling node of the current node in the game tree).
+
+

Game Menu

+
+
New
+
Start a new game.
+
Rated Game
+
Start a new rated game against +the computer.
+
Game Variant
+
Select a game variant and start a new game of this game variant.
+
Game Info
+
Display or edit additional information about the game like the name of the +players or the date when the game was played.
+
Undo Move
+
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 Edit/Truncate to remove inner nodes of the +game tree).
+
Find Move
+
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.
+
Open
+
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.
+
Open Recent
+
Load a recently used game.
+
Open Clipboard
+
Open a game from a text copied to the clipboard. The text must be a valid +game in Pentobi SGF file format.
+
Save
+
Save the current game.
+
Save As
+
Save the current game under a new file name.
+
Export/Image
+
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.
+
Export/ASCII Art
+
Save the current position as a text diagram. The text diagram should be +viewed using a monospace font.
+
Quit
+
Quit Pentobi.
+
+

Go Menu

+
+
Move Number
+
Go to the move with a given move number in the current variation.
+
Main Variation
+
Go back to the last position in the current variation that belonged to the +main variation.
+
Beginning of Branch
+
Go back to the last position in the current variation that had an +alternative move.
+
Next Comment
+
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.
+
+

Edit Menu

+
+
Annotation
+
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 Move Marking, on the board.
+
Make Main Variation
+
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.
+
Variation Up
+
Changes the order of variations such that the current position will appear +earlier when iterating over the variations with Next/Previous +Variation.
+
Variation Down
+
Changes the order of variations such that the current position will appear +later when iterating over the variations with Next/Previous +Variation.
+
Delete Variations
+
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 Back +to Main Variation.
+
Truncate
+
Remove the node with the current position, including any subtree, from the +game tree.
+
Truncate Children
+
Remove all child nodes of the node with the current position from the game +tree.
+
Keep Position
+
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.
+
Keep Subtree
+
Like Keep Position but does not delete the moves after the current +position.
+
Setup Mode
+
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 Next Color 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.
+
Next Color
+
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.
+
+

View Menu

+
+
Appearance
+
+
+
Coordinates
+
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.
+
Show variations
+
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.
+
Move number
+
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.
+
Move marking
+
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.
+
Show comment
+
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.
+
+
+
Comment
+
Toggle the visibility of the comment area in the current position.
+
+
+
Fullscreen
+
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.
+
+

Computer Menu

+
+
Settings
+
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.
+
Play
+
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.
+
Play Move
+
Make the computer play a single move for the current color without changing +the colors played by the computer.
+
Stop
+
Abort the current move generation. You can make the computer continue to +play by selecting Play.
+
+

Tools Menu

+
+
Rating
+
Show a dialog window with the rating of the user in the current game +variant.
+
Analyze Game
+
Perform a game analysis.
+
+

Help Menu

+
+
Pentobi Help
+
Show a window to browse the Pentobi user manual.
+
About Pentobi
+
Show an info dialog with information about this version of Pentobi.
+
+ + + diff --git a/doc/help/de/pentobi/become_stronger.html b/doc/help/de/pentobi/become_stronger.html new file mode 100644 index 0000000..de35692 --- /dev/null +++ b/doc/help/de/pentobi/become_stronger.html @@ -0,0 +1,66 @@ + + + +Pentobi-Hilfe + + + + + + +

Ein stärkerer Spieler werden

+

Pentobi besitzt Funktionen, die Ihnen helfen können, ein stärkerer +Blokus-Spieler zu werden.

+

Spielanalyse

+

Sie können ein Spiel analysieren, indem Sie Spiel analysieren aus dem +Extras-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.

+
+
Analyse eines Spiels der Spielvariante Klassisch (2 +Spieler)
+

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.

+

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 +Einzelnen Zug spielen aus dem Computer-Menü auswählen.

+

Ihre Wertung ermitteln

+

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.

+

Ein gewertetes Spiel wird mit Gewertetes Spiel aus dem +Spiel-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.

+

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.

+

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.

+

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.

+
+
Fenster mit Wertungsgraph
+

Sie können Ihre aktuelle Wertung jederzeit mit Wertung aus dem +Extras-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.

+ + + diff --git a/doc/help/de/pentobi/callisto_rules.html b/doc/help/de/pentobi/callisto_rules.html new file mode 100644 index 0000000..0329956 --- /dev/null +++ b/doc/help/de/pentobi/callisto_rules.html @@ -0,0 +1,48 @@ + + + +Pentobi-Hilfe + + + + + + +

Callisto-Regeln

+

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.

+
+
Die 21 Spielsteine
+

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.

+
+
Das Brett mit einer dunkleren Farbe im Zentrum
+

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.

+
+
Eine Beispielstellung nach ein paar Zügen
+

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.

+

Regeln für zwei oder drei Spieler

+

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.

+ + + diff --git a/doc/help/de/pentobi/classic_rules.html b/doc/help/de/pentobi/classic_rules.html new file mode 100644 index 0000000..857849c --- /dev/null +++ b/doc/help/de/pentobi/classic_rules.html @@ -0,0 +1,59 @@ + + + +Pentobi-Hilfe + + + + + + +

Klassische Regeln

+

Es gibt vier Spieler, Blau, Gelb, Rot und Grün, und ein Brett, das aus 20×20 +Quadraten besteht.

+

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.

+
+
Die 21 Spielsteine
+

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.

+
+
Das 20×20-Brett mit den durch farbige Punkte markierten +Startfeldern
+

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.

+
+
Eine Beispielstellung nach ein paar Zügen
+

Wenn der Spieler einer Farbe keine Spielsteine mehr setzen kann, muss der +Spieler aussetzen und die nächste Farbe ist am Zug.

+

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.

+

Regeln für zwei Spieler

+

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.

+

Regeln für drei Spieler

+

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.

+

Farblose Startfelder

+

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.

+ + + diff --git a/doc/help/de/pentobi/duo_rules.html b/doc/help/de/pentobi/duo_rules.html new file mode 100644 index 0000000..36d2041 --- /dev/null +++ b/doc/help/de/pentobi/duo_rules.html @@ -0,0 +1,26 @@ + + + +Pentobi-Hilfe + + + + + + +

Duo-Regeln

+

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.

+
+
Das 14×14-Brett, das in der Spielvariante Duo benutzt +wird, mit den durch farbige Punkte markierten Startfeldern
+
+
Eine Beispielstellung in der Spielvariante Duo
+ + + diff --git a/doc/help/de/pentobi/gembloq_rules.html b/doc/help/de/pentobi/gembloq_rules.html new file mode 100644 index 0000000..c73d0ee --- /dev/null +++ b/doc/help/de/pentobi/gembloq_rules.html @@ -0,0 +1,45 @@ + + + +Pentobi-Hilfe + + + + + + +

GembloQ-Regeln

+

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.

+
+
Die 21 Spielsteine
+

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.

+
+
Das Brett für GembloQ mit den durch farbige Punkte +markierten Startfeldern
+

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.

+
+
Eine Beispielstellung nach ein paar Zügen
+

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.

+

Regeln für zwei und drei Spieler

+

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.

+ + + diff --git a/doc/help/de/pentobi/index.html b/doc/help/de/pentobi/index.html new file mode 100644 index 0000000..19e4a35 --- /dev/null +++ b/doc/help/de/pentobi/index.html @@ -0,0 +1,32 @@ + + + +Pentobi-Hilfe + + + + + + +

Pentobi

+

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.

+

Klassische Regeln
+Duo-Regeln
+Trigon-Regeln
+Junior-Regeln
+Nexos-Regeln
+GembloQ-Regeln
+Callisto-Regeln
+Wie Sie Pentobi benutzen
+Ein stärkerer Spieler werden
+Fenstermenü und Werkzeugleiste
+Tastenkürzel
+Systemvoraussetzungen
+Lizenz

+ + + diff --git a/doc/help/de/pentobi/junior_rules.html b/doc/help/de/pentobi/junior_rules.html new file mode 100644 index 0000000..b95897d --- /dev/null +++ b/doc/help/de/pentobi/junior_rules.html @@ -0,0 +1,23 @@ + + + +Pentobi-Hilfe + + + + + + +

Junior-Regeln

+

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.

+
+
Die 24 Spielsteine, die in Junior benutzt werden
+

Bonuspunkte werden in Junior nicht benutzt.

+ + + diff --git a/doc/help/de/pentobi/license.html b/doc/help/de/pentobi/license.html new file mode 100644 index 0000000..d154427 --- /dev/null +++ b/doc/help/de/pentobi/license.html @@ -0,0 +1,29 @@ + + + +Pentobi-Hilfe + + + + + + +

Lizenz

+

Copyright © 2011–2018 Markus Enzenberger

+

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.

+

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.

+

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.

+ + + diff --git a/doc/help/de/pentobi/nexos_rules.html b/doc/help/de/pentobi/nexos_rules.html new file mode 100644 index 0000000..e9501f5 --- /dev/null +++ b/doc/help/de/pentobi/nexos_rules.html @@ -0,0 +1,43 @@ + + + +Pentobi-Hilfe + + + + + + +

Nexos-Regeln

+

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.

+
+
Die 24 Spielsteine
+

Jede Farbe hat einen Startkreuzungspunkt auf der Kreuzung der dritten Linien +nahe einer Ecke. Der erste Spielstein muss den Startkreuzungspunkt +berühren.

+
+
Das Brett für Nexos mit den durch farbige Punkte +markierten Startkreuzungspunkten
+

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.

+
+
Eine Beispielstellung nach ein paar Zügen
+

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.

+

Regeln für zwei Spieler

+

Wie Blokus kann Nexos von zwei Spielern gespielt werden, indem ein Spieler +Rot und Blau, und der andere Spieler Gelb und Grün spielt.

+ + + diff --git a/doc/help/de/pentobi/shortcuts.html b/doc/help/de/pentobi/shortcuts.html new file mode 100644 index 0000000..4e51174 --- /dev/null +++ b/doc/help/de/pentobi/shortcuts.html @@ -0,0 +1,81 @@ + + + +Pentobi-Hilfe + + + + + + +

Tastenkürzel

+

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.

+
+
Plus
+
+

Nächsten Spielstein auswählen

+
+
Minus
+
+

Vorherigen Spielstein auswählen

+
+
Escape
+
+

Spielsteinauswahl löschen

+
+
Links, Rechts, Oben, Unten, Umschalt+Links, Umschalt+Rechts, Umschalt+Oben, +Umschalt+Unten
+
+

Bewegen des ausgewählten Spielsteins. Mit der Umschalttaste wird der +Spielstein schneller bewegt.

+
+
Leertaste
+
+

Nächste Ausrichtung des ausgewählten Spielsteins

+
+
Umschalt+Leertaste
+
+

Vorherige Ausrichtung des ausgewählten Spielsteins

+
+
Enter
+
+

Spielen des ausgewählten Spielsteins.

+
+
1, 2, A, C, E, F, G, H, I, J, L, N, O, P, S, T, U, V, W, X, Y, Z
+
+

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“).

+
+
Strg+Pos1, Strg+Umschalt+Links, Strg+Links, Strg+Rechts, +Strg+Umschalt+Rechts, Strg+Ende, Strg+Oben, Strg+Unten
+
+

Im Spiel navigieren: Anfang, zehn Züge zurück, zurück, vorwärts, zehn Züge +vorwärts, Ende, vorherige Variante, nächste Variante.

+
+
Strg+Umschalt+H
+
+

Wie Zug finden (Strg+H), jedoch wird rückwärts durch die Liste der +legalen Züge iteriert.

+
+
Strg+T
+
+

Ansicht zwischen Kommentar und Spielanalyse umschalten.

+
+
Alt+M
+
+

Menü öffnen.

+
+
+ + + diff --git a/doc/help/de/pentobi/system.html b/doc/help/de/pentobi/system.html new file mode 100644 index 0000000..3ebaf8a --- /dev/null +++ b/doc/help/de/pentobi/system.html @@ -0,0 +1,23 @@ + + + +Pentobi-Hilfe + + + + + + +

Systemvoraussetzungen

+

Minimum: 1 GB RAM, 1 GHz CPU
+Empfohlen für Spielstufe 9: 4 GB RAM, 2,5 GHz Dual-Core- oder +schnellere CPU

+

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).

+ + + diff --git a/doc/help/de/pentobi/trigon_rules.html b/doc/help/de/pentobi/trigon_rules.html new file mode 100644 index 0000000..7cfb6a9 --- /dev/null +++ b/doc/help/de/pentobi/trigon_rules.html @@ -0,0 +1,44 @@ + + + +Pentobi-Hilfe + + + + + + +

Trigon-Regeln

+

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.

+
+
Die 22 Trigon-Spielsteine
+

Das Spielbrett besteht ebenfalls aus Dreiecken und hat die Form eines +Sechsecks mit jeweils neun Dreiecken pro Kante.

+
+
Das Brett mit den durch graue Punkte markierten +Startfeldern
+

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.

+
+
Eine Beispielstellung nach ein paar Zügen
+

Regeln für zwei Spieler

+

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.

+

Regeln für drei Spieler

+

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.

+ + + diff --git a/doc/help/de/pentobi/user_interface.html b/doc/help/de/pentobi/user_interface.html new file mode 100644 index 0000000..96fd211 --- /dev/null +++ b/doc/help/de/pentobi/user_interface.html @@ -0,0 +1,69 @@ + + + +Pentobi-Hilfe + + + + + + +

Wie Sie Pentobi benutzen

+

Spielbrett

+

Spielsteine können durch Klicken auf einen ungespielten Spielstein +ausgewählt werden oder durch Benutzen von Tastenkürzeln. 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.

+

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).

+

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.

+

Gegen den Computer spielen

+

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.

+

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 Einstellungen aus dem Menü Computer +oder der Werkzeugleiste und wählen Sie die Farben, die der Computer spielen +soll.

+

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.

+

Die Auswahl von Spielen aus dem Menü Computer 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.

+

Zugvarianten und der Spielbaum

+

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 Gehe zu und den Navigations-Buttons +navigieren.

+

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 Vorwärts-Button drücken). Die Hauptvariante sollte das wirklich +gespielte Spiel darstellen. Wenn Sie eine Nebenvariante zur Hauptvariante +machen wollen, wählen Sie Zu Hauptvariante machen aus dem +Bearbeiten-Menü.

+ + + diff --git a/doc/help/de/pentobi/window_menu.html b/doc/help/de/pentobi/window_menu.html new file mode 100644 index 0000000..eb6b3a5 --- /dev/null +++ b/doc/help/de/pentobi/window_menu.html @@ -0,0 +1,245 @@ + + + +Pentobi-Hilfe + + + + + + +

Fenstermenü und Werkzeugleiste

+

Navigations-Buttons in der Werkzeugleiste

+
+
Anfang
+
Geht zum Anfang des Spiels.
+
Zurück 10
+
Geht zehn Züge in der gegenwärtigen Variante zurück. Der Button unterstützt +automatische Wiederholung, wenn er gedrückt gehalten wird.
+
Zurück
+
Geht einen Zug in der gegenwärtigen Variante zurück. Der Button unterstützt +automatische Wiederholung, wenn er gedrückt gehalten wird.
+
Vorwärts
+
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.
+
Vorwärts 10
+
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.
+
Ende
+
Geht zum Ende der gegenwärtigen Variante. Wie bei Vorwärts wird auch +hier jeweils die erste Variante benutzt, wenn die Brettstellung mehrere +nachfolgende Varianten hat.
+
Nächste Variante
+
Geht zur nächsten Variante zum zuletzt gespielten Zug (d. h. zum +nächsten Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).
+
Vorherige Variante
+
Geht zur vorherigen Variante zum zuletzt gespielten Zug (d. h. zum +vorherigen Geschwisterknoten des gegenwärtigen Knotens im Spielbaum).
+
+

Spiel-Menü

+
+
Neu
+
Beginnt ein neues Spiel.
+
Gewertetes Spiel
+
Beginnt ein neues gewertetes +Spiel gegen den Computer.
+
Spielvariante
+
Wählt eine Spielvariante und beginnt ein neues Spiel dieser +Spielvariante.
+
Spielinformation
+
Ö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.
+
Zug rückgängig
+
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 +Bearbeiten/Abschneiden zum Entfernen innerer Knoten aus dem +Spielbaum).
+
Zug finden
+
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.
+
Öffnen
+
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.
+
Zuletzt benutzte Dateien
+
Lädt ein kürzlich benutztes Spiel.
+
Zwischenablage öffnen
+
Öffnet ein Spiel von einem Text, der in die Zwischenablage kopiert wurde. +Der Text muss ein gültiges Spiel im Pentobi-SGF-Dateiformat sein.
+
Speichern
+
Speichert das gegenwärtige Spiel.
+
Speichern unter
+
Speichert das gegenwärtige Spiel unter einem neuen Dateinamen.
+
Exportieren/Grafik
+
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.
+
Exportieren/ASCII-Art
+
Speichert die gegenwärtige Brettstellung als Textdiagramm. Das Textdiagramm +sollte mit einer Schriftart fester Breite betrachtet werden.
+
Beenden
+
Beendet Pentobi.
+
+

Gehe-zu-Menü

+
+
Zugnummer
+
Geht zum Zug mit einer bestimmten Nummer in der gegenwärtigen +Variante.
+
Hauptvariante
+
Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die +zur Hauptvariante gehörte.
+
Anfang der Verzweigung
+
Kehrt zur letzten Brettstellung in der gegenwärtigen Variante zurück, die +einen alternativen Zug hatte.
+
Nächster Kommentar
+
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.
+
+

Bearbeiten-Menü

+
+
Annotierung
+
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 +Zugmarkierung, an die auf dem Spielbrett.
+
Zu Hauptvariante machen
+
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.
+
Variante nach oben
+
Ändert die Reihenfolge der Varianten so, dass die gegenwärtige +Brettstellung beim Durchlaufen der Varianten mit Nächste/Vorherige +Variante früher erscheint.
+
Variante nach unten
+
Ändert die Reihenfolge der Varianten so, dass die gegenwärtige +Brettstellung beim Durchlaufen der Varianten mit Nächste/Vorherige +Variante später erscheint.
+
Varianten löschen
+
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 Zurück zu +Hauptvariante.
+
Abschneiden
+
Entfernt den Knoten mit der gegenwärtigen Brettstellung zusammen mit dem +auf ihn folgenden Teilbaum aus dem Spielbaum.
+
Kindknoten abschneiden
+
Entfernt alle Kindknoten des Knotens mit der gegenwärtigen Brettstellung +aus dem Spielbaum.
+
Brettstellung behalten
+
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.
+
Teilbaum behalten
+
Wie Brettstellung behalten, aber die Züge nach der gegenwärtigen +Brettstellung werden nicht gelöscht.
+
Stellungsaufbau
+
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 +Nächste Farbe 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.
+
Nächste Farbe
+
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.
+
+

Ansicht-Menü

+
+
Erscheinungsbild
+
+
+
Koordinaten
+
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.
+
Varianten zeigen
+
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.
+
Zugnummer
+
Diese Option existiert nur im Desktop-Modus und zeigt die Zugnummer, +Zugannotierung und Varianteninformation auf der rechten Seite der Statusleiste +an.
+
Zugmarkierung
+
Ä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.
+
Kommentar zeigen
+
Sichtbarkeit des Kommentarbereichs, wenn sich die Stellung ändert. Die
+
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.
+
+
+
Kommentar
+
Mach den Kommentarbereich in der aktuellen Stellung sichtbar oder nicht +sichtbar.
+
Vollbild
+
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.
+
+

Computer-Menü

+
+
Einstellungen
+
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.
+
Spielen
+
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.
+
Zug spielen
+
Lässt den Computer einen einzelnen Zug für die gegenwärtige Farbe spielen +ohne die vom Computer gespielten Farben zu ändern.
+
Stopp
+
Bricht die gegenwärtige Zuggenerierung ab. Sie können den Computer +weiterspielen lassen, indem Sie Spielen auswählen.
+
+

Extras-Menü

+
+
Wertung
+
Zeigt ein Dialogfenster mit der Wertung des Benutzers in der gegenwärtigen +Spielvariante.
+
Spiel analysieren
+
Führt eine Spielanalyse +durch.
+
+

Hilfe-Menü

+
+
Pentobi-Hilfe
+
Zeigt ein Fenster mit dem Pentobi-Benutzerhandbuch.
+
Über Pentobi
+
Zeigt eine Dialogfenster mit Informationen über diese Version von +Pentobi.
+
+ + + diff --git a/doc/man/CMakeLists.txt b/doc/man/CMakeLists.txt new file mode 100644 index 0000000..ec3a324 --- /dev/null +++ b/doc/man/CMakeLists.txt @@ -0,0 +1,11 @@ +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() diff --git a/doc/man/pentobi-thumbnailer.6.in b/doc/man/pentobi-thumbnailer.6.in new file mode 100644 index 0000000..1d84ade --- /dev/null +++ b/doc/man/pentobi-thumbnailer.6.in @@ -0,0 +1,38 @@ +.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 diff --git a/doc/man/pentobi.6.in b/doc/man/pentobi.6.in new file mode 100644 index 0000000..0c774df --- /dev/null +++ b/doc/man/pentobi.6.in @@ -0,0 +1,76 @@ +.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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..a343873 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,38 @@ +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() diff --git a/src/books/book_callisto.blksgf b/src/books/book_callisto.blksgf new file mode 100644 index 0000000..c9bccbf --- /dev/null +++ b/src/books/book_callisto.blksgf @@ -0,0 +1,16 @@ +( +;GM[Callisto] +( + ;1[g11]TE[1] + ( + ;2[n10]TE[1] + ) + ( + ;2[n11]TE[1] + ) +) +( + ;1[h12]TE[1] + ;2[m9]TE[1] +) +) diff --git a/src/books/book_callisto_2.blksgf b/src/books/book_callisto_2.blksgf new file mode 100644 index 0000000..ccddbbf --- /dev/null +++ b/src/books/book_callisto_2.blksgf @@ -0,0 +1,24 @@ +( +;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] + ) +) +) diff --git a/src/books/book_callisto_2_4.blksgf b/src/books/book_callisto_2_4.blksgf new file mode 100644 index 0000000..4a1a478 --- /dev/null +++ b/src/books/book_callisto_2_4.blksgf @@ -0,0 +1,12 @@ +( +;GM[Callisto Two-Player Four-Color]CA[UTF-8] +( + ;1[h12]TE[1] +) +( + ;1[g11]TE[1] +) +( + ;1[h13]TE[1] +) +) diff --git a/src/books/book_callisto_3.blksgf b/src/books/book_callisto_3.blksgf new file mode 100644 index 0000000..6109c33 --- /dev/null +++ b/src/books/book_callisto_3.blksgf @@ -0,0 +1,21 @@ +( +;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] + ) +) +) diff --git a/src/books/book_classic.blksgf b/src/books/book_classic.blksgf new file mode 100644 index 0000000..bf9ad9e --- /dev/null +++ b/src/books/book_classic.blksgf @@ -0,0 +1,14 @@ +( +;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] +) +) diff --git a/src/books/book_classic_2.blksgf b/src/books/book_classic_2.blksgf new file mode 100644 index 0000000..debcbc7 --- /dev/null +++ b/src/books/book_classic_2.blksgf @@ -0,0 +1,891 @@ +( +;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] +) +) diff --git a/src/books/book_classic_3.blksgf b/src/books/book_classic_3.blksgf new file mode 100644 index 0000000..273d56e --- /dev/null +++ b/src/books/book_classic_3.blksgf @@ -0,0 +1,14 @@ +( +;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] +) +) diff --git a/src/books/book_duo.blksgf b/src/books/book_duo.blksgf new file mode 100644 index 0000000..f31b5bc --- /dev/null +++ b/src/books/book_duo.blksgf @@ -0,0 +1,225 @@ +( +;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] +) +) diff --git a/src/books/book_gembloq.blksgf b/src/books/book_gembloq.blksgf new file mode 100644 index 0000000..c32e5d6 --- /dev/null +++ b/src/books/book_gembloq.blksgf @@ -0,0 +1,7 @@ +( +;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] +) diff --git a/src/books/book_gembloq_2.blksgf b/src/books/book_gembloq_2.blksgf new file mode 100644 index 0000000..7b63df6 --- /dev/null +++ b/src/books/book_gembloq_2.blksgf @@ -0,0 +1,4 @@ +( +;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] +) diff --git a/src/books/book_gembloq_2_4.blksgf b/src/books/book_gembloq_2_4.blksgf new file mode 100644 index 0000000..ba9e1b2 --- /dev/null +++ b/src/books/book_gembloq_2_4.blksgf @@ -0,0 +1,7 @@ +( +;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] +) diff --git a/src/books/book_gembloq_3.blksgf b/src/books/book_gembloq_3.blksgf new file mode 100644 index 0000000..63ddfac --- /dev/null +++ b/src/books/book_gembloq_3.blksgf @@ -0,0 +1,4 @@ +( +;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] +) diff --git a/src/books/book_junior.blksgf b/src/books/book_junior.blksgf new file mode 100644 index 0000000..9d86c8e --- /dev/null +++ b/src/books/book_junior.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Junior] +( + ;B[f9,e10,f10,e11,f11]TE[1] +) +( + ;B[g9,d10,e10,f10,g10]TE[1] +) +) diff --git a/src/books/book_nexos.blksgf b/src/books/book_nexos.blksgf new file mode 100644 index 0000000..7793fc6 --- /dev/null +++ b/src/books/book_nexos.blksgf @@ -0,0 +1,15 @@ +( +;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] +) +) diff --git a/src/books/book_nexos_2.blksgf b/src/books/book_nexos_2.blksgf new file mode 100644 index 0000000..e535a52 --- /dev/null +++ b/src/books/book_nexos_2.blksgf @@ -0,0 +1,15 @@ +( +;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] +) +) diff --git a/src/books/book_trigon.blksgf b/src/books/book_trigon.blksgf new file mode 100644 index 0000000..da0292e --- /dev/null +++ b/src/books/book_trigon.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Trigon] +( + ;1[r12,r13,s13,r14,s14,r15]TE[1] +) +( + ;1[t12,s13,t13,r14,s14,r15]TE[1] +) +) diff --git a/src/books/book_trigon_2.blksgf b/src/books/book_trigon_2.blksgf new file mode 100644 index 0000000..aa1545f --- /dev/null +++ b/src/books/book_trigon_2.blksgf @@ -0,0 +1,39 @@ +( +;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] +) +) diff --git a/src/books/book_trigon_3.blksgf b/src/books/book_trigon_3.blksgf new file mode 100644 index 0000000..7de2ba6 --- /dev/null +++ b/src/books/book_trigon_3.blksgf @@ -0,0 +1,9 @@ +( +;GM[Blokus Trigon Three-Player] +( + ;1[p11,o12,p12,o13,p13,p14]TE[1] +) +( + ;1[r11,q12,r12,p13,q13,p14]TE[1] +) +) diff --git a/src/books/pentobi_books.qrc b/src/books/pentobi_books.qrc new file mode 100644 index 0000000..eb5fd22 --- /dev/null +++ b/src/books/pentobi_books.qrc @@ -0,0 +1,22 @@ + + + book_callisto.blksgf + book_callisto_2.blksgf + book_callisto_2_4.blksgf + book_callisto_3.blksgf + book_classic.blksgf + book_classic_2.blksgf + book_classic_3.blksgf + book_duo.blksgf + book_gembloq.blksgf + book_gembloq_2.blksgf + book_gembloq_2_4.blksgf + book_gembloq_3.blksgf + book_junior.blksgf + book_nexos.blksgf + book_nexos_2.blksgf + book_trigon.blksgf + book_trigon_2.blksgf + book_trigon_3.blksgf + + diff --git a/src/convert/CMakeLists.txt b/src/convert/CMakeLists.txt new file mode 100644 index 0000000..7e05d01 --- /dev/null +++ b/src/convert/CMakeLists.txt @@ -0,0 +1,5 @@ +find_package(Qt5Gui REQUIRED) + +add_executable(convert Main.cpp) + +target_link_libraries(convert Qt5::Gui) diff --git a/src/convert/Main.cpp b/src/convert/Main.cpp new file mode 100644 index 0000000..ddf2b10 --- /dev/null +++ b/src/convert/Main.cpp @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include + +//----------------------------------------------------------------------------- + +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; +} + +//----------------------------------------------------------------------------- diff --git a/src/doc_libboardgame.cpp b/src/doc_libboardgame.cpp new file mode 100644 index 0000000..65e1bd0 --- /dev/null +++ b/src/doc_libboardgame.cpp @@ -0,0 +1,80 @@ +/** + +@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. +(PDF) + +@section libboardgame_doc_enz_2009 A Lock-free Multithreaded Monte-Carlo Tree Search Algorithm. +M. Enzenberger, M. Mueller. Advances in Computer Games 2009. +(PDF) + +@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. +(PDF) + +@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 +(PDF) + +*/ diff --git a/src/doc_mainpage.cpp b/src/doc_mainpage.cpp new file mode 100644 index 0000000..99dc581 --- /dev/null +++ b/src/doc_mainpage.cpp @@ -0,0 +1,60 @@ +/** @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 + Qt/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 + Gnome desktop + - pentobi_kde_thumbnailer - + Plugin for file preview thumbnails for the + KDE desktop +*/ diff --git a/src/icon/pentobi-16.svg b/src/icon/pentobi-16.svg new file mode 100644 index 0000000..1aab930 --- /dev/null +++ b/src/icon/pentobi-16.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icon/pentobi-32.svg b/src/icon/pentobi-32.svg new file mode 100644 index 0000000..01f632a --- /dev/null +++ b/src/icon/pentobi-32.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icon/pentobi-64.svg b/src/icon/pentobi-64.svg new file mode 100644 index 0000000..1bc65fa --- /dev/null +++ b/src/icon/pentobi-64.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icon/pentobi.svg b/src/icon/pentobi.svg new file mode 100644 index 0000000..a5b8b0e --- /dev/null +++ b/src/icon/pentobi.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icon/pentobi_icon.qrc b/src/icon/pentobi_icon.qrc new file mode 100644 index 0000000..35af691 --- /dev/null +++ b/src/icon/pentobi_icon.qrc @@ -0,0 +1,5 @@ + + +pentobi-64.svg + + diff --git a/src/icon/pentobi_icon_desktop.qrc b/src/icon/pentobi_icon_desktop.qrc new file mode 100644 index 0000000..af173be --- /dev/null +++ b/src/icon/pentobi_icon_desktop.qrc @@ -0,0 +1,7 @@ + + +pentobi-16.svg +pentobi-32.svg +pentobi.svg + + diff --git a/src/learn_tool/CMakeLists.txt b/src/learn_tool/CMakeLists.txt new file mode 100644 index 0000000..dc9455c --- /dev/null +++ b/src/learn_tool/CMakeLists.txt @@ -0,0 +1,12 @@ +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 +) diff --git a/src/learn_tool/Main.cpp b/src/learn_tool/Main.cpp new file mode 100644 index 0000000..9cdca87 --- /dev/null +++ b/src/learn_tool/Main.cpp @@ -0,0 +1,522 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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 feature; + + + Features() { feature.fill(0); } + + void operator+=(const Features& f) + { + for (unsigned i = 0; i < _nu_features; ++i) + feature[i] = static_cast(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; +}; + + +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 probs; + +array weights; + +array grad_weights; + +GridExt feature_grid_point; + +GridExt feature_grid_adj; + +GridExt feature_grid_attach; + +vector 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 +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(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(is_forbidden[pa]); + } + else + for (auto pa : geo.get_adj(p)) + n += 1u - static_cast(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( + mv, move_info_ext_array); + auto& info = BoardConst::get_move_info(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(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 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(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 specs = { + "sgffiles:", + "steps:" + }; + Options opt(argc, argv, specs); + train(opt.get("sgffiles"), opt.get("steps", 3000)); + } + catch (const exception& e) + { + LIBBOARDGAME_LOG("Error: ", e.what()); + return 1; + } + return 0; +} + +//----------------------------------------------------------------------------- diff --git a/src/libboardgame_base/CMakeLists.txt b/src/libboardgame_base/CMakeLists.txt new file mode 100644 index 0000000..1b0d94e --- /dev/null +++ b/src/libboardgame_base/CMakeLists.txt @@ -0,0 +1,34 @@ +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 ..) diff --git a/src/libboardgame_base/CoordPoint.cpp b/src/libboardgame_base/CoordPoint.cpp new file mode 100644 index 0000000..d9bb537 --- /dev/null +++ b/src/libboardgame_base/CoordPoint.cpp @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/CoordPoint.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "CoordPoint.h" + +#include + +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 diff --git a/src/libboardgame_base/CoordPoint.h b/src/libboardgame_base/CoordPoint.h new file mode 100644 index 0000000..5c1148e --- /dev/null +++ b/src/libboardgame_base/CoordPoint.h @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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::max()); + LIBBOARDGAME_ASSERT(y < numeric_limits::max()); + this->x = static_cast(x); + this->y = static_cast(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::max(), numeric_limits::max()}; +} + +inline bool CoordPoint::is_onboard(int x, int y, unsigned width, + unsigned height) +{ + return x >= 0 && x < static_cast(width) + && y >= 0 && y < static_cast(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::max(); +} + +//----------------------------------------------------------------------------- + +ostream& operator<<(ostream& out, CoordPoint p); + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_COORD_POINT_H diff --git a/src/libboardgame_base/Engine.cpp b/src/libboardgame_base/Engine.cpp new file mode 100644 index 0000000..fcd9ef1 --- /dev/null +++ b/src/libboardgame_base/Engine.cpp @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +/** @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
+ Arguments: random seed */ +void Engine::cmd_set_random_seed(Arguments args) +{ + RandomGenerator::set_global_seed(args.parse()); +} + +void Engine::on_handle_cmd_begin() +{ + flush_log(); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base diff --git a/src/libboardgame_base/Engine.h b/src/libboardgame_base/Engine.h new file mode 100644 index 0000000..fa2aef5 --- /dev/null +++ b/src/libboardgame_base/Engine.h @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_base/Geometry.h b/src/libboardgame_base/Geometry.h new file mode 100644 index 0000000..b92a347 --- /dev/null +++ b/src/libboardgame_base/Geometry.h @@ -0,0 +1,347 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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 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; + + /** On-board diagonal neighbors of a point + Currently supports up to 11 diagonal points as used on boards + for GembloQ. */ + using DiagList = ArrayList; + + /** Adjacent neighbors of a coordinate. */ + using AdjCoordList = ArrayList; + + /** Diagonal neighbors of a coordinate. */ + using DiagCoordList = ArrayList; + + 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 string_rep = make_unique()); + + /** 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 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 +Geometry

::Geometry(unique_ptr string_rep) + : m_string_rep(move(string_rep)) +{ } + +template +Geometry

::~Geometry() = default; // Non-inline to avoid GCC -Winline warning + +template +bool Geometry

::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 +inline auto Geometry

::get_adj(Point p) const -> const AdjList& +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_adj[p.to_int()]; +} + +template +inline auto Geometry

::get_diag(Point p) const -> const DiagList& +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_diag[p.to_int()]; +} + +template +inline auto Geometry

::get_point(unsigned x, unsigned y) const -> Point +{ + LIBBOARDGAME_ASSERT(x < m_width); + LIBBOARDGAME_ASSERT(y < m_height); + return m_points[x][y]; +} + +template +inline auto Geometry

::get_point(int x, int y) const -> Point +{ + if (x < 0 || static_cast(x) >= m_width + || y < 0 || static_cast(y) >= m_height) + return Point::null(); + return m_points[x][y]; +} + +template +inline unsigned Geometry

::get_point_type(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_point_type[p.to_int()]; +} + +template +inline unsigned Geometry

::get_point_type(CoordPoint p) const +{ + return get_point_type(p.x, p.y); +} + +template +inline unsigned Geometry

::get_x(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_x[p.to_int()]; +} + +template +inline unsigned Geometry

::get_y(Point p) const +{ + LIBBOARDGAME_ASSERT(is_valid(p)); + return m_y[p.to_int()]; +} + +template +void Geometry

::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 +bool Geometry

::is_onboard(unsigned x, unsigned y) const +{ + return x < m_width && y < m_height && ! get_point(x, y).is_null(); +} + +template +bool Geometry

::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 +inline bool Geometry

::is_valid(Point p) const +{ + return p.to_int() < m_range; +} + +#endif + +template +inline const string& Geometry

::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 diff --git a/src/libboardgame_base/GeometryUtil.h b/src/libboardgame_base/GeometryUtil.h new file mode 100644 index 0000000..6c5c586 --- /dev/null +++ b/src/libboardgame_base/GeometryUtil.h @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +/** @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 +void normalize_offset(T begin, T end, unsigned& width, unsigned& height, + CoordPoint& offset) +{ + int min_x = numeric_limits::max(); + int min_y = numeric_limits::max(); + int max_x = numeric_limits::min(); + int max_y = numeric_limits::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(max_x - min_x + 1); + height = static_cast(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 +CoordPoint type_match_offset(const Geometry

& 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 +void type_match_shift(const Geometry

& 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 diff --git a/src/libboardgame_base/Grid.h b/src/libboardgame_base/Grid.h new file mode 100644 index 0000000..683b8ff --- /dev/null +++ b/src/libboardgame_base/Grid.h @@ -0,0 +1,229 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#include +#include "Geometry.h" + +namespace libboardgame_base { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +string grid_to_string(const T& grid, const Geometry& 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 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 Grid +{ + friend class GridExt; // for GridExt::copy_from(Grid) + +public: + using Point = P; + + using Geometry = libboardgame_base::Geometry

; + + + T& operator[](const Point& p); + + const T& operator[](const Point& p) const; + + /** Fill all on-board points for a given geometry with a value. */ + void fill(const T& val, const Geometry& geo); + + /** Fill points with a value. */ + void fill_all(const T& val); + + string to_string(const Geometry& geo) const; + + void copy_from(const Grid& grid, const Geometry& geo); + + /** Specialized version for trivially copyable elements. + Can be used instead of copy_from if the compiler is not smart enough to + figure out that it can use memcpy. + @pre std::is_trivially_copyable::value */ + void memcpy_from(const Grid& grid, const Geometry& geo); + +private: + T m_a[Point::range_onboard]; +}; + + +template +inline T& Grid::operator[](const Point& p) +{ + LIBBOARDGAME_ASSERT(! p.is_null()); + return m_a[p.to_int()]; +} + +template +inline const T& Grid::operator[](const Point& p) const +{ + LIBBOARDGAME_ASSERT(! p.is_null()); + return m_a[p.to_int()]; +} + +template +inline void Grid::copy_from(const Grid& grid, const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +inline void Grid::fill(const T& val, const Geometry& geo) +{ + std::fill(m_a, m_a + geo.get_range(), val); +} + +template +inline void Grid::fill_all(const T& val) +{ + std::fill(m_a, m_a + Point::range_onboard, val); +} + +template +void Grid::memcpy_from(const Grid& grid, const Geometry& geo) +{ +#if ! (__GNUC__ && __GNUC__ < 5) + static_assert(is_trivially_copyable::value, ""); +#endif + memcpy(&m_a, grid.m_a, geo.get_range() * sizeof(T)); +} + +template +string Grid::to_string(const Geometry& geo) const +{ + return grid_to_string(*this, geo); +} + +//----------------------------------------------------------------------------- + +/** Like Grid, but allows Point::null() as index. */ +template +class GridExt +{ +public: + using Point = P; + + using Geometry = libboardgame_base::Geometry

; + + + T& operator[](const Point& p); + + const T& operator[](const Point& p) const; + + /** Fill all on-board points for a given geometry with a value. */ + void fill(const T& val, const Geometry& geo); + + /** Fill points with a value. */ + void fill_all(const T& val); + + string to_string(const Geometry& geo) const; + + void copy_from(const Grid& grid, const Geometry& geo); + + void copy_from(const GridExt& grid, const Geometry& geo); + +private: + T m_a[Point::range]; +}; + + +template +inline T& GridExt::operator[](const Point& p) +{ + return m_a[p.to_int()]; +} + +template +inline const T& GridExt::operator[](const Point& p) const +{ + return m_a[p.to_int()]; +} + +template +inline void GridExt::fill(const T& val, const Geometry& geo) +{ + std::fill(m_a, m_a + geo.get_range(), val); +} + +template +inline void GridExt::fill_all(const T& val) +{ + std::fill(m_a, m_a + Point::range, val); +} + +template +inline void GridExt::copy_from(const Grid& grid, + const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +inline void GridExt::copy_from(const GridExt& grid, + const Geometry& geo) +{ + copy(grid.m_a, grid.m_a + geo.get_range(), m_a); +} + +template +string GridExt::to_string(const Geometry& geo) const +{ + return grid_to_string(*this, geo); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_GRID_H diff --git a/src/libboardgame_base/Marker.h b/src/libboardgame_base/Marker.h new file mode 100644 index 0000000..4ec2f40 --- /dev/null +++ b/src/libboardgame_base/Marker.h @@ -0,0 +1,103 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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 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::max() times. */ + void setup_for_overflow_test(unsigned nu_clear); + +private: + unsigned m_current; + + unsigned m_a[Point::range]; + + void reset(); +}; + + +template +inline Marker

::Marker() +{ + reset(); +} + +template +bool Marker

::operator[](Point p) const +{ + return m_a[p.to_int()] == m_current; +} + +template +inline void Marker

::clear() +{ + if (--m_current == 0) + reset(); +} + +template +inline void Marker

::setup_for_overflow_test(unsigned nu_clear) +{ + reset(); + m_current -= nu_clear; +} + +template +inline void Marker

::reset() +{ + m_current = numeric_limits::max() - 1; + fill(m_a, m_a + Point::range, numeric_limits::max()); +} + +template +inline bool Marker

::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 diff --git a/src/libboardgame_base/Point.h b/src/libboardgame_base/Point.h new file mode 100644 index 0000000..e7f4108 --- /dev/null +++ b/src/libboardgame_base/Point.h @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 +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::is_integer, ""); + + static_assert(! numeric_limits::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 +inline Point::Point() +{ +#ifdef LIBBOARDGAME_DEBUG + m_i = value_uninitialized; +#endif +} + +template +inline Point::Point(unsigned i) +{ + LIBBOARDGAME_ASSERT(i < range); + m_i = static_cast(i); +} + +template +inline bool Point::operator==(const Point& p) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(p.is_initialized()); + return m_i == p.m_i; +} + +template +inline bool Point::operator!=(const Point& p) const +{ + return ! operator==(p); +} + +template +inline bool Point::operator<(const Point& p) const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + LIBBOARDGAME_ASSERT(p.is_initialized()); + return m_i < p.m_i; +} + +template +inline bool Point::is_initialized() const +{ + return m_i < value_uninitialized; +} + +template +inline bool Point::is_null() const +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i == value_null; +} + +template +inline auto Point::null() -> Point +{ + return Point(value_null); +} + +template +inline auto Point::to_int() const -> IntType +{ + LIBBOARDGAME_ASSERT(is_initialized()); + return m_i; +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_POINT_H diff --git a/src/libboardgame_base/PointTransform.h b/src/libboardgame_base/PointTransform.h new file mode 100644 index 0000000..53e4ee1 --- /dev/null +++ b/src/libboardgame_base/PointTransform.h @@ -0,0 +1,410 @@ +//----------------------------------------------------------------------------- +/** @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 +#include "Geometry.h" +#include "libboardgame_util/Unused.h" + +namespace libboardgame_base { + +//----------------------------------------------------------------------------- + +/** %Transform a point. + @tparam P An instance of class Point. */ +template +class PointTransform +{ +public: + using Point = P; + + virtual ~PointTransform() = default; + + virtual Point get_transformed(Point p, const Geometry

& geo) const = 0; +}; + + +//----------------------------------------------------------------------------- + +template +class PointTransfIdent + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfIdent

::get_transformed(Point p, const Geometry

& geo) const +{ + LIBBOARDGAME_UNUSED(geo); + return p; +} + +//----------------------------------------------------------------------------- + +/** Rotate point by 90 degrees. */ +template +class PointTransfRot90 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfRot90

::get_transformed(Point p, const Geometry

& 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 PointTransfRot180 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfRot180

::get_transformed(Point p, const Geometry

& 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 PointTransfRot270 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfRot270

::get_transformed(Point p, const Geometry

& 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 PointTransfRot270Refl + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfRot270Refl

::get_transformed(Point p, const Geometry

& 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 PointTransfRot90Refl + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfRot90Refl

::get_transformed(Point p, const Geometry

& 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 PointTransfRefl + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfRefl

::get_transformed(Point p, const Geometry

& 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 PointTransfReflRot180 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfReflRot180

::get_transformed(Point p, const Geometry

& 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 PointTransfTrigonRot60 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonRot60

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx + 0.5f * px + 1.5f * py)); + auto y = static_cast(round(cy - 0.5f * px + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot120 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonRot120

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx - 0.5f * px + 1.5f * py)); + auto y = static_cast(round(cy - 0.5f * px - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot240 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonRot240

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx - 0.5f * px - 1.5f * py)); + auto y = static_cast(round(cy + 0.5f * px - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonRot300 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonRot300

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx + 0.5f * px - 1.5f * py)); + auto y = static_cast(round(cy + 0.5f * px + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot60 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonReflRot60

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx + 0.5f * (-px) + 1.5f * py)); + auto y = static_cast(round(cy - 0.5f * (-px) + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot120 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonReflRot120

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx - 0.5f * (-px) + 1.5f * py)); + auto y = static_cast(round(cy - 0.5f * (-px) - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot240 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonReflRot240

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx - 0.5f * (-px) - 1.5f * py)); + auto y = static_cast(round(cy + 0.5f * (-px) - 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +template +class PointTransfTrigonReflRot300 + : public PointTransform

+{ +public: + using Point = P; + + Point get_transformed(Point p, const Geometry

& geo) const override; +}; + + +template +P PointTransfTrigonReflRot300

::get_transformed(Point p, const Geometry

& geo) const +{ + float cx = 0.5f * static_cast(geo.get_width() - 1); + float cy = 0.5f * static_cast(geo.get_height() - 1); + float px = static_cast(geo.get_x(p)) - cx; + float py = static_cast(geo.get_y(p)) - cy; + auto x = static_cast(round(cx + 0.5f * (-px) - 1.5f * py)); + auto y = static_cast(round(cy + 0.5f * (-px) + 0.5f * py)); + return geo.get_point(x, y); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_POINT_TRANSFORM_H diff --git a/src/libboardgame_base/Rating.cpp b/src/libboardgame_base/Rating.cpp new file mode 100644 index 0000000..228e6ab --- /dev/null +++ b/src/libboardgame_base/Rating.cpp @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/Rating.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Rating.h" + +#include +#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(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 diff --git a/src/libboardgame_base/Rating.h b/src/libboardgame_base/Rating.h new file mode 100644 index 0000000..3b481c8 --- /dev/null +++ b/src/libboardgame_base/Rating.h @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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(round(m_elo)); } + +private: + double m_elo; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_base + +#endif // LIBBOARDGAME_BASE_RATING_H diff --git a/src/libboardgame_base/RectGeometry.h b/src/libboardgame_base/RectGeometry.h new file mode 100644 index 0000000..40eac8e --- /dev/null +++ b/src/libboardgame_base/RectGeometry.h @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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 RectGeometry final + : public Geometry

+{ +public: + using Point = P; + + using AdjCoordList = typename Geometry

::AdjCoordList; + + using DiagCoordList = typename Geometry

::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 +RectGeometry

::RectGeometry(unsigned width, unsigned height) +{ + Geometry

::init(width, height); +} + +template +const RectGeometry

& RectGeometry

::get(unsigned width, unsigned height) +{ + static map, shared_ptr> 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(width, height); + return *s_geometry.insert(make_pair(key, geometry)).first->second; +} + +template +auto RectGeometry

::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 +auto RectGeometry

::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 +unsigned RectGeometry

::get_period_x() const +{ + return 1; +} + +template +unsigned RectGeometry

::get_period_y() const +{ + return 1; +} + +template +unsigned RectGeometry

::get_point_type(int x, int y) const +{ + LIBBOARDGAME_UNUSED(x); + LIBBOARDGAME_UNUSED(y); + return 0; +} + +template +bool RectGeometry

::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 diff --git a/src/libboardgame_base/RectTransform.cpp b/src/libboardgame_base/RectTransform.cpp new file mode 100644 index 0000000..83b0c8b --- /dev/null +++ b/src/libboardgame_base/RectTransform.cpp @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_base/RectTransform.h b/src/libboardgame_base/RectTransform.h new file mode 100644 index 0000000..bb32ad1 --- /dev/null +++ b/src/libboardgame_base/RectTransform.h @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_base/StringRep.cpp b/src/libboardgame_base/StringRep.cpp new file mode 100644 index 0000000..b971e49 --- /dev/null +++ b/src/libboardgame_base/StringRep.cpp @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_base/StringRep.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "StringRep.h" + +#include +#include +#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(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((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 diff --git a/src/libboardgame_base/StringRep.h b/src/libboardgame_base/StringRep.h new file mode 100644 index 0000000..d20341b --- /dev/null +++ b/src/libboardgame_base/StringRep.h @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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 diff --git a/src/libboardgame_base/Transform.cpp b/src/libboardgame_base/Transform.cpp new file mode 100644 index 0000000..0569276 --- /dev/null +++ b/src/libboardgame_base/Transform.cpp @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_base/Transform.h b/src/libboardgame_base/Transform.h new file mode 100644 index 0000000..48c5bb1 --- /dev/null +++ b/src/libboardgame_base/Transform.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @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 + void transform(I begin, I end) const; + +protected: + explicit Transform(unsigned point_type) + : m_point_type(point_type) + {} + +private: + unsigned m_point_type; +}; + +template +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 diff --git a/src/libboardgame_gtp/Arguments.cpp b/src/libboardgame_gtp/Arguments.cpp new file mode 100644 index 0000000..2b34e4d --- /dev/null +++ b/src/libboardgame_gtp/Arguments.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Arguments.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Arguments.h" + +#include + +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(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 diff --git a/src/libboardgame_gtp/Arguments.h b/src/libboardgame_gtp/Arguments.h new file mode 100644 index 0000000..dce8f75 --- /dev/null +++ b/src/libboardgame_gtp/Arguments.h @@ -0,0 +1,237 @@ +//----------------------------------------------------------------------------- +/** @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 +#endif +#include +#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 + 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 + 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 + 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 + T parse_min_max(unsigned i, T min, T max) const; + + template + 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 + 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(m_line.get_elements().size()) + - m_line.get_idx_name() - 1; +} + +template +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 +T Arguments::parse() const +{ + check_size(1); + return parse(0); +} + +template +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() << ")"; + throw Failure(msg.str()); + } + return result; +} + +template +T Arguments::parse_min(unsigned i, T min) const +{ + auto result = parse(i); + if (result < min) + { + ostringstream msg; + msg << "argument " << (i + 1) << " must be greater or equal " << min; + throw Failure(msg.str()); + } + return result; +} + +template +T Arguments::parse_min_max(T min, T max) const +{ + check_size(1); + return parse_min_max(0, min, max); +} + +template +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 diff --git a/src/libboardgame_gtp/CMakeLists.txt b/src/libboardgame_gtp/CMakeLists.txt new file mode 100644 index 0000000..c4342ff --- /dev/null +++ b/src/libboardgame_gtp/CMakeLists.txt @@ -0,0 +1,14 @@ +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 ..) diff --git a/src/libboardgame_gtp/CmdLine.cpp b/src/libboardgame_gtp/CmdLine.cpp new file mode 100644 index 0000000..b63d4fe --- /dev/null +++ b/src/libboardgame_gtp/CmdLine.cpp @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/CmdLine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "CmdLine.h" + +#include +#include + +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::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(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(*begin)) != 0) + ++begin; + auto end = m_line.end(); + while (end > begin && isspace(static_cast(*(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 diff --git a/src/libboardgame_gtp/CmdLine.h b/src/libboardgame_gtp/CmdLine.h new file mode 100644 index 0000000..c6183ab --- /dev/null +++ b/src/libboardgame_gtp/CmdLine.h @@ -0,0 +1,95 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#include +#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& 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 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(out)); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_CMDLINE_H diff --git a/src/libboardgame_gtp/CmdLineRange.h b/src/libboardgame_gtp/CmdLineRange.h new file mode 100644 index 0000000..acf0692 --- /dev/null +++ b/src/libboardgame_gtp/CmdLineRange.h @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include + +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(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 diff --git a/src/libboardgame_gtp/Engine.cpp b/src/libboardgame_gtp/Engine.cpp new file mode 100644 index 0000000..8326448 --- /dev/null +++ b/src/libboardgame_gtp/Engine.cpp @@ -0,0 +1,201 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_gtp/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Engine.h" + +#include +#include +#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(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 diff --git a/src/libboardgame_gtp/Engine.h b/src/libboardgame_gtp/Engine.h new file mode 100644 index 0000000..d5f9e1d --- /dev/null +++ b/src/libboardgame_gtp/Engine.h @@ -0,0 +1,198 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#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; + + using HandlerNoArgs = function; + + using HandlerNoResponse = function; + + using HandlerNoArgsNoResponse = function; + + + /** @page libboardgame_gtp_commands libboardgame_gtp::Engine GTP commands +

+
@link cmd_known_command() @c known_command @endlink
+
@copydoc cmd_known_command()
+
@link cmd_list_commands() @c list_commands @endlink
+
@copydoc cmd_list_commands()
+
@link cmd_quit() @c quit @endlink
+
@copydoc cmd_quit()
+
*/ + /** @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 + void add(const string& name, void (T::*f)(Arguments, Response&), T* t); + + template + void add(const string& name, void (T::*f)(Arguments), T* t); + + template + void add(const string& name, void (T::*f)(Response&), T* t); + + template + 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 + void add(const string& name, void (T::*f)(Arguments, Response&)); + + template + void add(const string& name, void (T::*f)(Arguments)); + + template + void add(const string& name, void (T::*f)(Response&)); + + template + void add(const string& name, void (T::*f)()); + +private: + /** Flag to quit main loop. */ + bool m_quit; + + map m_handlers; + + + bool handle_cmd(CmdLine& line, ostream* out, Response& response, + string& buffer); +}; + +template +void Engine::add(const string& name, void (T::*f)(Arguments, Response&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)(Response&)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)(Arguments)) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)()) +{ + add(name, f, dynamic_cast(this)); +} + +template +void Engine::add(const string& name, void (T::*f)(Arguments, Response&), T* t) +{ + assert(f); + add(name, + static_cast(bind(f, t, placeholders::_1, placeholders::_2))); +} + +template +void Engine::add(const string& name, void (T::*f)(Response&), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t, placeholders::_1))); +} + +template +void Engine::add(const string& name, void (T::*f)(Arguments), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t, placeholders::_1))); +} + +template +void Engine::add(const string& name, void (T::*f)(), T* t) +{ + assert(f); + add(name, static_cast(bind(f, t))); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_gtp + +#endif // LIBBOARDGAME_GTP_ENGINE_H diff --git a/src/libboardgame_gtp/Failure.h b/src/libboardgame_gtp/Failure.h new file mode 100644 index 0000000..dd90a06 --- /dev/null +++ b/src/libboardgame_gtp/Failure.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @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 + +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 diff --git a/src/libboardgame_gtp/Response.cpp b/src/libboardgame_gtp/Response.cpp new file mode 100644 index 0000000..3ab3c96 --- /dev/null +++ b/src/libboardgame_gtp/Response.cpp @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_gtp/Response.h b/src/libboardgame_gtp/Response.h new file mode 100644 index 0000000..7988f4d --- /dev/null +++ b/src/libboardgame_gtp/Response.h @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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 + 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 diff --git a/src/libboardgame_mcts/Atomic.h b/src/libboardgame_mcts/Atomic.h new file mode 100644 index 0000000..b31f1ae --- /dev/null +++ b/src/libboardgame_mcts/Atomic.h @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 struct Atomic; + +template +struct Atomic +{ + 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 +struct Atomic +{ + atomic 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 diff --git a/src/libboardgame_mcts/CMakeLists.txt b/src/libboardgame_mcts/CMakeLists.txt new file mode 100644 index 0000000..7a93bfc --- /dev/null +++ b/src/libboardgame_mcts/CMakeLists.txt @@ -0,0 +1,18 @@ +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 + ) diff --git a/src/libboardgame_mcts/LastGoodReply.h b/src/libboardgame_mcts/LastGoodReply.h new file mode 100644 index 0000000..2f49133 --- /dev/null +++ b/src/libboardgame_mcts/LastGoodReply.h @@ -0,0 +1,150 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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 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 m_lgr1[max_players][Move::range]; + + Atomic m_lgr2[max_players][hash_table_size]; + + size_t get_index(Move last, Move second_last) const; +}; + +template +LastGoodReply::LastGoodReply() +{ + mt19937 generator; + for (auto& hash : m_hash1) + hash = generator(); + for (auto& hash : m_hash2) + hash = generator(); +} + +template +inline size_t LastGoodReply::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 +inline auto LastGoodReply::get_lgr1(PlayerInt player, + Move last) const -> Move +{ + return Move(m_lgr1[player][last.to_int()].load(memory_order_relaxed)); +} + +template +inline auto LastGoodReply::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 +void LastGoodReply::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 +inline void LastGoodReply::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 +inline void LastGoodReply::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 diff --git a/src/libboardgame_mcts/Node.h b/src/libboardgame_mcts/Node.h new file mode 100644 index 0000000..8c5d5df --- /dev/null +++ b/src/libboardgame_mcts/Node.h @@ -0,0 +1,289 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 +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 m_value; + + Atomic m_value_count; + + Atomic m_visit_count; + + Float m_move_prior; + + Atomic m_nu_children; + + Move m_move; + + Atomic m_first_child; +}; + +template +void Node::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 +void Node::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 +void Node::copy_data_from(const Node& node) +{ + // Reminder to update this function when the class gets additional members + struct Dummy + { + Atomic m_value; + Atomic m_value_count; + Atomic m_visit_count; + Float m_move_prior; + Atomic 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 +inline auto Node::get_value_count() const -> Float +{ + return m_value_count.load(memory_order_relaxed); +} + +template +inline NodeIdx Node::get_first_child() const +{ + LIBBOARDGAME_ASSERT(has_children()); + return m_first_child.load(memory_order_acquire); +} + +template +inline unsigned short Node::get_nu_children() const +{ + return m_nu_children.load(memory_order_acquire); +} + +template +inline auto Node::get_value() const -> Float +{ + return m_value.load(memory_order_relaxed); +} + +template +inline auto Node::get_visit_count() const -> Float +{ + return m_visit_count.load(memory_order_relaxed); +} + +template +inline bool Node::has_children() const +{ + return get_nu_children() > 0; +} + +template +inline void Node::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 +void Node::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 +void Node::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 +inline void Node::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 +inline void Node::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 +inline void Node::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 diff --git a/src/libboardgame_mcts/PlayerMove.h b/src/libboardgame_mcts/PlayerMove.h new file mode 100644 index 0000000..12b889e --- /dev/null +++ b/src/libboardgame_mcts/PlayerMove.h @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @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 + +namespace libboardgame_mcts { + +//----------------------------------------------------------------------------- + +using PlayerInt = uint_fast8_t; + +//----------------------------------------------------------------------------- + +template +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 diff --git a/src/libboardgame_mcts/SearchBase.h b/src/libboardgame_mcts/SearchBase.h new file mode 100644 index 0000000..b11f572 --- /dev/null +++ b/src/libboardgame_mcts/SearchBase.h @@ -0,0 +1,1514 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#include +#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 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; + + using Tree = libboardgame_mcts::Tree; + + using PlayerMove = libboardgame_mcts::PlayerMove; + + + static const PlayerInt max_players = SearchParamConst::max_players; + + static const unsigned max_moves = SearchParamConst::max_moves; + + static const size_t lgr_hash_table_size = + SearchParamConst::lgr_hash_table_size; + + static_assert(! SearchParamConst::use_lgr || lgr_hash_table_size > 0, ""); + + + /** Constructor. + @param nu_threads + @param memory The memory to be used for (all) the search trees. */ + SearchBase(unsigned nu_threads, size_t memory); + + virtual ~SearchBase(); + + + /** @name Pure virtual functions */ + /** @{ */ + + /** Create a new game-specific state to be used in a thread of the + search. */ + virtual unique_ptr 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& 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& callback); + + /** Get evaluation for a player at root node. */ + const StatisticsDirtyLockFree& get_root_val(PlayerInt player) const; + + /** Get evaluation for get_player() at root node. */ + const StatisticsDirtyLockFree& 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 nodes; + + ArrayList moves; + + array 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; + + 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 was_played; + + /** Local variable for update_rave(). + Reused for efficiency. */ + array 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; + + + 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 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, max_players> m_root_val; + + LastGoodReply m_lgr; + + /** See get_nu_simulations(). */ + Atomic 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> m_threads; + + Tree m_tmp_tree; + +#ifdef LIBBOARDGAME_DEBUG + AssertionHandler m_assertion_handler; +#endif + + + function m_callback; + + ArrayList 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 +SearchBase::Thread::Thread(SearchFunc& search_func) + : m_search_func(search_func) +{ } + +template +SearchBase::Thread::~Thread() +{ + if (! m_thread.joinable()) + return; + m_quit = true; + { + lock_guard lock(m_start_search_mutex); + m_start_search_flag = true; + } + m_start_search_cond.notify_one(); + m_thread.join(); +} + +template +void SearchBase::Thread::run() +{ + m_thread = thread(bind(&Thread::thread_main, this)); + m_thread_ready.wait(); +} + +template +void SearchBase::Thread::start_search() +{ + LIBBOARDGAME_ASSERT(m_thread.joinable()); + m_search_finished_lock.lock(); + { + lock_guard lock(m_start_search_mutex); + m_start_search_flag = true; + } + m_start_search_cond.notify_one(); +} + +template +void SearchBase::Thread::thread_main() +{ + unique_lock 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 lock(m_search_finished_mutex); + m_search_finished_flag = true; + } + m_search_finished_cond.notify_one(); + } +} + +template +void SearchBase::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 +SearchBase::AssertionHandler::AssertionHandler( + const SearchBase& search) + : m_search(search) +{ +} + +template +void SearchBase::AssertionHandler::run() +{ + LIBBOARDGAME_LOG(m_search.dump()); +} +#endif // LIBBOARDGAME_DEBUG + + +template +SearchBase::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 +SearchBase::~SearchBase() = default; // Non-inline to avoid GCC -Winline warning + +template +bool SearchBase::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 +bool SearchBase::check_abort_expensive( + ThreadState& thread_state) const +{ + if (get_abort()) + { + LIBBOARDGAME_LOG_THREAD(thread_state, "Search aborted"); + return true; + } + static_assert(numeric_limits::radix == 2, ""); + auto count = m_tree.get_root().get_visit_count(); + if (count >= (size_t(1) << numeric_limits::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 +bool SearchBase::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 +bool SearchBase::check_followup(ArrayList& sequence) +{ + LIBBOARDGAME_UNUSED(sequence); + return false; +} + +template +void SearchBase::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( + bind(&SearchBase::search_loop, this, placeholders::_1)); + for (unsigned i = 0; i < m_nu_threads; ++i) + { + auto t = make_unique(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 +string SearchBase::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 +bool SearchBase::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 +inline size_t SearchBase::get_nu_simulations() const +{ + return m_nu_simulations; +} + +template +inline auto SearchBase::get_root_val(PlayerInt player) const +-> const StatisticsDirtyLockFree& +{ + LIBBOARDGAME_ASSERT(player < m_nu_players); + return m_root_val[player]; +} + +template +inline auto SearchBase::get_root_val() const +-> const StatisticsDirtyLockFree& +{ + return get_root_val(get_player()); +} + +template +inline auto SearchBase::get_root_visit_count() const -> Float +{ + return m_tree.get_root().get_visit_count(); +} + +template +inline auto SearchBase::get_rave_parent_max() const -> Float +{ + return m_rave_parent_max; +} + +template +inline auto SearchBase::get_rave_child_max() const -> Float +{ + return m_rave_child_max; +} + +template +inline auto SearchBase::get_rave_weight() const -> Float +{ + return m_rave_weight; +} + +template +inline bool SearchBase::get_reuse_subtree() const +{ + return m_reuse_subtree; +} + +template +inline bool SearchBase::get_reuse_tree() const +{ + return m_reuse_tree; +} + +template +inline S& SearchBase::get_state(unsigned thread_id) +{ + LIBBOARDGAME_ASSERT(thread_id < m_threads.size()); + return *m_threads[thread_id]->thread_state.state; +} + +template +inline const S& SearchBase::get_state(unsigned thread_id) const +{ + LIBBOARDGAME_ASSERT(thread_id < m_threads.size()); + return *m_threads[thread_id]->thread_state.state; +} + +template +inline auto SearchBase::get_tree() const -> const Tree& +{ + return m_tree; +} + +template +void SearchBase::on_start_search(bool is_followup) +{ + // Default implementation does nothing + LIBBOARDGAME_UNUSED(is_followup); +} + +template +void SearchBase::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 +void SearchBase::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 +string SearchBase::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 +string SearchBase::get_info_ext() const +{ + return string(); +} + +template +bool SearchBase::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::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 +bool SearchBase::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 +bool SearchBase::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::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 +void SearchBase::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( + 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 +inline auto SearchBase::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 +auto SearchBase::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 +bool SearchBase::select_move(Move& mv) const +{ + auto child = select_final(); + if (child) + { + mv = child->get_move(); + return true; + } + return false; +} + +template +void SearchBase::set_callback( + const function& callback) +{ + m_callback = callback; +} + +template +void SearchBase::set_rave_parent_max(Float n) +{ + m_rave_parent_max = n; +} + +template +void SearchBase::set_rave_child_max(Float n) +{ + m_rave_child_max = n; +} + +template +void SearchBase::set_rave_weight(Float v) +{ + m_rave_weight = v; +} + +template +void SearchBase::set_reuse_subtree(bool enable) +{ + m_reuse_subtree = enable; +} + +template +void SearchBase::set_reuse_tree(bool enable) +{ + m_reuse_tree = enable; +} + +template +void SearchBase::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 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 +void SearchBase::update_rave(ThreadState& thread_state) +{ + const auto& state = *thread_state.state; + auto& moves = thread_state.simulation.moves; + auto nu_moves = static_cast(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(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(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(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 +void SearchBase::update_values(ThreadState& thread_state) +{ + const auto& simulation = thread_state.simulation; + auto& nodes = simulation.nodes; + auto& eval = simulation.eval; + auto nu_nodes = static_cast(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 diff --git a/src/libboardgame_mcts/Tree.h b/src/libboardgame_mcts/Tree.h new file mode 100644 index 0000000..29cc290 --- /dev/null +++ b/src/libboardgame_mcts/Tree.h @@ -0,0 +1,465 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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.

+ 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 +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; + + + /** 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::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 m_nodes; + + unique_ptr 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 +inline Tree::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 +inline void Tree::NodeExpander::add_child(const Move& mv, Float value, + Float count, Float move_prior) +{ + // -numeric_limits::max() ist init value for m_best_value + LIBBOARDGAME_ASSERT(value > -numeric_limits::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 +inline bool Tree::NodeExpander::check_capacity( + unsigned short nu_children) const +{ + return m_thread_storage.end - m_thread_storage.next >= nu_children; +} + +template +inline auto Tree::NodeExpander::get_best_child() const -> const Node* +{ + return m_best_child; +} + +template +inline auto Tree::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 +inline auto Tree::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 +inline auto Tree::get_node(NodeIdx i) const -> const Node& +{ + return m_nodes[i]; +} + +template +inline void Tree::NodeExpander::link_children(Tree& tree, const Node& node) +{ + auto nu_children = + static_cast(m_thread_storage.next - m_first_child); + tree.link_children(node, m_first_child, nu_children); +} + + +template +Tree::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(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(numeric_limits::max())); + m_nu_threads = nu_threads; + m_max_nodes = max_nodes; + + // Using make_unique(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(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 +inline void Tree::add_value(const Node& node, Float v) +{ + non_const(node).add_value(v); +} + +template +inline void Tree::add_value(const Node& node, Float v, Float weight) +{ + non_const(node).add_value(v, weight); +} + +template +void Tree::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 +bool Tree::contains(const Node& node) const +{ + return &node >= m_nodes.get() && &node < m_nodes.get() + m_max_nodes; +} + +template +void Tree::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 +void Tree::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(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 +void Tree::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 +size_t Tree::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 +inline auto Tree::get_root() const -> const Node& +{ + return m_nodes[0]; +} + +/** Get the thread storage a node belongs to. */ +template +inline unsigned Tree::get_thread_storage(const Node& node) const +{ + size_t diff = &node - m_nodes.get(); + return static_cast(diff / m_nodes_per_thread); +} + +template +inline void Tree::inc_visit_count(const Node& node) +{ + non_const(node).inc_visit_count(); +} + +template +inline void Tree::link_children(const Node& node, const Node* first_child, + unsigned short nu_children) +{ + auto first_child_idx = static_cast(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 +inline auto Tree::non_const(const Node& node) const -> Node& +{ + LIBBOARDGAME_ASSERT(contains(node)); + return const_cast(node); +} + +template +inline void Tree::add_value_remove_loss(const Node& node, Float v) +{ + non_const(node).add_value_remove_loss(v); +} + +template +void Tree::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 m_thread_storage; + unique_ptr 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 diff --git a/src/libboardgame_mcts/TreeUtil.h b/src/libboardgame_mcts/TreeUtil.h new file mode 100644 index 0000000..9b2ef2f --- /dev/null +++ b/src/libboardgame_mcts/TreeUtil.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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 +const N* find_child(const Tree& 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 +const N* find_node(const Tree& 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 diff --git a/src/libboardgame_sgf/CMakeLists.txt b/src/libboardgame_sgf/CMakeLists.txt new file mode 100644 index 0000000..86730bd --- /dev/null +++ b/src/libboardgame_sgf/CMakeLists.txt @@ -0,0 +1,22 @@ +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) diff --git a/src/libboardgame_sgf/Reader.cpp b/src/libboardgame_sgf/Reader.cpp new file mode 100644 index 0000000..20d4c6d --- /dev/null +++ b/src/libboardgame_sgf/Reader.cpp @@ -0,0 +1,247 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Reader.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Reader.h" + +#include +#include +#include +#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& 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 diff --git a/src/libboardgame_sgf/Reader.h b/src/libboardgame_sgf/Reader.h new file mode 100644 index 0000000..a847d70 --- /dev/null +++ b/src/libboardgame_sgf/Reader.h @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include + +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& 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 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 diff --git a/src/libboardgame_sgf/SgfError.cpp b/src/libboardgame_sgf/SgfError.cpp new file mode 100644 index 0000000..619bd39 --- /dev/null +++ b/src/libboardgame_sgf/SgfError.cpp @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfError.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "SgfError.h" + +#include + +namespace libboardgame_sgf { + +//----------------------------------------------------------------------------- + +MissingProperty::MissingProperty(const string& id) + : SgfError("Missing SGF property '" + id + "'") +{ +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/SgfError.h b/src/libboardgame_sgf/SgfError.h new file mode 100644 index 0000000..89de646 --- /dev/null +++ b/src/libboardgame_sgf/SgfError.h @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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 + InvalidProperty(const string& id, const T& value); + +private: + template + static string get_message(const string& id, const T& value); +}; + +template +InvalidProperty::InvalidProperty(const string& id, const T& value) + : SgfError(get_message(id, value)) +{ +} + +template +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 diff --git a/src/libboardgame_sgf/SgfNode.cpp b/src/libboardgame_sgf/SgfNode.cpp new file mode 100644 index 0000000..3557381 --- /dev/null +++ b/src/libboardgame_sgf/SgfNode.cpp @@ -0,0 +1,287 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfNode.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "SgfNode.h" + +#include + +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 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(); + 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::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& 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 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::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 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 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 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::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::remove_child(SgfNode& child) +{ + auto node = &m_first_child; + unique_ptr* previous = nullptr; + while (true) + { + if (node->get() == &child) + { + unique_ptr 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 diff --git a/src/libboardgame_sgf/SgfNode.h b/src/libboardgame_sgf/SgfNode.h new file mode 100644 index 0000000..5787d42 --- /dev/null +++ b/src/libboardgame_sgf/SgfNode.h @@ -0,0 +1,390 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#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 values; + + unique_ptr next; + + Property(const Property& p) + : id(p.id), + values(p.values) + { } + + Property(const string& id, const vector& 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 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& get_multi_property(const string& id) const; + + /** Get property parsed as a type. + @throws InvalidProperty + @throws MissingProperty */ + template + T parse_property(const string& id) const; + + /** Get property parsed as a type with default value. + @throws InvalidProperty */ + template + T parse_property(const string& id, const T& default_value) const; + + /** @return true, if property was added or changed. */ + template + 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 + bool set_property(const string& id, const vector& 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& 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 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 m_first_child; + + unique_ptr 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 m_properties; + + forward_list::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(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 +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 +T SgfNode::parse_property(const string& id, const T& default_value) const +{ + if (! has_property(id)) + return default_value; + return parse_property(id); +} + +inline void SgfNode::remove_children() +{ + m_first_child.reset(); +} + +template +bool SgfNode::set_property(const string& id, const T& value) +{ + vector values(1, value); + return set_property(id, values); +} + +inline bool SgfNode::set_property(const string& id, const char* value) +{ + return set_property(id, value); +} + +template +bool SgfNode::set_property(const string& id, const vector& values) +{ + vector values_to_string; + values_to_string.reserve(values.size()); + for (const T& v : values) + values_to_string.push_back(to_string(v)); + forward_list::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 diff --git a/src/libboardgame_sgf/SgfTree.cpp b/src/libboardgame_sgf/SgfTree.cpp new file mode 100644 index 0000000..66b9eda --- /dev/null +++ b/src/libboardgame_sgf/SgfTree.cpp @@ -0,0 +1,265 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "SgfTree.h" + +#include +#include +#include +#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("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("TE", 0); +} + +unique_ptr 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(); + m_root = move(root); + m_modified = false; +} + +void SgfTree::init(unique_ptr& 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 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 diff --git a/src/libboardgame_sgf/SgfTree.h b/src/libboardgame_sgf/SgfTree.h new file mode 100644 index 0000000..034345e --- /dev/null +++ b/src/libboardgame_sgf/SgfTree.h @@ -0,0 +1,282 @@ +//----------------------------------------------------------------------------- +/** @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& root); + + /** Get the root node and transfer the ownership to the caller. */ + unique_ptr 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 + 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 + void set_property(const SgfNode& node, const string& id, + const vector& 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 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 m_root; + + SgfNode& non_const(const SgfNode& node); +}; + +inline void SgfTree::append(const SgfNode& node, unique_ptr 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(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 +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 +void SgfTree::set_property(const SgfNode& node, const string& id, + const vector& 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 diff --git a/src/libboardgame_sgf/SgfUtil.cpp b/src/libboardgame_sgf/SgfUtil.cpp new file mode 100644 index 0000000..4775289 --- /dev/null +++ b/src/libboardgame_sgf/SgfUtil.cpp @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/SgfUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "SgfUtil.h" + +#include +#include +#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& 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 diff --git a/src/libboardgame_sgf/SgfUtil.h b/src/libboardgame_sgf/SgfUtil.h new file mode 100644 index 0000000..cf7dcf0 --- /dev/null +++ b/src/libboardgame_sgf/SgfUtil.h @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @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 +#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& 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 diff --git a/src/libboardgame_sgf/TreeReader.cpp b/src/libboardgame_sgf/TreeReader.cpp new file mode 100644 index 0000000..e48ad2b --- /dev/null +++ b/src/libboardgame_sgf/TreeReader.cpp @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @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 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(); + 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& values) +{ + m_current->set_property(identifier, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/TreeReader.h b/src/libboardgame_sgf/TreeReader.h new file mode 100644 index 0000000..c2161f3 --- /dev/null +++ b/src/libboardgame_sgf/TreeReader.h @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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& values) override; + + const SgfNode& get_tree() const { return *m_root; } + + /** Get the tree and transfer the ownership to the caller. */ + unique_ptr get_tree_transfer_ownership(); + +private: + SgfNode* m_current = nullptr; + + unique_ptr m_root; + + stack m_stack; +}; + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf + +#endif // LIBBOARDGAME_SGF_TREE_READER_H diff --git a/src/libboardgame_sgf/TreeWriter.cpp b/src/libboardgame_sgf/TreeWriter.cpp new file mode 100644 index 0000000..0ea7773 --- /dev/null +++ b/src/libboardgame_sgf/TreeWriter.cpp @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +/** @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& values) +{ + m_writer.write_property(id, values); +} + +//----------------------------------------------------------------------------- + +} // namespace libboardgame_sgf diff --git a/src/libboardgame_sgf/TreeWriter.h b/src/libboardgame_sgf/TreeWriter.h new file mode 100644 index 0000000..3d34599 --- /dev/null +++ b/src/libboardgame_sgf/TreeWriter.h @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +/** @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& 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 diff --git a/src/libboardgame_sgf/Writer.cpp b/src/libboardgame_sgf/Writer.cpp new file mode 100644 index 0000000..e1d083d --- /dev/null +++ b/src/libboardgame_sgf/Writer.cpp @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sgf/Writer.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Writer.h" + +#include + +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(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(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 diff --git a/src/libboardgame_sgf/Writer.h b/src/libboardgame_sgf/Writer.h new file mode 100644 index 0000000..e488ea2 --- /dev/null +++ b/src/libboardgame_sgf/Writer.h @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#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 + void write_property(const string& id, const T& value); + + template + void write_property(const string& id, const vector& 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 values(1, value); + write_property(id, values); +} + +template +void Writer::write_property(const string& id, const T& value) +{ + vector values(1, value); + write_property(id, values); +} + +template +void Writer::write_property(const string& id, const vector& 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 diff --git a/src/libboardgame_sys/CMakeLists.txt b/src/libboardgame_sys/CMakeLists.txt new file mode 100644 index 0000000..9e44513 --- /dev/null +++ b/src/libboardgame_sys/CMakeLists.txt @@ -0,0 +1,24 @@ +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 ..) diff --git a/src/libboardgame_sys/Compiler.h b/src/libboardgame_sys/Compiler.h new file mode 100644 index 0000000..70cda1b --- /dev/null +++ b/src/libboardgame_sys/Compiler.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#ifdef __GNUC__ +#include +#include +#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 +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 diff --git a/src/libboardgame_sys/CpuTime.cpp b/src/libboardgame_sys/CpuTime.cpp new file mode 100644 index 0000000..efdacc5 --- /dev/null +++ b/src/libboardgame_sys/CpuTime.cpp @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/CpuTime.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "CpuTime.h" + +#ifdef _WIN32 +#include +#endif + +#ifdef HAVE_UNISTD_H +#include +#endif + +#ifdef HAVE_SYS_TIMES_H +#include +#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 diff --git a/src/libboardgame_sys/CpuTime.h b/src/libboardgame_sys/CpuTime.h new file mode 100644 index 0000000..45b0f13 --- /dev/null +++ b/src/libboardgame_sys/CpuTime.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_sys/Memory.cpp b/src/libboardgame_sys/Memory.cpp new file mode 100644 index 0000000..cc0e91a --- /dev/null +++ b/src/libboardgame_sys/Memory.cpp @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_sys/Memory.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Memory.h" + +#ifdef _WIN32 +#include +#include +#else +#include +#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 +#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(status.ullTotalVirtual); + auto total_phys = static_cast(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(phys_pages) * static_cast(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 diff --git a/src/libboardgame_sys/Memory.h b/src/libboardgame_sys/Memory.h new file mode 100644 index 0000000..769aa1f --- /dev/null +++ b/src/libboardgame_sys/Memory.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +/** @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 + +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 diff --git a/src/libboardgame_test/CMakeLists.txt b/src/libboardgame_test/CMakeLists.txt new file mode 100644 index 0000000..0ecfcea --- /dev/null +++ b/src/libboardgame_test/CMakeLists.txt @@ -0,0 +1,12 @@ +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) diff --git a/src/libboardgame_test/Main.cpp b/src/libboardgame_test/Main.cpp new file mode 100644 index 0000000..2b9a586 --- /dev/null +++ b/src/libboardgame_test/Main.cpp @@ -0,0 +1,16 @@ +//----------------------------------------------------------------------------- +/** @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); +} + +//---------------------------------------------------------------------------- diff --git a/src/libboardgame_test/Test.cpp b/src/libboardgame_test/Test.cpp new file mode 100644 index 0000000..7077bae --- /dev/null +++ b/src/libboardgame_test/Test.cpp @@ -0,0 +1,111 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_test/Test.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Test.h" + +#include +#include "libboardgame_util/Assert.h" +#include "libboardgame_util/Log.h" + +namespace libboardgame_test { + +//----------------------------------------------------------------------------- + +namespace { + +map& get_all_tests() +{ + static map 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 diff --git a/src/libboardgame_test/Test.h b/src/libboardgame_test/Test.h new file mode 100644 index 0000000..c541941 --- /dev/null +++ b/src/libboardgame_test/Test.h @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#include + +namespace libboardgame_test { + +using namespace std; + +//----------------------------------------------------------------------------- + +using TestFunction = function; + +//----------------------------------------------------------------------------- + +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 diff --git a/src/libboardgame_util/Abort.cpp b/src/libboardgame_util/Abort.cpp new file mode 100644 index 0000000..d447ff1 --- /dev/null +++ b/src/libboardgame_util/Abort.cpp @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Abort.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Abort.h" + +//---------------------------------------------------------------------------- + +namespace libboardgame_util { + +atomic abort(false); + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/Abort.h b/src/libboardgame_util/Abort.h new file mode 100644 index 0000000..fdd8a40 --- /dev/null +++ b/src/libboardgame_util/Abort.h @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +/** @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 + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +extern atomic 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 diff --git a/src/libboardgame_util/ArrayList.h b/src/libboardgame_util/ArrayList.h new file mode 100644 index 0000000..7822e60 --- /dev/null +++ b/src/libboardgame_util/ArrayList.h @@ -0,0 +1,350 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#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 +class ArrayList +{ +public: + using IntType = I; + + static_assert(numeric_limits::is_integer, ""); + + static const IntType max_size = M; + + using iterator = typename array::iterator; + + using const_iterator = typename array::const_iterator; + + using value_type = T; + + + ArrayList() = default; + + ArrayList(const ArrayList& l); + + ArrayList(const initializer_list& 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 m_a; + + I m_size = 0; +}; + +template +ArrayList::ArrayList(const ArrayList& l) +{ + *this = l; +} + +template +ArrayList::ArrayList(const initializer_list& l) + : m_size(0) +{ + for (auto& t : l) + push_back(t); +} + +template +auto ArrayList::operator=(const ArrayList& l) -> ArrayList& +{ + m_size = l.size(); + copy(l.begin(), l.end(), begin()); + return *this; +} + +template +inline T& ArrayList::operator[](I i) +{ + LIBBOARDGAME_ASSERT(i < m_size); + return m_a[i]; +} + +template +inline const T& ArrayList::operator[](I i) const +{ + LIBBOARDGAME_ASSERT(i < m_size); + return m_a[i]; +} + +template +bool ArrayList::operator==(const ArrayList& array_list) const +{ + return equal(begin(), end(), array_list.begin(), array_list.end()); +} + +template +bool ArrayList::operator!=(const ArrayList& array_list) const +{ + return ! operator==(array_list); +} + +template +inline void ArrayList::assign(const T& t) +{ + m_size = 1; + m_a[0] = t; +} + +template +inline T& ArrayList::back() +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[m_size - 1]; +} + +template +inline const T& ArrayList::back() const +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[m_size - 1]; +} + +template +inline auto ArrayList::begin() -> iterator +{ + return m_a.begin(); +} + +template +inline auto ArrayList::begin() const -> const_iterator +{ + return m_a.begin(); +} + +template +inline void ArrayList::clear() +{ + m_size = 0; +} + +template +bool ArrayList::contains(const T& t) const +{ + return find(begin(), end(), t) != end(); +} + +template +inline bool ArrayList::empty() const +{ + return m_size == 0; +} + +template +inline auto ArrayList::end() -> iterator +{ + return begin() + m_size; +} + +template +inline auto ArrayList::end() const -> const_iterator +{ + return begin() + m_size; +} + +template +inline T& ArrayList::get_unchecked(I i) +{ + LIBBOARDGAME_ASSERT(i < max_size); + return m_a[i]; +} + +template +inline const T& ArrayList::get_unchecked(I i) const +{ + LIBBOARDGAME_ASSERT(i < max_size); + return m_a[i]; +} + +template +bool ArrayList::include(const T& t) +{ + if (contains(t)) + return false; + push_back(t); + return true; +} + +template +inline const T& ArrayList::pop_back() +{ + LIBBOARDGAME_ASSERT(m_size > 0); + return m_a[--m_size]; +} + +template +inline void ArrayList::push_back(const T& t) +{ + LIBBOARDGAME_ASSERT(m_size < max_size); + m_a[m_size++] = t; +} + +template +inline bool ArrayList::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 +inline bool ArrayList::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 +inline void ArrayList::remove_fast(iterator i) +{ + LIBBOARDGAME_ASSERT(i >= begin()); + LIBBOARDGAME_ASSERT(i < end()); + --m_size; + *i = *(begin() + m_size); +} + +template +inline void ArrayList::resize(I size) +{ + LIBBOARDGAME_ASSERT(size <= max_size); + m_size = size; +} + +template +inline I ArrayList::size() const +{ + return m_size; +} + +//----------------------------------------------------------------------------- + +template +ostream& operator<<(ostream& out, const ArrayList& 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 diff --git a/src/libboardgame_util/Assert.cpp b/src/libboardgame_util/Assert.cpp new file mode 100644 index 0000000..bbd9c3d --- /dev/null +++ b/src/libboardgame_util/Assert.cpp @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/Assert.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Assert.h" + +#include + +#ifdef LIBBOARDGAME_DEBUG +#include +#include +#include +#include +#include +#include "Log.h" +#endif + +#ifdef LIBBOARDGAME_DISABLE_LOG +#include "Unused.h" +#endif + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +namespace { + +list& get_all_handlers() +{ + static list 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 diff --git a/src/libboardgame_util/Assert.h b/src/libboardgame_util/Assert.h new file mode 100644 index 0000000..36671dd --- /dev/null +++ b/src/libboardgame_util/Assert.h @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +/** @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(0)) \ + : libboardgame_util::handle_assertion(#expr, __FILE__, __LINE__)) +#else +#define LIBBOARDGAME_ASSERT(expr) (static_cast(0)) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_ASSERT_H diff --git a/src/libboardgame_util/Barrier.cpp b/src/libboardgame_util/Barrier.cpp new file mode 100644 index 0000000..30a69b4 --- /dev/null +++ b/src/libboardgame_util/Barrier.cpp @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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 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 diff --git a/src/libboardgame_util/Barrier.h b/src/libboardgame_util/Barrier.h new file mode 100644 index 0000000..f96cef8 --- /dev/null +++ b/src/libboardgame_util/Barrier.h @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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 diff --git a/src/libboardgame_util/CMakeLists.txt b/src/libboardgame_util/CMakeLists.txt new file mode 100644 index 0000000..d6c7318 --- /dev/null +++ b/src/libboardgame_util/CMakeLists.txt @@ -0,0 +1,41 @@ +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 + "$<$:-DLIBBOARDGAME_DEBUG>") + +target_include_directories(boardgame_util PUBLIC ..) + +target_link_libraries(boardgame_util boardgame_sys) diff --git a/src/libboardgame_util/CpuTimeSource.cpp b/src/libboardgame_util/CpuTimeSource.cpp new file mode 100644 index 0000000..5b1df44 --- /dev/null +++ b/src/libboardgame_util/CpuTimeSource.cpp @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/CpuTimeSource.h b/src/libboardgame_util/CpuTimeSource.h new file mode 100644 index 0000000..b4ad8de --- /dev/null +++ b/src/libboardgame_util/CpuTimeSource.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/FmtSaver.h b/src/libboardgame_util/FmtSaver.h new file mode 100644 index 0000000..a832e0f --- /dev/null +++ b/src/libboardgame_util/FmtSaver.h @@ -0,0 +1,44 @@ +//---------------------------------------------------------------------------- +/** @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 + +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 diff --git a/src/libboardgame_util/IntervalChecker.cpp b/src/libboardgame_util/IntervalChecker.cpp new file mode 100644 index 0000000..9573023 --- /dev/null +++ b/src/libboardgame_util/IntervalChecker.cpp @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/IntervalChecker.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "IntervalChecker.h" + +#include +#include "Assert.h" + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +IntervalChecker::IntervalChecker(TimeSource& time_source, double time_interval, + const function& 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::max())) + m_count_interval = numeric_limits::max(); + else if (new_count_interval < 1) + m_count_interval = 1; + else + m_count_interval = static_cast(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 diff --git a/src/libboardgame_util/IntervalChecker.h b/src/libboardgame_util/IntervalChecker.h new file mode 100644 index 0000000..6522499 --- /dev/null +++ b/src/libboardgame_util/IntervalChecker.h @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------------- +/** @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 +#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& 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 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 diff --git a/src/libboardgame_util/Log.cpp b/src/libboardgame_util/Log.cpp new file mode 100644 index 0000000..28d040a --- /dev/null +++ b/src/libboardgame_util/Log.cpp @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------- +/** @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 + +#if defined ANDROID || defined __ANDROID__ +#include +#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 diff --git a/src/libboardgame_util/Log.h b/src/libboardgame_util/Log.h new file mode 100644 index 0000000..796b80a --- /dev/null +++ b/src/libboardgame_util/Log.h @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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 +void _log_buffered(ostream& buffer, const T& t) +{ + buffer << t; +} + +/** Helper function needed for log(const Ts&...) */ +template +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 +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(0)) +#endif + +//----------------------------------------------------------------------------- + +#endif // LIBBOARDGAME_UTIL_LOG_H diff --git a/src/libboardgame_util/MathUtil.h b/src/libboardgame_util/MathUtil.h new file mode 100644 index 0000000..f58a9a7 --- /dev/null +++ b/src/libboardgame_util/MathUtil.h @@ -0,0 +1,41 @@ +//---------------------------------------------------------------------------- +/** @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 +inline T fast_exp(T x) +{ + x = static_cast(1) + x / static_cast(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 diff --git a/src/libboardgame_util/Options.cpp b/src/libboardgame_util/Options.cpp new file mode 100644 index 0000000..46c9413 --- /dev/null +++ b/src/libboardgame_util/Options.cpp @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------------- +/** @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& 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& specs) + : Options(argc, const_cast(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 diff --git a/src/libboardgame_util/Options.h b/src/libboardgame_util/Options.h new file mode 100644 index 0000000..4f3504e --- /dev/null +++ b/src/libboardgame_util/Options.h @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#include +#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& 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& 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 + 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 + 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& get_args() const; + +private: + set m_names; + + vector m_args; + + map m_map; + + void check_name(const string& name) const; +}; + +template +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 +T Options::get(const string& name, const T& default_value) const +{ + if (! contains(name)) + return default_value; + return get(name); +} + +inline const vector& Options::get_args() const +{ + return m_args; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_OPTIONS_H diff --git a/src/libboardgame_util/RandomGenerator.cpp b/src/libboardgame_util/RandomGenerator.cpp new file mode 100644 index 0000000..6a1d29b --- /dev/null +++ b/src/libboardgame_util/RandomGenerator.cpp @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/RandomGenerator.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "RandomGenerator.h" + +#include + +namespace libboardgame_util { + +//---------------------------------------------------------------------------- + +namespace { + +bool is_seed_set = false; + +RandomGenerator::ResultType the_seed; + +list& get_all_generators() +{ + static list 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 diff --git a/src/libboardgame_util/RandomGenerator.h b/src/libboardgame_util/RandomGenerator.h new file mode 100644 index 0000000..a443892 --- /dev/null +++ b/src/libboardgame_util/RandomGenerator.h @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------------- +/** @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 + +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 distribution(a, b); + return distribution(m_generator); +} + +inline float RandomGenerator::generate_float(float a, float b) +{ + uniform_real_distribution 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 diff --git a/src/libboardgame_util/Range.h b/src/libboardgame_util/Range.h new file mode 100644 index 0000000..601ebf8 --- /dev/null +++ b/src/libboardgame_util/Range.h @@ -0,0 +1,54 @@ +//---------------------------------------------------------------------------- +/** @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 + +namespace libboardgame_util { + +//----------------------------------------------------------------------------- + +template +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 +bool Range::contains(T& t) const +{ + for (auto& i : *this) + if (i == t) + return true; + return false; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_RANGE_H diff --git a/src/libboardgame_util/Statistics.h b/src/libboardgame_util/Statistics.h new file mode 100644 index 0000000..02e4b6b --- /dev/null +++ b/src/libboardgame_util/Statistics.h @@ -0,0 +1,457 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#include +#include +#include +#include "FmtSaver.h" + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +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 +inline StatisticsBase::StatisticsBase(FLOAT init_val) +{ + clear(init_val); +} + +template +void StatisticsBase::add(FLOAT val) +{ + FLOAT count = m_count; + ++count; + val -= m_mean; + m_mean += val / count; + m_count = count; +} + +template +inline void StatisticsBase::clear(FLOAT init_val) +{ + m_count = 0; + m_mean = init_val; +} + +template +inline FLOAT StatisticsBase::get_count() const +{ + return m_count; +} + +template +inline FLOAT StatisticsBase::get_mean() const +{ + return m_mean; +} + +template +void StatisticsBase::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << m_mean; +} + +//---------------------------------------------------------------------------- + +template +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 m_statistics_base; + + FLOAT m_variance; +}; + +template +inline Statistics::Statistics(FLOAT init_val) +{ + clear(init_val); +} + +template +void Statistics::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 +inline void Statistics::clear(FLOAT init_val) +{ + m_statistics_base.clear(init_val); + m_variance = 0; +} + +template +inline FLOAT Statistics::get_count() const +{ + return m_statistics_base.get_count(); +} + +template +inline FLOAT Statistics::get_deviation() const +{ + // m_variance can become negative (due to rounding errors?) + return m_variance < 0 ? 0 : sqrt(m_variance); +} + +template +FLOAT Statistics::get_error() const +{ + auto count = get_count(); + return count == 0 ? 0 : get_deviation() / sqrt(count); +} + +template +inline FLOAT Statistics::get_mean() const +{ + return m_statistics_base.get_mean(); +} + +template +inline FLOAT Statistics::get_variance() const +{ + return m_variance; +} + +template +void Statistics::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 +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 m_statistics; + + FLOAT m_max; + + FLOAT m_min; +}; + +template +inline StatisticsExt::StatisticsExt(FLOAT init_val) +{ + clear(init_val); +} + +template +void StatisticsExt::add(FLOAT val) +{ + m_statistics.add(val); + if (val > m_max) + m_max = val; + if (val < m_min) + m_min = val; +} + +template +inline void StatisticsExt::clear(FLOAT init_val) +{ + m_statistics.clear(init_val); + m_min = numeric_limits::max(); + m_max = -numeric_limits::max(); +} + +template +inline FLOAT StatisticsExt::get_count() const +{ + return m_statistics.get_count(); +} + +template +inline FLOAT StatisticsExt::get_deviation() const +{ + return m_statistics.get_deviation(); +} + +template +inline FLOAT StatisticsExt::get_error() const +{ + return m_statistics.get_error(); +} + +template +inline FLOAT StatisticsExt::get_max() const +{ + return m_max; +} + +template +inline FLOAT StatisticsExt::get_mean() const +{ + return m_statistics.get_mean(); +} + +template +inline FLOAT StatisticsExt::get_min() const +{ + return m_min; +} + +template +inline FLOAT StatisticsExt::get_variance() const +{ + return m_statistics.get_variance(); +} + +template +string StatisticsExt::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 +void StatisticsExt::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::max()) + out << "-"; + else + out << m_min; + out << " max="; + if (m_max == -numeric_limits::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 +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 m_count; + + atomic m_mean; +}; + +template +inline StatisticsDirtyLockFree::StatisticsDirtyLockFree(FLOAT init_val) +{ + clear(init_val); +} + +template +StatisticsDirtyLockFree& +StatisticsDirtyLockFree::operator=(const StatisticsDirtyLockFree& s) +{ + m_count = s.m_count.load(); + m_mean = s.m_mean.load(); + return *this; +} + +template +void StatisticsDirtyLockFree::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 +inline void StatisticsDirtyLockFree::clear(FLOAT init_val) +{ + init(init_val, 0); +} + +template +inline FLOAT StatisticsDirtyLockFree::get_count() const +{ + return m_count.load(memory_order_relaxed); +} + +template +inline FLOAT StatisticsDirtyLockFree::get_mean() const +{ + return m_mean.load(memory_order_relaxed); +} + +template +inline void StatisticsDirtyLockFree::init(FLOAT mean, FLOAT count) +{ + m_count = count; + m_mean = mean; +} + +template +void StatisticsDirtyLockFree::write(ostream& out, bool fixed, + unsigned precision) const +{ + FmtSaver saver(out); + if (fixed) + out << std::fixed; + out << setprecision(precision) << get_mean(); +} + +//---------------------------------------------------------------------------- + +template +inline ostream& operator<<(ostream& out, const StatisticsExt& s) +{ + s.write(out); + return out; +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util + +#endif // LIBBOARDGAME_UTIL_STATISTICS_H diff --git a/src/libboardgame_util/StringUtil.cpp b/src/libboardgame_util/StringUtil.cpp new file mode 100644 index 0000000..8659f8a --- /dev/null +++ b/src/libboardgame_util/StringUtil.cpp @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/StringUtil.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "StringUtil.h" + +#include +#include +#include + +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 split(const string& s, char separator) +{ + vector 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(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(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 diff --git a/src/libboardgame_util/StringUtil.h b/src/libboardgame_util/StringUtil.h new file mode 100644 index 0000000..65fb83b --- /dev/null +++ b/src/libboardgame_util/StringUtil.h @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include + +namespace libboardgame_util { + +using namespace std; + +//----------------------------------------------------------------------------- + +template +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 split(const string& s, char separator); + +string time_to_string(double seconds, bool with_seconds_as_double = false); + +template +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 diff --git a/src/libboardgame_util/TimeIntervalChecker.cpp b/src/libboardgame_util/TimeIntervalChecker.cpp new file mode 100644 index 0000000..f3dfed8 --- /dev/null +++ b/src/libboardgame_util/TimeIntervalChecker.cpp @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/TimeIntervalChecker.h b/src/libboardgame_util/TimeIntervalChecker.h new file mode 100644 index 0000000..d898b02 --- /dev/null +++ b/src/libboardgame_util/TimeIntervalChecker.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/TimeSource.cpp b/src/libboardgame_util/TimeSource.cpp new file mode 100644 index 0000000..6976be2 --- /dev/null +++ b/src/libboardgame_util/TimeSource.cpp @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/TimeSource.h b/src/libboardgame_util/TimeSource.h new file mode 100644 index 0000000..bc040fd --- /dev/null +++ b/src/libboardgame_util/TimeSource.h @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/Timer.cpp b/src/libboardgame_util/Timer.cpp new file mode 100644 index 0000000..e6b72f5 --- /dev/null +++ b/src/libboardgame_util/Timer.cpp @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/Timer.h b/src/libboardgame_util/Timer.h new file mode 100644 index 0000000..13a23b9 --- /dev/null +++ b/src/libboardgame_util/Timer.h @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libboardgame_util/Unused.h b/src/libboardgame_util/Unused.h new file mode 100644 index 0000000..4f50ba3 --- /dev/null +++ b/src/libboardgame_util/Unused.h @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------------- +/** @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 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 diff --git a/src/libboardgame_util/WallTimeSource.cpp b/src/libboardgame_util/WallTimeSource.cpp new file mode 100644 index 0000000..89ef238 --- /dev/null +++ b/src/libboardgame_util/WallTimeSource.cpp @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +/** @file libboardgame_util/WallTimeSource.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "WallTimeSource.h" + +#include + +namespace libboardgame_util { + +using namespace std::chrono; + +//----------------------------------------------------------------------------- + +double WallTimeSource::operator()() +{ + auto t = system_clock::now().time_since_epoch(); + return duration_cast>(t).count(); +} + +//---------------------------------------------------------------------------- + +} // namespace libboardgame_util diff --git a/src/libboardgame_util/WallTimeSource.h b/src/libboardgame_util/WallTimeSource.h new file mode 100644 index 0000000..9c99371 --- /dev/null +++ b/src/libboardgame_util/WallTimeSource.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/Board.cpp b/src/libpentobi_base/Board.cpp new file mode 100644 index 0000000..88fa85a --- /dev/null +++ b/src/libpentobi_base/Board.cpp @@ -0,0 +1,872 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Board.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Board.h" + +#include +#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::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 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(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(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 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(x), + static_cast(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(width - 1), + static_cast(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& 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 diff --git a/src/libpentobi_base/Board.h b/src/libpentobi_base/Board.h new file mode 100644 index 0000000..273d681 --- /dev/null +++ b/src/libpentobi_base/Board.h @@ -0,0 +1,919 @@ +//----------------------------------------------------------------------------- +/** @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; + + /** Maximum number of pieces per player in any game variant. */ + static const unsigned max_pieces = Setup::max_pieces; + + using PiecesLeftList = ArrayList; + + 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 + 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& 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& 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 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& + 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 forbidden; + + Grid is_attach_point; + + PiecesLeftList pieces_left; + + PieceMap nu_left_piece; + + unsigned nu_onboard_pieces; + + ScoreType points; + }; + + /** Snapshot for fast restoration of a previous position. */ + struct Snapshot + { + StateBase state_base; + + ColorMap state_color; + + unsigned moves_size; + + ColorMap attach_points_size; + }; + + + StateBase m_state_base; + + ColorMap 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 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 m_is_center_section; + + /** The 1x1 piece. */ + Piece m_one_piece; + + ColorMap m_attach_points; + + /** See get_second_color() */ + ColorMap m_second_color; + + ColorMap m_color_char; + + ColorMap m_color_esc_sequence; + + ColorMap m_color_esc_sequence_text; + + ColorMap m_color_name; + + ArrayList 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 + 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& 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(is_forbidden(*i, c)); + for (unsigned j = 1; j < PrecompMoves::adj_status_nu_adj; ++j) + result |= (static_cast(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(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 Board::get_move_points(Move mv) const +{ + return m_bc->get_move_points(mv); +} + +inline auto Board::get_moves() const -> const ArrayList& +{ + 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(m_nu_players); + for (Color i : get_colors()) + if (i != c) + score -= get_points(i); + score = get_points(c) + score / (static_cast(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& + 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& 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 +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(mv, m_move_info_array); + auto& info_ext = BoardConst::get_move_info_ext( + 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 +inline void Board::play(Color c, Move mv) +{ + place(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 diff --git a/src/libpentobi_base/BoardConst.cpp b/src/libpentobi_base/BoardConst.cpp new file mode 100644 index 0000000..a6bbb1d --- /dev/null +++ b/src/libpentobi_base/BoardConst.cpp @@ -0,0 +1,1425 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/BoardConst.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "BoardConst.h" + +#include +#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, 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 create_pieces_callisto(const Geometry& geo, + const PieceTransforms& transforms) +{ + auto geometry_type = GeometryType::callisto; + vector 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 create_pieces_classic(const Geometry& geo, + const PieceTransforms& transforms) +{ + auto geometry_type = GeometryType::classic; + vector 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 create_pieces_gembloq(const Geometry& geo, + const PieceTransforms& transforms) +{ + auto geometry_type = GeometryType::gembloq; + vector 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 create_pieces_junior(const Geometry& geo, + const PieceTransforms& transforms) +{ + auto geometry_type = GeometryType::classic; + vector 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 create_pieces_trigon(const Geometry& geo, + const PieceTransforms& transforms) +{ + auto geometry_type = GeometryType::trigon; + vector 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 create_pieces_nexos(const Geometry& geo, + const PieceTransforms& transforms) +{ + auto geometry_type = GeometryType::nexos; + vector 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(m_range); + m_nu_pieces = static_cast(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 +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(moves_created)); + void* place = + static_cast*>(m_move_info.get()) + + moves_created; + new(place) MoveInfo(piece, points); + place = + static_cast*>(m_move_info_ext.get()) + + moves_created; + auto& info_ext = *new(place) MoveInfoExt(); + 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( + 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(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(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(info_ext.size_attach_points)); + if (log_move_creation) + { + Grid 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 +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 transformed_points(nu_transforms); + vector 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(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(m_geo.get_x(p)); + auto y = static_cast(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( + 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>> 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 +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(mv); + auto& info_ext_2 = m_move_info_ext_2[i]; + info_ext_2.breaks_symmetry = false; + array 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(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 diff --git a/src/libpentobi_base/BoardConst.h b/src/libpentobi_base/BoardConst.h new file mode 100644 index 0000000..efd2ae1 --- /dev/null +++ b/src/libpentobi_base/BoardConst.h @@ -0,0 +1,347 @@ +//----------------------------------------------------------------------------- +/** @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; + + /** 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 + static const MoveInfo& + get_move_info(Move mv, MoveInfoArray move_info_array); + + template + static const MoveInfoExt& + 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 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 + const Point* get_move_points_begin(Move mv) const; + + Piece get_move_piece(Move mv) const; + + template + 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 m_pieces; + + Grid m_adj_status_points; + + unique_ptr m_transforms; + + PieceMap m_nu_attach_points{0}; + + /** Array of MoveInfo with MAX_SIZE being the maximum piece size + in the corresponding game variant. + See comments at MoveInfo. */ + unique_ptr m_move_info; + + /** Array of MoveInfoExt 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 m_move_info_ext; + + unique_ptr 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 m_compare_val; + + SymmetricPoints m_symmetric_points; + + + BoardConst(BoardType board_type, PieceSet piece_set); + + template + void create_move(unsigned& moves_created, Piece piece, + const MovePoints& points, Point label_pos); + + void create_moves(); + + template + void create_moves(unsigned& moves_created, Piece piece); + + template + const MoveInfo& get_move_info(Move mv) const; + + void init_adj_status_points(Point p); + + template + void init_symmetry_info(); +}; + +inline const Geometry& BoardConst::get_geometry() const +{ + return m_geo; +} + +template +inline const MoveInfo& +BoardConst::get_move_info(Move mv, MoveInfoArray move_info_array) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + return *(static_cast*>(move_info_array) + + mv.to_int()); +} + +template +inline const MoveInfo& BoardConst::get_move_info(Move mv) const +{ + LIBBOARDGAME_ASSERT(m_max_piece_size == MAX_SIZE); + return get_move_info(mv, m_move_info.get()); +} + +template +inline const MoveInfoExt& +BoardConst::get_move_info_ext(Move mv, MoveInfoExtArray move_info_ext_array) +{ + LIBBOARDGAME_ASSERT(! mv.is_null()); + return *(static_cast*>( + 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 +inline Piece BoardConst::get_move_piece(Move mv) const +{ + return get_move_info(mv).get_piece(); +} + +inline Range 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 +inline const Point* BoardConst::get_move_points_begin(Move mv) const +{ + return get_move_info(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 diff --git a/src/libpentobi_base/BoardUpdater.cpp b/src/libpentobi_base/BoardUpdater.cpp new file mode 100644 index 0000000..9a06e76 --- /dev/null +++ b/src/libpentobi_base/BoardUpdater.cpp @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------------- +/** @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; + +/** Helper function used in init_setup. */ +void handle_setup_property(const SgfNode& node, const char* id, Color c, + const Board& bd, Setup& setup, + ColorMap& 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& 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 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 diff --git a/src/libpentobi_base/BoardUpdater.h b/src/libpentobi_base/BoardUpdater.h new file mode 100644 index 0000000..1925313 --- /dev/null +++ b/src/libpentobi_base/BoardUpdater.h @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +/** @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 m_path; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_UPDATER_H diff --git a/src/libpentobi_base/BoardUtil.cpp b/src/libpentobi_base/BoardUtil.cpp new file mode 100644 index 0000000..3de20eb --- /dev/null +++ b/src/libpentobi_base/BoardUtil.cpp @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +/** @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 +#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& 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 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 diff --git a/src/libpentobi_base/BoardUtil.h b/src/libpentobi_base/BoardUtil.h new file mode 100644 index 0000000..049c8e1 --- /dev/null +++ b/src/libpentobi_base/BoardUtil.h @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @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& transform); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_BOARD_UTIL_H diff --git a/src/libpentobi_base/Book.cpp b/src/libpentobi_base/Book.cpp new file mode 100644 index 0000000..14bcc09 --- /dev/null +++ b/src/libpentobi_base/Book.cpp @@ -0,0 +1,123 @@ +//----------------------------------------------------------------------------- +/** @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 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 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(good_moves.size()); + return good_moves[m_random.generate() % nu_good_moves]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/Book.h b/src/libpentobi_base/Book.h new file mode 100644 index 0000000..8de5385 --- /dev/null +++ b/src/libpentobi_base/Book.h @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +/** @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 +#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; + + + PentobiTree m_tree; + + RandomGenerator m_random; + + vector> m_transforms; + + vector> 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 diff --git a/src/libpentobi_base/CMakeLists.txt b/src/libpentobi_base/CMakeLists.txt new file mode 100644 index 0000000..f723765 --- /dev/null +++ b/src/libpentobi_base/CMakeLists.txt @@ -0,0 +1,86 @@ +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) diff --git a/src/libpentobi_base/CallistoGeometry.cpp b/src/libpentobi_base/CallistoGeometry.cpp new file mode 100644 index 0000000..ddddcd9 --- /dev/null +++ b/src/libpentobi_base/CallistoGeometry.cpp @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/CallistoGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "CallistoGeometry.h" + +#include +#include +#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> s_geometry; + + auto pos = s_geometry.find(nu_colors); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr 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 + diff --git a/src/libpentobi_base/CallistoGeometry.h b/src/libpentobi_base/CallistoGeometry.h new file mode 100644 index 0000000..acfa45d --- /dev/null +++ b/src/libpentobi_base/CallistoGeometry.h @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/Color.h b/src/libpentobi_base/Color.h new file mode 100644 index 0000000..efa1f86 --- /dev/null +++ b/src/libpentobi_base/Color.h @@ -0,0 +1,168 @@ +//----------------------------------------------------------------------------- +/** @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 +#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(m_i + 1) % nu_colors); +} + +inline Color Color::get_previous(IntType nu_colors) const +{ + return Color(static_cast(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 +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 diff --git a/src/libpentobi_base/ColorMap.h b/src/libpentobi_base/ColorMap.h new file mode 100644 index 0000000..66b3610 --- /dev/null +++ b/src/libpentobi_base/ColorMap.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 +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 m_a; +}; + +template +inline ColorMap::ColorMap(const T& val) +{ + fill(val); +} + +template +inline T& ColorMap::operator[](Color c) +{ + return m_a[c.to_int()]; +} + +template +inline const T& ColorMap::operator[](Color c) const +{ + return m_a[c.to_int()]; +} + +template +void ColorMap::fill(const T& val) +{ + m_a.fill(val); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_COLOR_MAP_H diff --git a/src/libpentobi_base/ColorMove.h b/src/libpentobi_base/ColorMove.h new file mode 100644 index 0000000..9974a9b --- /dev/null +++ b/src/libpentobi_base/ColorMove.h @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/Engine.cpp b/src/libpentobi_base/Engine.cpp new file mode 100644 index 0000000..9480909 --- /dev/null +++ b/src/libpentobi_base/Engine.cpp @@ -0,0 +1,373 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Engine.h" + +#include +#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(); + auto marker = make_unique(); + 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(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()); + } + 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(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(1); + else if (name == "resign") + m_resign = args.parse(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 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 +
+ 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 diff --git a/src/libpentobi_base/Engine.h b/src/libpentobi_base/Engine.h new file mode 100644 index 0000000..b0ec4f1 --- /dev/null +++ b/src/libpentobi_base/Engine.h @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/Game.cpp b/src/libpentobi_base/Game.cpp new file mode 100644 index 0000000..c755874 --- /dev/null +++ b/src/libpentobi_base/Game.cpp @@ -0,0 +1,187 @@ +//----------------------------------------------------------------------------- +/** @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& 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 diff --git a/src/libpentobi_base/Game.h b/src/libpentobi_base/Game.h new file mode 100644 index 0000000..99e67a7 --- /dev/null +++ b/src/libpentobi_base/Game.h @@ -0,0 +1,420 @@ +//----------------------------------------------------------------------------- +/** @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& 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 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 diff --git a/src/libpentobi_base/GembloQGeometry.cpp b/src/libpentobi_base/GembloQGeometry.cpp new file mode 100644 index 0000000..2cd52c4 --- /dev/null +++ b/src/libpentobi_base/GembloQGeometry.cpp @@ -0,0 +1,165 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/GembloQGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "GembloQGeometry.h" + +#include +#include +#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> s_geometry; + + auto pos = s_geometry.find(nu_players); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr 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(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 + diff --git a/src/libpentobi_base/GembloQGeometry.h b/src/libpentobi_base/GembloQGeometry.h new file mode 100644 index 0000000..5db928a --- /dev/null +++ b/src/libpentobi_base/GembloQGeometry.h @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +/** @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: + + 0 1 2 3 4 5 6 7 8 + 0 | / | \ | / | \ | / + 1 | \ | / | \ | / | \ + 2 | / | \ | / | \ | / + 3 | \ | / | \ | / | \ + + 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 diff --git a/src/libpentobi_base/GembloQTransform.cpp b/src/libpentobi_base/GembloQTransform.cpp new file mode 100644 index 0000000..8094618 --- /dev/null +++ b/src/libpentobi_base/GembloQTransform.cpp @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +/** @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(p.y % 2 != 0); + break; + case 1: + case 2: + x -= static_cast(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(p.y % 2 != 0); + break; + case 1: + case 2: + x += static_cast(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(p.y % 2 != 0); + break; + case 1: + case 2: + x -= static_cast(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(p.y % 2 != 0); + break; + case 1: + case 2: + x += static_cast(p.y % 2 == 0); + break; + } + return {x, y}; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/GembloQTransform.h b/src/libpentobi_base/GembloQTransform.h new file mode 100644 index 0000000..77696d6 --- /dev/null +++ b/src/libpentobi_base/GembloQTransform.h @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/Geometry.h b/src/libpentobi_base/Geometry.h new file mode 100644 index 0000000..5900814 --- /dev/null +++ b/src/libpentobi_base/Geometry.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @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; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GEOMETRY_H diff --git a/src/libpentobi_base/Grid.h b/src/libpentobi_base/Grid.h new file mode 100644 index 0000000..78d4e72 --- /dev/null +++ b/src/libpentobi_base/Grid.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @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 +using Grid = libboardgame_base::Grid; + +template +using GridExt = libboardgame_base::GridExt; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_GRID_H diff --git a/src/libpentobi_base/Marker.h b/src/libpentobi_base/Marker.h new file mode 100644 index 0000000..0adb60a --- /dev/null +++ b/src/libpentobi_base/Marker.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @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; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MARKER_H diff --git a/src/libpentobi_base/Move.h b/src/libpentobi_base/Move.h new file mode 100644 index 0000000..3317205 --- /dev/null +++ b/src/libpentobi_base/Move.h @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 diff --git a/src/libpentobi_base/MoveInfo.h b/src/libpentobi_base/MoveInfo.h new file mode 100644 index 0000000..bf17f7f --- /dev/null +++ b/src/libpentobi_base/MoveInfo.h @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------------- +/** @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 +class MoveInfo +{ +public: + MoveInfo() = default; + + MoveInfo(Piece piece, const MovePoints& points) + { + m_piece = static_cast(piece.to_int()); + m_size = static_cast(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 +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 diff --git a/src/libpentobi_base/MoveList.h b/src/libpentobi_base/MoveList.h new file mode 100644 index 0000000..a4df562 --- /dev/null +++ b/src/libpentobi_base/MoveList.h @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +/** @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; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MOVE_LIST_H diff --git a/src/libpentobi_base/MoveMarker.h b/src/libpentobi_base/MoveMarker.h new file mode 100644 index 0000000..5e205af --- /dev/null +++ b/src/libpentobi_base/MoveMarker.h @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 + void set(const T& t) + { + for (Move mv : t) + set(mv); + } + + template + 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 m_a; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_MOVE_MARKER_H diff --git a/src/libpentobi_base/MovePoints.h b/src/libpentobi_base/MovePoints.h new file mode 100644 index 0000000..e24135b --- /dev/null +++ b/src/libpentobi_base/MovePoints.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @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; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +//----------------------------------------------------------------------------- + +#endif // LIBPENTOBI_BASE_MOVE_POINTS_H diff --git a/src/libpentobi_base/NexosGeometry.cpp b/src/libpentobi_base/NexosGeometry.cpp new file mode 100644 index 0000000..fdbcf95 --- /dev/null +++ b/src/libpentobi_base/NexosGeometry.cpp @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/NexosGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "NexosGeometry.h" + +#include +#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 s_geometry; + + if (! s_geometry) + s_geometry = make_unique(); + 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(x), static_cast(y)) != 3; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/NexosGeometry.h b/src/libpentobi_base/NexosGeometry.h new file mode 100644 index 0000000..9f7f25f --- /dev/null +++ b/src/libpentobi_base/NexosGeometry.h @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------- +/** @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: + + 0 1 2 3 4 5 6 ... + 0 + - + - + - + + 1 | | | | + 2 + - + - + - + + 3 | | | | + 4 + - + - + - + + + 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 diff --git a/src/libpentobi_base/NodeUtil.cpp b/src/libpentobi_base/NodeUtil.cpp new file mode 100644 index 0000000..0ce322d --- /dev/null +++ b/src/libpentobi_base/NodeUtil.cpp @@ -0,0 +1,198 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/NodeUtil.h b/src/libpentobi_base/NodeUtil.h new file mode 100644 index 0000000..0165ae2 --- /dev/null +++ b/src/libpentobi_base/NodeUtil.h @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PentobiSgfUtil.cpp b/src/libpentobi_base/PentobiSgfUtil.cpp new file mode 100644 index 0000000..3c294ad --- /dev/null +++ b/src/libpentobi_base/PentobiSgfUtil.cpp @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PentobiSgfUtil.h b/src/libpentobi_base/PentobiSgfUtil.h new file mode 100644 index 0000000..3d672a5 --- /dev/null +++ b/src/libpentobi_base/PentobiSgfUtil.h @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PentobiTree.cpp b/src/libpentobi_base/PentobiTree.cpp new file mode 100644 index 0000000..cd0c556 --- /dev/null +++ b/src/libpentobi_base/PentobiTree.cpp @@ -0,0 +1,344 @@ +//----------------------------------------------------------------------------- +/** @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& 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& 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(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 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 diff --git a/src/libpentobi_base/PentobiTree.h b/src/libpentobi_base/PentobiTree.h new file mode 100644 index 0000000..3589867 --- /dev/null +++ b/src/libpentobi_base/PentobiTree.h @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------------- +/** @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& root); + + void init(unique_ptr& 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 diff --git a/src/libpentobi_base/PentobiTreeWriter.cpp b/src/libpentobi_base/PentobiTreeWriter.cpp new file mode 100644 index 0000000..0e152fe --- /dev/null +++ b/src/libpentobi_base/PentobiTreeWriter.cpp @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------------- +/** @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& 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 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 diff --git a/src/libpentobi_base/PentobiTreeWriter.h b/src/libpentobi_base/PentobiTreeWriter.h new file mode 100644 index 0000000..cc09456 --- /dev/null +++ b/src/libpentobi_base/PentobiTreeWriter.h @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @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& values) override; + +private: + Variant m_variant; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PENTOBI_TREE_WRITER_H diff --git a/src/libpentobi_base/Piece.h b/src/libpentobi_base/Piece.h new file mode 100644 index 0000000..b8246cb --- /dev/null +++ b/src/libpentobi_base/Piece.h @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 diff --git a/src/libpentobi_base/PieceInfo.cpp b/src/libpentobi_base/PieceInfo.cpp new file mode 100644 index 0000000..e558e37 --- /dev/null +++ b/src/libpentobi_base/PieceInfo.cpp @@ -0,0 +1,207 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/PieceInfo.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PieceInfo.h" + +#include +#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 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(points.size()); + else if (points.size() == 1 && geometry_type == GeometryType::callisto) + m_score_points = 0; + else + m_score_points = static_cast(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 diff --git a/src/libpentobi_base/PieceInfo.h b/src/libpentobi_base/PieceInfo.h new file mode 100644 index 0000000..31f9dbd --- /dev/null +++ b/src/libpentobi_base/PieceInfo.h @@ -0,0 +1,126 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#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; + + + /** 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& + 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 m_transforms; + + map m_equivalent_transform; +}; + +//----------------------------------------------------------------------------- + +using PiecePoints = PieceInfo::Points; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PIECE_INFO_H diff --git a/src/libpentobi_base/PieceMap.h b/src/libpentobi_base/PieceMap.h new file mode 100644 index 0000000..0a8bd76 --- /dev/null +++ b/src/libpentobi_base/PieceMap.h @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include "Piece.h" + +namespace libpentobi_base { + +//----------------------------------------------------------------------------- + +/** Container mapping a unique piece to another element type. + The elements must be default-constructible. */ +template +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 m_a; +}; + +template +inline PieceMap::PieceMap(const T& val) +{ + fill(val); +} + +template +bool PieceMap::operator==(const PieceMap& piece_map) const +{ + return equal(m_a.begin(), m_a.end(), piece_map.m_a.begin()); +} + +template +inline T& PieceMap::operator[](Piece piece) +{ + LIBBOARDGAME_ASSERT(! piece.is_null()); + return m_a[piece.to_int()]; +} + +template +inline const T& PieceMap::operator[](Piece piece) const +{ + LIBBOARDGAME_ASSERT(! piece.is_null()); + return m_a[piece.to_int()]; +} + +template +void PieceMap::fill(const T& val) +{ + m_a.fill(val); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PIECE_MAP_H diff --git a/src/libpentobi_base/PieceTransforms.cpp b/src/libpentobi_base/PieceTransforms.cpp new file mode 100644 index 0000000..a675041 --- /dev/null +++ b/src/libpentobi_base/PieceTransforms.cpp @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PieceTransforms.h b/src/libpentobi_base/PieceTransforms.h new file mode 100644 index 0000000..eabc889 --- /dev/null +++ b/src/libpentobi_base/PieceTransforms.h @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------- +/** @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 +#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& 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 + const Transform* find() const; + +protected: + /** All piece transformations. + Must be initialized in constructor of subclass. */ + vector m_all; +}; + +template +const Transform* PieceTransforms::find() const +{ + for (auto t : m_all) + if (dynamic_cast(t)) + return t; + return nullptr; +} + +inline const vector& PieceTransforms::get_all() const +{ + return m_all; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_PIECE_TRANSFORMS_H diff --git a/src/libpentobi_base/PieceTransformsClassic.cpp b/src/libpentobi_base/PieceTransformsClassic.cpp new file mode 100644 index 0000000..2dee93b --- /dev/null +++ b/src/libpentobi_base/PieceTransformsClassic.cpp @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PieceTransformsClassic.h b/src/libpentobi_base/PieceTransformsClassic.h new file mode 100644 index 0000000..21d4ed2 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsClassic.h @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PieceTransformsGembloQ.cpp b/src/libpentobi_base/PieceTransformsGembloQ.cpp new file mode 100644 index 0000000..cba5fe4 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsGembloQ.cpp @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PieceTransformsGembloQ.h b/src/libpentobi_base/PieceTransformsGembloQ.h new file mode 100644 index 0000000..7e8acd0 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsGembloQ.h @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PieceTransformsTrigon.cpp b/src/libpentobi_base/PieceTransformsTrigon.cpp new file mode 100644 index 0000000..ca52519 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsTrigon.cpp @@ -0,0 +1,178 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PieceTransformsTrigon.h b/src/libpentobi_base/PieceTransformsTrigon.h new file mode 100644 index 0000000..025e395 --- /dev/null +++ b/src/libpentobi_base/PieceTransformsTrigon.h @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PlayerBase.cpp b/src/libpentobi_base/PlayerBase.cpp new file mode 100644 index 0000000..064dfaf --- /dev/null +++ b/src/libpentobi_base/PlayerBase.cpp @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PlayerBase.h b/src/libpentobi_base/PlayerBase.h new file mode 100644 index 0000000..b04e685 --- /dev/null +++ b/src/libpentobi_base/PlayerBase.h @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/Point.h b/src/libpentobi_base/Point.h new file mode 100644 index 0000000..457480d --- /dev/null +++ b/src/libpentobi_base/Point.h @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PointList.h b/src/libpentobi_base/PointList.h new file mode 100644 index 0000000..a396474 --- /dev/null +++ b/src/libpentobi_base/PointList.h @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +/** @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; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_POINT_LIST_H diff --git a/src/libpentobi_base/PointState.h b/src/libpentobi_base/PointState.h new file mode 100644 index 0000000..610c937 --- /dev/null +++ b/src/libpentobi_base/PointState.h @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/PrecompMoves.h b/src/libpentobi_base/PrecompMoves.h new file mode 100644 index 0000000..806e5d9 --- /dev/null +++ b/src/libpentobi_base/PrecompMoves.h @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------------- +/** @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; + + + /** 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, 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 m_move_lists; +}; + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_PRECOMP_MOVES_H diff --git a/src/libpentobi_base/ScoreUtil.h b/src/libpentobi_base/ScoreUtil.h new file mode 100644 index 0000000..ee30de4 --- /dev/null +++ b/src/libpentobi_base/ScoreUtil.h @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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 +void get_multiplayer_result(unsigned nu_players, + const array& points, + array& result, + bool break_ties) +{ + array 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 diff --git a/src/libpentobi_base/Setup.h b/src/libpentobi_base/Setup.h new file mode 100644 index 0000000..ec4b337 --- /dev/null +++ b/src/libpentobi_base/Setup.h @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +/** @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; + + + Color to_play = Color(0); + + ColorMap 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 diff --git a/src/libpentobi_base/StartingPoints.cpp b/src/libpentobi_base/StartingPoints.cpp new file mode 100644 index 0000000..672f0f7 --- /dev/null +++ b/src/libpentobi_base/StartingPoints.cpp @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/StartingPoints.h b/src/libpentobi_base/StartingPoints.h new file mode 100644 index 0000000..acd748f --- /dev/null +++ b/src/libpentobi_base/StartingPoints.h @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +/** @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& + get_starting_points(Color c) const; + +private: + Grid m_is_colored_starting_point; + + Grid m_is_colorless_starting_point; + + Grid m_starting_point_color; + + ColorMap> 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& + 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 diff --git a/src/libpentobi_base/SymmetricPoints.cpp b/src/libpentobi_base/SymmetricPoints.cpp new file mode 100644 index 0000000..c156942 --- /dev/null +++ b/src/libpentobi_base/SymmetricPoints.cpp @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @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 transform; + for (Point p : geo) + m_symmetric_point[p] = transform.get_transformed(p, geo); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + diff --git a/src/libpentobi_base/SymmetricPoints.h b/src/libpentobi_base/SymmetricPoints.h new file mode 100644 index 0000000..151452a --- /dev/null +++ b/src/libpentobi_base/SymmetricPoints.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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 m_symmetric_point; +}; + +inline Point SymmetricPoints::operator[](Point p) const +{ + return m_symmetric_point[p]; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base + +#endif // LIBPENTOBI_BASE_SYMMETRIC_POINTS_H diff --git a/src/libpentobi_base/TreeUtil.cpp b/src/libpentobi_base/TreeUtil.cpp new file mode 100644 index 0000000..204d878 --- /dev/null +++ b/src/libpentobi_base/TreeUtil.cpp @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/TreeUtil.h b/src/libpentobi_base/TreeUtil.h new file mode 100644 index 0000000..b1a7ea7 --- /dev/null +++ b/src/libpentobi_base/TreeUtil.h @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/TrigonGeometry.cpp b/src/libpentobi_base/TrigonGeometry.cpp new file mode 100644 index 0000000..28ba1ef --- /dev/null +++ b/src/libpentobi_base/TrigonGeometry.cpp @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonGeometry.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "TrigonGeometry.h" + +#include +#include + +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> s_geometry; + + auto pos = s_geometry.find(sz); + if (pos != s_geometry.end()) + return *pos->second; + shared_ptr 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 + diff --git a/src/libpentobi_base/TrigonGeometry.h b/src/libpentobi_base/TrigonGeometry.h new file mode 100644 index 0000000..a456d03 --- /dev/null +++ b/src/libpentobi_base/TrigonGeometry.h @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @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: + + 0 1 2 3 4 5 6 7 8 9 10 + 0 / \ / \ / \ / \ + 1 / \ / \ / \ / \ / \ + 2 / \ / \ / \ / \ / \ / \ + 3 \ / \ / \ / \ / \ / \ / + 4 \ / \ / \ / \ / \ / + 5 \ / \ / \ / \ / + + 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 diff --git a/src/libpentobi_base/TrigonTransform.cpp b/src/libpentobi_base/TrigonTransform.cpp new file mode 100644 index 0000000..0f1a9dc --- /dev/null +++ b/src/libpentobi_base/TrigonTransform.cpp @@ -0,0 +1,131 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_base/TrigonTransform.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "TrigonTransform.h" + +#include + +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(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::ceil(0.5f * px - 1.5f * py)); + auto y = static_cast(std::floor(0.5f * px + 0.5f * py)); + return {x, y}; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot120::get_transformed(CoordPoint p) const +{ + auto px = static_cast(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::ceil(-0.5f * px - 1.5f * py)); + auto y = static_cast(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(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::floor(-0.5f * px + 1.5f * py)); + auto y = static_cast(std::ceil(-0.5f * px - 0.5f * py)); + return {x, y}; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonRot300::get_transformed(CoordPoint p) const +{ + auto px = static_cast(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::floor(0.5f * px + 1.5f * py)); + auto y = static_cast(std::floor(-0.5f * px + 0.5f * py)); + return {x, y}; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot60::get_transformed(CoordPoint p) const +{ + auto px = static_cast(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::ceil(0.5f * (-px) - 1.5f * py)); + auto y = static_cast(std::floor(0.5f * (-px) + 0.5f * py)); + return {x, y}; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot120::get_transformed(CoordPoint p) const +{ + auto px = static_cast(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::ceil(-0.5f * (-px) - 1.5f * py)); + auto y = static_cast(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(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::floor(-0.5f * (-px) + 1.5f * py)); + auto y = static_cast(std::ceil(-0.5f * (-px) - 0.5f * py)); + return {x, y}; +} + +//----------------------------------------------------------------------------- + +CoordPoint TransfTrigonReflRot300::get_transformed(CoordPoint p) const +{ + auto px = static_cast(p.x); + auto py = static_cast(p.y); + auto x = static_cast(std::floor(0.5f * (-px) + 1.5f * py)); + auto y = static_cast(std::floor(-0.5f * (-px) + 0.5f * py)); + return {x, y}; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_base diff --git a/src/libpentobi_base/TrigonTransform.h b/src/libpentobi_base/TrigonTransform.h new file mode 100644 index 0000000..1ec9801 --- /dev/null +++ b/src/libpentobi_base/TrigonTransform.h @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_base/Variant.cpp b/src/libpentobi_base/Variant.cpp new file mode 100644 index 0000000..9148e7e --- /dev/null +++ b/src/libpentobi_base/Variant.cpp @@ -0,0 +1,569 @@ +//----------------------------------------------------------------------------- +/** @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::get(14, 14); + break; + case BoardType::classic: + result = &RectGeometry::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>>& transforms, + vector>>& inv_transforms) +{ + transforms.clear(); + inv_transforms.clear(); + transforms.emplace_back(new PointTransfIdent); + inv_transforms.emplace_back(new PointTransfIdent); + switch (get_board_type(variant)) + { + case BoardType::duo: + transforms.emplace_back(new PointTransfRot270Refl); + inv_transforms.emplace_back(new PointTransfRot270Refl); + break; + case BoardType::trigon: + transforms.emplace_back(new PointTransfTrigonRot60); + inv_transforms.emplace_back(new PointTransfTrigonRot300); + transforms.emplace_back(new PointTransfTrigonRot120); + inv_transforms.emplace_back(new PointTransfTrigonRot240); + transforms.emplace_back(new PointTransfRot180); + inv_transforms.emplace_back(new PointTransfRot180); + transforms.emplace_back(new PointTransfTrigonRot240); + inv_transforms.emplace_back(new PointTransfTrigonRot120); + transforms.emplace_back(new PointTransfTrigonRot300); + inv_transforms.emplace_back(new PointTransfTrigonRot60); + transforms.emplace_back(new PointTransfRefl); + inv_transforms.emplace_back(new PointTransfRefl); + transforms.emplace_back(new PointTransfTrigonReflRot60); + inv_transforms.emplace_back(new PointTransfTrigonReflRot60); + transforms.emplace_back(new PointTransfTrigonReflRot120); + inv_transforms.emplace_back(new PointTransfTrigonReflRot120); + transforms.emplace_back(new PointTransfReflRot180); + inv_transforms.emplace_back(new PointTransfReflRot180); + transforms.emplace_back(new PointTransfTrigonReflRot240); + inv_transforms.emplace_back(new PointTransfTrigonReflRot240); + transforms.emplace_back(new PointTransfTrigonReflRot300); + inv_transforms.emplace_back(new PointTransfTrigonReflRot300); + break; + case BoardType::callisto_2: + case BoardType::callisto: + case BoardType::callisto_3: + transforms.emplace_back(new PointTransfRot90); + inv_transforms.emplace_back(new PointTransfRot270); + transforms.emplace_back(new PointTransfRot180); + inv_transforms.emplace_back(new PointTransfRot180); + transforms.emplace_back(new PointTransfRot270); + inv_transforms.emplace_back(new PointTransfRot90); + transforms.emplace_back(new PointTransfRefl); + inv_transforms.emplace_back(new PointTransfRefl); + transforms.emplace_back(new PointTransfReflRot180); + inv_transforms.emplace_back(new PointTransfReflRot180); + transforms.emplace_back(new PointTransfRot90Refl); + inv_transforms.emplace_back(new PointTransfRot90Refl); + transforms.emplace_back(new PointTransfRot270Refl); + inv_transforms.emplace_back(new PointTransfRot270Refl); + 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 diff --git a/src/libpentobi_base/Variant.h b/src/libpentobi_base/Variant.h new file mode 100644 index 0000000..03181f8 --- /dev/null +++ b/src/libpentobi_base/Variant.h @@ -0,0 +1,187 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#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>>& transforms, + vector>>& 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 diff --git a/src/libpentobi_kde_thumbnailer/CMakeLists.txt b/src/libpentobi_kde_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..f119061 --- /dev/null +++ b/src/libpentobi_kde_thumbnailer/CMakeLists.txt @@ -0,0 +1,45 @@ +# 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) diff --git a/src/libpentobi_mcts/AnalyzeGame.cpp b/src/libpentobi_mcts/AnalyzeGame.cpp new file mode 100644 index 0000000..df5e322 --- /dev/null +++ b/src/libpentobi_mcts/AnalyzeGame.cpp @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------------- +/** @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& progress_callback) +{ + m_variant = game.get_variant(); + m_moves.clear(); + m_values.clear(); + auto& tree = game.get_tree(); + unique_ptr 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(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( + 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( + search.get_root_val().get_mean())); + } + node = node->get_first_child_or_null(); + } + while (node != nullptr); +} + +void AnalyzeGame::set(Variant variant, const vector& moves, + const vector& values) +{ + LIBBOARDGAME_ASSERT(moves.size() == values.size()); + m_variant = variant; + m_moves = moves; + m_values = values; +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/AnalyzeGame.h b/src/libpentobi_mcts/AnalyzeGame.h new file mode 100644 index 0000000..0cb4cdd --- /dev/null +++ b/src/libpentobi_mcts/AnalyzeGame.h @@ -0,0 +1,88 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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& 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& moves, + const vector& values); +private: + Variant m_variant; + + vector m_moves; + + vector 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(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 diff --git a/src/libpentobi_mcts/CMakeLists.txt b/src/libpentobi_mcts/CMakeLists.txt new file mode 100644 index 0000000..05020a8 --- /dev/null +++ b/src/libpentobi_mcts/CMakeLists.txt @@ -0,0 +1,37 @@ +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) diff --git a/src/libpentobi_mcts/Float.h b/src/libpentobi_mcts/Float.h new file mode 100644 index 0000000..9379459 --- /dev/null +++ b/src/libpentobi_mcts/Float.h @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +/** @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 + +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::value, ""); + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts + +#endif // LIBPENTOBI_MCTS_FLOAT_H diff --git a/src/libpentobi_mcts/History.cpp b/src/libpentobi_mcts/History.cpp new file mode 100644 index 0000000..1670fc2 --- /dev/null +++ b/src/libpentobi_mcts/History.cpp @@ -0,0 +1,73 @@ +//---------------------------------------------------------------------------- +/** @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(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& 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 diff --git a/src/libpentobi_mcts/History.h b/src/libpentobi_mcts/History.h new file mode 100644 index 0000000..83f14c3 --- /dev/null +++ b/src/libpentobi_mcts/History.h @@ -0,0 +1,105 @@ +//---------------------------------------------------------------------------- +/** @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& 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 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 diff --git a/src/libpentobi_mcts/LocalPoints.cpp b/src/libpentobi_mcts/LocalPoints.cpp new file mode 100644 index 0000000..d67e73b --- /dev/null +++ b/src/libpentobi_mcts/LocalPoints.cpp @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_mcts/LocalPoints.h b/src/libpentobi_mcts/LocalPoints.h new file mode 100644 index 0000000..51d2e31 --- /dev/null +++ b/src/libpentobi_mcts/LocalPoints.h @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------- +/** @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 + void init(const Board& bd); + + bool contains(Point p) const { return m_is_local[p]; } + +private: + GridExt m_is_local; + + /** Points in m_is_local with value true. */ + PointList m_points; +}; + +template +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( + 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 diff --git a/src/libpentobi_mcts/Player.cpp b/src/libpentobi_mcts/Player.cpp new file mode 100644 index 0000000..1235a64 --- /dev/null +++ b/src/libpentobi_mcts/Player.cpp @@ -0,0 +1,369 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Player.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Player.h" + +#include +#include +#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(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(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(); + else + m_time_source = make_unique(); +} + +//----------------------------------------------------------------------------- + +} // namespace libpentobi_mcts diff --git a/src/libpentobi_mcts/Player.h b/src/libpentobi_mcts/Player.h new file mode 100644 index 0000000..74641d1 --- /dev/null +++ b/src/libpentobi_mcts/Player.h @@ -0,0 +1,192 @@ +//----------------------------------------------------------------------------- +/** @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 m_weight_max_count_classic; + + array m_weight_max_count_trigon; + + array m_weight_max_count_duo; + + array m_weight_max_count_callisto; + + array 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 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 diff --git a/src/libpentobi_mcts/PlayoutFeatures.h b/src/libpentobi_mcts/PlayoutFeatures.h new file mode 100644 index 0000000..280708b --- /dev/null +++ b/src/libpentobi_mcts/PlayoutFeatures.h @@ -0,0 +1,228 @@ +//----------------------------------------------------------------------------- +/** @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 + void set_forbidden(const MoveInfo& info); + + /** Set adjacent points of move to forbidden. */ + template + void set_forbidden(const MoveInfoExt& info_ext); + + template + void set_local(const Board& bd); + +private: + GridExt m_point_value; + + Grid 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(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 +inline void PlayoutFeatures::set_forbidden(const MoveInfo& 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 +inline void PlayoutFeatures::set_forbidden( + const MoveInfoExt& info_ext) +{ + for (auto i = info_ext.begin_adj(), end = info_ext.end_adj(); i != end; + ++i) + m_point_value[*i] = 0x01000u; +} + +template +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( + 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 diff --git a/src/libpentobi_mcts/PriorKnowledge.cpp b/src/libpentobi_mcts/PriorKnowledge.cpp new file mode 100644 index 0000000..5f27191 --- /dev/null +++ b/src/libpentobi_mcts/PriorKnowledge.cpp @@ -0,0 +1,418 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/PriorKnowledge.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PriorKnowledge.h" + +#include + +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(geo.get_width()); + auto height = static_cast(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(geo.get_x(p)); + auto y = static_cast(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::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( + 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 diff --git a/src/libpentobi_mcts/PriorKnowledge.h b/src/libpentobi_mcts/PriorKnowledge.h new file mode 100644 index 0000000..b3c567b --- /dev/null +++ b/src/libpentobi_mcts/PriorKnowledge.h @@ -0,0 +1,440 @@ +//----------------------------------------------------------------------------- +/** @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; + + using Tree = libboardgame_mcts::Tree; + + + 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 + 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 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 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 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 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 m_dist_to_center; + + + template + void compute_features(const Board& bd, const MoveList& moves, + bool check_dist_to_center, bool check_connect); + + void init_variant(const Board& bd); +}; + + +template +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 gamma_point; + gamma_point[Point::null()] = 1; + Grid gamma_attach; + Grid 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(is_forbidden[pa]); + } + else + for (auto pa : geo.get_adj(p)) + n += 1u - static_cast(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::max(); + m_sum_gamma = 0; + m_min_dist_to_center = numeric_limits::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( + mv, move_info_ext_array); + auto& features = m_features[i]; + auto& info = BoardConst::get_move_info(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 +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(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( + 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(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 diff --git a/src/libpentobi_mcts/Search.cpp b/src/libpentobi_mcts/Search.cpp new file mode 100644 index 0000000..ab0c54c --- /dev/null +++ b/src/libpentobi_mcts/Search.cpp @@ -0,0 +1,170 @@ +//----------------------------------------------------------------------------- +/** @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& 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 Search::create_state() +{ + return make_unique(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 diff --git a/src/libpentobi_mcts/Search.h b/src/libpentobi_mcts/Search.h new file mode 100644 index 0000000..5913615 --- /dev/null +++ b/src/libpentobi_mcts/Search.h @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------- +/** @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 +{ +public: + Search(Variant initial_variant, unsigned nu_threads, size_t memory); + + + unique_ptr create_state() override; + + PlayerInt get_nu_players() const override; + + PlayerInt get_player() const override; + + bool check_followup(ArrayList& 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(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 diff --git a/src/libpentobi_mcts/SearchParamConst.h b/src/libpentobi_mcts/SearchParamConst.h new file mode 100644 index 0000000..a230a25 --- /dev/null +++ b/src/libpentobi_mcts/SearchParamConst.h @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_mcts/SharedConst.cpp b/src/libpentobi_mcts/SharedConst.cpp new file mode 100644 index 0000000..c42c31e --- /dev/null +++ b/src/libpentobi_mcts/SharedConst.cpp @@ -0,0 +1,369 @@ +//----------------------------------------------------------------------------- +/** @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& 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& 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& 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 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 diff --git a/src/libpentobi_mcts/SharedConst.h b/src/libpentobi_mcts/SharedConst.h new file mode 100644 index 0000000..196fe7c --- /dev/null +++ b/src/libpentobi_mcts/SharedConst.h @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +/** @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 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*, Board::max_moves> is_piece_considered; + + /** List of unique values for is_piece_considered. */ + ArrayList, 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 is_piece_considered_all; + + PieceMap 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 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 diff --git a/src/libpentobi_mcts/State.cpp b/src/libpentobi_mcts/State.cpp new file mode 100644 index 0000000..f4fcd3c --- /dev/null +++ b/src/libpentobi_mcts/State.cpp @@ -0,0 +1,925 @@ +//----------------------------------------------------------------------------- +/** @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 +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( + mv, get_move_info(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 +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(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 +bool State::check_forbidden(const GridExt& is_forbidden, Move mv, + MoveList& moves, unsigned& nu_moves) +{ + auto p = get_move_info(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(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 +bool State::check_move(Move mv, const MoveInfo& 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 +inline bool State::check_move(Move mv, const MoveInfo& info, + MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma) +{ + return check_move( + 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& 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& result) +{ + auto nu_players = m_bd.get_nu_players(); + LIBBOARDGAME_ASSERT(nu_players > 2); + array points; + for (Color::IntType i = 0; i < nu_players; ++i) + points[i] = m_bd.get_points(Color(i)); + array 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& 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(geo.get_x(p)); + auto py = static_cast(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(geo.get_x(pp)); + auto ppy = static_cast(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& 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(get_player(), + moves[static_cast(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& 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 +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(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(m_bd.get_attach_points(Color(0)).size()) + - static_cast(m_bd.get_attach_points(Color(1)).size()); + for (Point p : m_bd.get_attach_points(Color(0))) + n -= static_cast(m_bd.is_forbidden(p, Color(0))); + for (Point p : m_bd.get_attach_points(Color(1))) + n += static_cast(m_bd.is_forbidden(p, Color(1))); + auto attach = static_cast(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(m_bd.get_attach_points(Color(0)).size()) + + static_cast(m_bd.get_attach_points(Color(2)).size()) + - static_cast(m_bd.get_attach_points(Color(1)).size()) + - static_cast(m_bd.get_attach_points(Color(3)).size()); + for (Point p : m_bd.get_attach_points(Color(0))) + n -= static_cast(m_bd.is_forbidden(p, Color(0))); + for (Point p : m_bd.get_attach_points(Color(2))) + n -= static_cast(m_bd.is_forbidden(p, Color(2))); + for (Point p : m_bd.get_attach_points(Color(1))) + n += static_cast(m_bd.is_forbidden(p, Color(1))); + for (Point p : m_bd.get_attach_points(Color(3))) + n += static_cast(m_bd.is_forbidden(p, Color(3))); + auto attach = static_cast(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(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 +void State::init_moves_with_gamma(Color c) +{ + m_is_piece_considered[c] = &get_is_piece_considered(c); + m_playout_features[c] + .set_local(m_bd); + auto& marker = m_marker[c]; + auto& moves = m_moves[c]; + marker.clear(moves); + auto& pieces = get_pieces_considered(c); + if (m_bd.is_first_piece(c) && ! IS_CALLISTO) + add_starting_moves(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(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(c); + } +} + +template +void State::init_moves_without_gamma(Color c) +{ + m_is_piece_considered[c] = &get_is_piece_considered(c); + auto& marker = m_marker[c]; + auto& moves = m_moves[c]; + marker.clear(moves); + auto& pieces = get_pieces_considered(c); + auto& is_forbidden = m_bd.is_forbidden(c); + if (m_bd.is_first_piece(c) && ! IS_CALLISTO) + add_starting_moves(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( + 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(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 +void State::update_moves(Color c) +{ + auto& playout_features = m_playout_features[c]; + playout_features.set_local(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(m_last_move[c]).get_piece()))) + for (Move mv : moves) + { + auto& info = get_move_info(mv); + if (info.get_piece() == piece + || ! check_move( + mv, info, moves, nu_moves, playout_features, + total_gamma)) + marker.clear(mv); + } + else + for (Move mv : moves) + { + auto& info = get_move_info(mv); + if (! m_bd.is_piece_left(c, info.get_piece()) + || ! check_move( + mv, info, moves, nu_moves, playout_features, + total_gamma)) + marker.clear(mv); + } + + // Find new legal moves because of new pieces played by this color + auto& pieces = get_pieces_considered(c); + auto& attach_points = m_bd.get_attach_points(c); + auto begin = m_last_attach_points_end[c]; + auto end = attach_points.end(); + for (auto i = begin; i != end; ++i) + if (! is_forbidden[*i] && ! m_moves_added_at[c][*i]) + { + m_moves_added_at[c][*i] = true; + add_moves(*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( + 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 diff --git a/src/libpentobi_mcts/State.h b/src/libpentobi_mcts/State.h new file mode 100644 index 0000000..60a2a8d --- /dev/null +++ b/src/libpentobi_mcts/State.h @@ -0,0 +1,543 @@ +//----------------------------------------------------------------------------- +/** @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; + + using Tree = libboardgame_mcts::Tree; + + using LastGoodReply = + libboardgame_mcts::LastGoodReply; + + + /** Constructor. + @param initial_variant Game variant to initialize the internal + board with (may avoid unnecessary BoardConst creation for game variant + that is never used) + @param shared_const (@ref libboardgame_doc_storesref) */ + State(Variant initial_variant, const SharedConst& shared_const); + + State& operator=(const State&) = delete; + + /** Play a move in the in-tree phase of the search. */ + void play_in_tree(Move mv); + + /** Handle end of in-tree phase. */ + void finish_in_tree(); + + /** Play a move right after expanding a node. */ + void play_expanded_child(Move mv); + + /** 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& mv); + + void evaluate_playout(array& 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 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 m_moves; + + ColorMap*> 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 m_gamma_local; + + /** Gamma value for a piece. */ + PieceMap m_gamma_piece; + + /** Number of moves played by a color since the last update of its move + list. */ + ColorMap m_nu_new_moves; + + /** Board::get_attach_points().end() for a color at the last update of + its move list. */ + ColorMap m_last_attach_points_end; + + /** Last move played by a color since the last update of its move list. */ + ColorMap m_last_move; + + ColorMap m_is_move_list_initialized; + + ColorMap m_has_moves; + + /** Marks moves contained in m_moves. */ + ColorMap m_marker; + + ColorMap m_playout_features; + + RandomGenerator m_random; + + /** Used in get_quality_bonus(). */ + ColorMap> m_stat_score; + + /** Used in get_quality_bonus(). */ + Statistics m_stat_len; + + /** Used in get_quality_bonus(). */ + Statistics 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> m_moves_added_at; + + + template + void add_moves(Point p, Color c, const Board::PiecesLeftList& pieces, + float& total_gamma, MoveList& moves, unsigned& nu_moves); + + template + 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& result); + + void evaluate_multiplayer(array& result); + + void evaluate_twocolor(array& 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 + const MoveInfo& get_move_info(Move mv) const; + + template + const MoveInfoExt& 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& get_is_piece_considered(Color c) const; + + template + const Board::PiecesLeftList& get_pieces_considered(Color c); + + void init_gamma(); + + template + void init_moves_with_gamma(Color c); + + template + void init_moves_without_gamma(Color c); + + template + bool check_forbidden(const GridExt& is_forbidden, Move mv, + MoveList& moves, unsigned& nu_moves); + + bool check_lgr(Move mv) const; + + template + bool check_move(Move mv, const MoveInfo& info, float gamma_piece, + MoveList& moves, unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma); + + template + bool check_move(Move mv, const MoveInfo& info, MoveList& moves, + unsigned& nu_moves, + const PlayoutFeatures& playout_features, + float& total_gamma); + + bool gen_playout_move_full(PlayerMove& mv); + + template + void update_moves(Color c); + + template + void update_playout_features(Color c, Move mv); + + template + 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(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& 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& 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(player, lgr2); + return true; + } + Move lgr1 = lgr.get_lgr1(player, last); + if (check_lgr(lgr1)) + { + mv = PlayerMove(player, lgr1); + return true; + } + return gen_playout_move_full(mv); +} + +template +inline const MoveInfo& State::get_move_info(Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_range()); + return BoardConst::get_move_info(mv, m_move_info_array); +} + +template +inline const MoveInfoExt& State::get_move_info_ext( + Move mv) const +{ + LIBBOARDGAME_ASSERT(mv.to_int() < m_bc->get_range()); + return BoardConst::get_move_info_ext( + 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(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 +inline void State::update_playout_features(Color c, Move mv) +{ + auto& info = get_move_info(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(mv).size_adj_points == 0); + else + m_playout_features[c].set_forbidden( + get_move_info_ext(mv)); +} + +template +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(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 diff --git a/src/libpentobi_mcts/StateUtil.cpp b/src/libpentobi_mcts/StateUtil.cpp new file mode 100644 index 0000000..a7c8173 --- /dev/null +++ b/src/libpentobi_mcts/StateUtil.cpp @@ -0,0 +1,95 @@ +//----------------------------------------------------------------------------- +/** @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 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(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 diff --git a/src/libpentobi_mcts/StateUtil.h b/src/libpentobi_mcts/StateUtil.h new file mode 100644 index 0000000..9431948 --- /dev/null +++ b/src/libpentobi_mcts/StateUtil.h @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_mcts/Util.cpp b/src/libpentobi_mcts/Util.cpp new file mode 100644 index 0000000..3ca8d2d --- /dev/null +++ b/src/libpentobi_mcts/Util.cpp @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_mcts/Util.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Util.h" + +#include +#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 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 diff --git a/src/libpentobi_mcts/Util.h b/src/libpentobi_mcts/Util.h new file mode 100644 index 0000000..0a85243 --- /dev/null +++ b/src/libpentobi_mcts/Util.h @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/libpentobi_paint/CMakeLists.txt b/src/libpentobi_paint/CMakeLists.txt new file mode 100644 index 0000000..dd1724f --- /dev/null +++ b/src/libpentobi_paint/CMakeLists.txt @@ -0,0 +1,12 @@ +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) diff --git a/src/libpentobi_paint/Paint.cpp b/src/libpentobi_paint/Paint.cpp new file mode 100644 index 0000000..0d608ca --- /dev/null +++ b/src/libpentobi_paint/Paint.cpp @@ -0,0 +1,900 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_paint/Paint.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Paint.h" + +#include +#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, const Grid& pieceId, + const ColorMap& base, const ColorMap& light, + const ColorMap& 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, const ColorMap& base, + const ColorMap& light, const ColorMap& 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, const ColorMap& base, + const ColorMap& light, const ColorMap& 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, + const Grid& pieceId, Point p, qreal gridWidth, + qreal gridHeight, const ColorMap& base) +{ + auto x = geo.get_x(p); + auto y = geo.get_y(p); + ArrayList 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, const Grid& pieceId, + const ColorMap& base, const ColorMap& light, + const ColorMap& 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, const ColorMap& base, + const ColorMap& light, const ColorMap& 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, + const Grid& 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 blue{ { + QColor(0, 115, 207), QColor(20, 153, 255), QColor(0, 72, 129)} }; + array green{ { + QColor(0, 192, 0), QColor(0, 250, 0), QColor(0, 120, 0)} }; + array orange{ { + QColor(240, 146, 23), QColor(255, 187, 103), QColor(157, 94, 11)} }; + array purple{ { + QColor(161, 44, 207), QColor(190, 112, 220), QColor(109, 39, 135)} }; + array red{ { + QColor(230, 62, 44), QColor(255, 101, 90), QColor(144, 38, 27)} }; + array yellow{ { + QColor(245, 195, 32), QColor(255, 219, 88), QColor(170, 133, 22)} }; + ColorMap piecesBase; + ColorMap piecesLight; + ColorMap 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 diff --git a/src/libpentobi_paint/Paint.h b/src/libpentobi_paint/Paint.h new file mode 100644 index 0000000..2a5c339 --- /dev/null +++ b/src/libpentobi_paint/Paint.h @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 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, + const Grid& 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 diff --git a/src/libpentobi_thumbnail/CMakeLists.txt b/src/libpentobi_thumbnail/CMakeLists.txt new file mode 100644 index 0000000..cf3433d --- /dev/null +++ b/src/libpentobi_thumbnail/CMakeLists.txt @@ -0,0 +1,12 @@ +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) diff --git a/src/libpentobi_thumbnail/CreateThumbnail.cpp b/src/libpentobi_thumbnail/CreateThumbnail.cpp new file mode 100644 index 0000000..1e4ba95 --- /dev/null +++ b/src/libpentobi_thumbnail/CreateThumbnail.cpp @@ -0,0 +1,169 @@ +//----------------------------------------------------------------------------- +/** @file libpentobi_thumbnail/CreateThumbnail.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "CreateThumbnail.h" + +#include +#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, + Grid& 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, Grid& 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, + Grid& 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; + Grid 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(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; +} + +//----------------------------------------------------------------------------- diff --git a/src/libpentobi_thumbnail/CreateThumbnail.h b/src/libpentobi_thumbnail/CreateThumbnail.h new file mode 100644 index 0000000..f2b1e50 --- /dev/null +++ b/src/libpentobi_thumbnail/CreateThumbnail.h @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +/** @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 diff --git a/src/pentobi/AnalyzeGameModel.cpp b/src/pentobi/AnalyzeGameModel.cpp new file mode 100644 index 0000000..63bc065 --- /dev/null +++ b/src/pentobi/AnalyzeGameModel.cpp @@ -0,0 +1,275 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AnalyzeGameModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "AnalyzeGameModel.h" + +#include +#include +#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::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 AnalyzeGameModel::elements() +{ + return {this, m_elements}; +} + +void AnalyzeGameModel::gotoMove(GameModel* gameModel, int moveNumber) +{ + if (moveNumber < 0) + return; + auto n = static_cast(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(); + 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 moves; + vector 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)), 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 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(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(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(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AnalyzeGameModel.h b/src/pentobi/AnalyzeGameModel.h new file mode 100644 index 0000000..2a67fb6 --- /dev/null +++ b/src/pentobi/AnalyzeGameModel.h @@ -0,0 +1,116 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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 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 elements(); + +signals: + void isRunningChanged(); + + void markMoveNumberChanged(); + + void progressChanged(); + + void elementsChanged(); + +private: + bool m_isRunning = false; + + int m_markMoveNumber = -1; + + size_t m_nuSimulations; + + QList m_elements; + + QFutureWatcher 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 diff --git a/src/pentobi/AndroidUtils.cpp b/src/pentobi/AndroidUtils.cpp new file mode 100644 index 0000000..3bd26a4 --- /dev/null +++ b/src/pentobi/AndroidUtils.cpp @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/AndroidUtils.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "AndroidUtils.h" + +#include +#include + +#ifdef Q_OS_ANDROID +#include +#include +#include +#include +#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("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( + "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()); + 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(), uri.object()); + if (! intent.isValid()) + return; + QtAndroid::androidActivity().callMethod( + "sendBroadcast", "(Landroid/content/Intent;)V", + intent.object()); +#else + Q_UNUSED(pathname); +#endif +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/AndroidUtils.h b/src/pentobi/AndroidUtils.h new file mode 100644 index 0000000..d532522 --- /dev/null +++ b/src/pentobi/AndroidUtils.h @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +//----------------------------------------------------------------------------- + +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 diff --git a/src/pentobi/CMakeLists.txt b/src/pentobi/CMakeLists.txt new file mode 100644 index 0000000..d31929c --- /dev/null +++ b/src/pentobi/CMakeLists.txt @@ -0,0 +1,85 @@ +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}) diff --git a/src/pentobi/GameModel.cpp b/src/pentobi/GameModel.cpp new file mode 100644 index 0000000..1c70b52 --- /dev/null +++ b/src/pentobi/GameModel.cpp @@ -0,0 +1,1930 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/GameModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "GameModel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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(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(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(round(dx * qreal(0.5))) * 2; + else + offX = static_cast(round((dx - 1) * qreal(0.5))) * 2 + 1; + } + else + offX = static_cast(round(dx)); + auto offY = static_cast(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(variant); + if (pieceModel->getPiece() == piece && ! pieceModel->isPlayed()) + return pieceModel; + } + return nullptr; +} + +QVariantList GameModel::getPieceModels(int color) +{ + if (color >= 0 && color <= static_cast(Color::range)) + return m_pieceModels[Color(static_cast(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(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(pos.x()), + static_cast(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(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(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(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(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(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(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(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 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( + 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(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(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(); + if (m_legalMoves->empty()) + { + if (! m_marker) + m_marker = make_unique(); + 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(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(currentPickedPiece->getPiece().to_int()); + else + i = 0; + while (true) + { + if (i == 0) + i = static_cast(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(); + 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 +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(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& 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(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(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> 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 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(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(get_move_number(tree, current)), + &GameModel::moveNumberChanged); + set(m_movesLeft, static_cast(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(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/GameModel.h b/src/pentobi/GameModel.h new file mode 100644 index 0000000..388f9bb --- /dev/null +++ b/src/pentobi/GameModel.h @@ -0,0 +1,695 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#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(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 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 m_legalMoves; + + unsigned m_legalMoveIndex; + + /** Local variable reused for efficiency. */ + unique_ptr 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 + 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& isPlayed); + + void updatePieces(); + + void updatePositionInfo(); + + void updateProperties(); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_GAME_MODEL_H diff --git a/src/pentobi/ImageProvider.cpp b/src/pentobi/ImageProvider.cpp new file mode 100644 index 0000000..74e85d3 --- /dev/null +++ b/src/pentobi/ImageProvider.cpp @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/ImageProvider.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "ImageProvider.h" + +#include +#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; +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/ImageProvider.h b/src/pentobi/ImageProvider.h new file mode 100644 index 0000000..c76b377 --- /dev/null +++ b/src/pentobi/ImageProvider.h @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +/** @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 + +//----------------------------------------------------------------------------- + +class ImageProvider + : public QQuickImageProvider +{ +public: + ImageProvider(); + + QPixmap requestPixmap(const QString& id, QSize* size, + const QSize& requestedSize) override; +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_IMAGE_PROVIDER_H diff --git a/src/pentobi/Main.cpp b/src/pentobi/Main.cpp new file mode 100644 index 0000000..a9429cd --- /dev/null +++ b/src/pentobi/Main.cpp @@ -0,0 +1,225 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#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 +#endif + +#ifndef PENTOBI_OPEN_HELP_EXTERNALLY +#include +#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 ."), + 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 ."), + QStringLiteral("n")); + parser.addOption(optionSeed); + QCommandLineOption optionThreads( + QStringLiteral("threads"), + QStringLiteral("Use 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("pentobi", 1, 0, "AnalyzeGameModel"); + qmlRegisterType("pentobi", 1, 0, "AndroidUtils"); + qmlRegisterType("pentobi", 1, 0, "GameModel"); + qmlRegisterType("pentobi", 1, 0, "PlayerModel"); + qmlRegisterType("pentobi", 1, 0, "RatingModel"); + qmlRegisterType("pentobi", 1, 0, "SyncSettings"); + qmlRegisterInterface("AnalyzeGameElement"); + qmlRegisterInterface("GameModelMove"); + qmlRegisterInterface("PieceModel"); + QTranslator translator; + translator.load(":qml/i18n/qml_" + QLocale::system().name()); + QCoreApplication::installTranslator(&translator); +#ifdef Q_OS_ANDROID + return mainAndroid(); +#else + return mainDesktop(); +#endif +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/Pentobi.pro b/src/pentobi/Pentobi.pro new file mode 100644 index 0000000..2529463 --- /dev/null +++ b/src/pentobi/Pentobi.pro @@ -0,0 +1,263 @@ +############################################################################# +# 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 diff --git a/src/pentobi/PieceModel.cpp b/src/pentobi/PieceModel.cpp new file mode 100644 index 0000000..67519a0 --- /dev/null +++ b/src/pentobi/PieceModel.cpp @@ -0,0 +1,425 @@ +//----------------------------------------------------------------------------- +/** @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(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 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(); + if (state == QStringLiteral("rot120")) + return transforms.find(); + if (state == QStringLiteral("rot180")) + return transforms.find(); + if (state == QStringLiteral("rot240")) + return transforms.find(); + if (state == QStringLiteral("rot300")) + return transforms.find(); + if (state == QStringLiteral("flip")) + return transforms.find(); + if (state == QStringLiteral("rot60Flip")) + return transforms.find(); + if (state == QStringLiteral("rot120Flip")) + return transforms.find(); + if (state == QStringLiteral("rot180Flip")) + return transforms.find(); + if (state == QStringLiteral("rot240Flip")) + return transforms.find(); + if (state == QStringLiteral("rot300Flip")) + return transforms.find(); + return transforms.find(); + } + if (pieceSet == PieceSet::gembloq) + { + if (state == QStringLiteral("rot90")) + return transforms.find(); + if (state == QStringLiteral("rot180")) + return transforms.find(); + if (state == QStringLiteral("rot270")) + return transforms.find(); + if (state == QStringLiteral("flip")) + return transforms.find(); + if (state == QStringLiteral("rot90Flip")) + return transforms.find(); + if (state == QStringLiteral("rot180Flip")) + return transforms.find(); + if (state == QStringLiteral("rot270Flip")) + return transforms.find(); + return transforms.find(); + } + if (state == QStringLiteral("rot90")) + return transforms.find(); + if (state == QStringLiteral("rot180")) + return transforms.find(); + if (state == QStringLiteral("rot270")) + return transforms.find(); + if (state == QStringLiteral("flip")) + return transforms.find(); + if (state == QStringLiteral("rot90Flip")) + return transforms.find(); + if (state == QStringLiteral("rot180Flip")) + return transforms.find(); + if (state == QStringLiteral("rot270Flip")) + return transforms.find(); + return transforms.find(); +} + +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(1) / 3 : static_cast(2) / 3; + } + else if (isGembloQ) + { + centerX = (pointType == 1 || pointType == 3) ? + static_cast(1) / 3 : static_cast(2) / 3; + centerY = (pointType == 0 || pointType == 3) ? + static_cast(1) / 3 : static_cast(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(transform)) + state = QStringLiteral("rot60"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot120"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot180"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot240"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot300"); + else if (dynamic_cast(transform)) + state = QStringLiteral("flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot60Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot120Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot180Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot240Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot300Flip"); + } + else if (pieceSet == PieceSet::gembloq) + { + if (dynamic_cast(transform)) + state = QStringLiteral("rot90"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot180"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot270"); + else if (dynamic_cast(transform)) + state = QStringLiteral("flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot90Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot180Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot270Flip"); + } + else + { + if (dynamic_cast(transform)) + state = QStringLiteral("rot90"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot180"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot270"); + else if (dynamic_cast(transform)) + state = QStringLiteral("flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot90Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot180Flip"); + else if (dynamic_cast(transform)) + state = QStringLiteral("rot270Flip"); + } + if (m_state == state) + return; + m_state = state; + emit stateChanged(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/PieceModel.h b/src/pentobi/PieceModel.h new file mode 100644 index 0000000..0a2064b --- /dev/null +++ b/src/pentobi/PieceModel.h @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#include +#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 diff --git a/src/pentobi/PlayerModel.cpp b/src/pentobi/PlayerModel.cpp new file mode 100644 index 0000000..8ee5580 --- /dev/null +++ b/src/pentobi/PlayerModel.cpp @@ -0,0 +1,182 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/PlayerModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PlayerModel.h" + +#include +#include +#include +#include +#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(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::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(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 future = + QtConcurrent::run(this, &PlayerModel::asyncGenMove, gameModel, + bd.get_effective_to_play(), m_genMoveId); + m_watcher.setFuture(future); + setIsGenMoveRunning(true); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/PlayerModel.h b/src/pentobi/PlayerModel.h new file mode 100644 index 0000000..382dbc5 --- /dev/null +++ b/src/pentobi/PlayerModel.h @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 m_player; + + QFutureWatcher 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 diff --git a/src/pentobi/RatingModel.cpp b/src/pentobi/RatingModel.cpp new file mode 100644 index 0000000..7a8ecb0 --- /dev/null +++ b/src/pentobi/RatingModel.cpp @@ -0,0 +1,297 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/RatingModel.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "RatingModel.h" + +#include +#include +#include +#include +#include +#include +#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)), + 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(get_nu_players(variant) - 1); + Rating opponentRating = + Player::get_rating(variant, static_cast(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(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(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 newHistory; + newHistory.reserve(m_history.size()); + for (auto& i : m_history) + { + auto& info = dynamic_cast(*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(*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(*o1).number() + > dynamic_cast(*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(); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi/RatingModel.h b/src/pentobi/RatingModel.h new file mode 100644 index 0000000..d06708b --- /dev/null +++ b/src/pentobi/RatingModel.h @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 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& 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 m_history; + + + QString getDir() const; + + void saveSettings(); + + void setBestRating(double rating); + + void setRating(double rating); + + void setNumberGames(int numberGames); +}; + +//----------------------------------------------------------------------------- + +#endif // PENTOBI_RATING_MODEL_H diff --git a/src/pentobi/SyncSettings.h b/src/pentobi/SyncSettings.h new file mode 100644 index 0000000..d18240d --- /dev/null +++ b/src/pentobi/SyncSettings.h @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +/** @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 + +//----------------------------------------------------------------------------- + +/** 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 diff --git a/src/pentobi/android/AndroidManifest.xml b/src/pentobi/android/AndroidManifest.xml new file mode 100644 index 0000000..0dd3ca5 --- /dev/null +++ b/src/pentobi/android/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/android/res/drawable-hdpi/icon.png b/src/pentobi/android/res/drawable-hdpi/icon.png new file mode 100644 index 0000000..aa2b8f5 Binary files /dev/null and b/src/pentobi/android/res/drawable-hdpi/icon.png differ diff --git a/src/pentobi/android/res/drawable-mdpi/icon.png b/src/pentobi/android/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..9a93a86 Binary files /dev/null and b/src/pentobi/android/res/drawable-mdpi/icon.png differ diff --git a/src/pentobi/android/res/drawable/splash.xml b/src/pentobi/android/res/drawable/splash.xml new file mode 100644 index 0000000..32f67f9 --- /dev/null +++ b/src/pentobi/android/res/drawable/splash.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/pentobi/android/res/values/theme.xml b/src/pentobi/android/res/values/theme.xml new file mode 100644 index 0000000..adec232 --- /dev/null +++ b/src/pentobi/android/res/values/theme.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/pentobi/android_icons_svg/icon48.svg b/src/pentobi/android_icons_svg/icon48.svg new file mode 100644 index 0000000..7c054c0 --- /dev/null +++ b/src/pentobi/android_icons_svg/icon48.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/android_icons_svg/icon72.svg b/src/pentobi/android_icons_svg/icon72.svg new file mode 100644 index 0000000..2463d0f --- /dev/null +++ b/src/pentobi/android_icons_svg/icon72.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/AboutDialog.qml b/src/pentobi/qml/AboutDialog.qml new file mode 100644 index 0000000..d794233 --- /dev/null +++ b/src/pentobi/qml/AboutDialog.qml @@ -0,0 +1,71 @@ +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: "pentobi.sourceforge.io" + 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 + } + } + } +} diff --git a/src/pentobi/qml/AnalyzeDialog.qml b/src/pentobi/qml/AnalyzeDialog.qml new file mode 100644 index 0000000..2db821a --- /dev/null +++ b/src/pentobi/qml/AnalyzeDialog.qml @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/AnalyzeGame.qml b/src/pentobi/qml/AnalyzeGame.qml new file mode 100644 index 0000000..ff2b153 --- /dev/null +++ b/src/pentobi/qml/AnalyzeGame.qml @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------- +/** @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) + } + } +} diff --git a/src/pentobi/qml/AppearanceDialog.qml b/src/pentobi/qml/AppearanceDialog.qml new file mode 100644 index 0000000..1a2319f --- /dev/null +++ b/src/pentobi/qml/AppearanceDialog.qml @@ -0,0 +1,213 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/AsciiArtSaveDialog.qml b/src/pentobi/qml/AsciiArtSaveDialog.qml new file mode 100644 index 0000000..8a909e6 --- /dev/null +++ b/src/pentobi/qml/AsciiArtSaveDialog.qml @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +/** @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) + } +} diff --git a/src/pentobi/qml/Board.qml b/src/pentobi/qml/Board.qml new file mode 100644 index 0000000..e380caf --- /dev/null +++ b/src/pentobi/qml/Board.qml @@ -0,0 +1,295 @@ +//----------------------------------------------------------------------------- +/** @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) + } + } + } +} diff --git a/src/pentobi/qml/BoardContextMenu.qml b/src/pentobi/qml/BoardContextMenu.qml new file mode 100644 index 0000000..2c72000 --- /dev/null +++ b/src/pentobi/qml/BoardContextMenu.qml @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @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() + } + } +} diff --git a/src/pentobi/qml/BusyIndicator.qml b/src/pentobi/qml/BusyIndicator.qml new file mode 100644 index 0000000..874bf5f --- /dev/null +++ b/src/pentobi/qml/BusyIndicator.qml @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/Button.qml b/src/pentobi/qml/Button.qml new file mode 100644 index 0000000..441bb06 --- /dev/null +++ b/src/pentobi/qml/Button.qml @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @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" + } +} diff --git a/src/pentobi/qml/ButtonApply.qml b/src/pentobi/qml/ButtonApply.qml new file mode 100644 index 0000000..a85d408 --- /dev/null +++ b/src/pentobi/qml/ButtonApply.qml @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/ButtonCancel.qml b/src/pentobi/qml/ButtonCancel.qml new file mode 100644 index 0000000..c8fe66c --- /dev/null +++ b/src/pentobi/qml/ButtonCancel.qml @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/ButtonClose.qml b/src/pentobi/qml/ButtonClose.qml new file mode 100644 index 0000000..1003a06 --- /dev/null +++ b/src/pentobi/qml/ButtonClose.qml @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/ButtonOk.qml b/src/pentobi/qml/ButtonOk.qml new file mode 100644 index 0000000..f27018e --- /dev/null +++ b/src/pentobi/qml/ButtonOk.qml @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/ButtonToolTip.qml b/src/pentobi/qml/ButtonToolTip.qml new file mode 100644 index 0000000..ec7b3f2 --- /dev/null +++ b/src/pentobi/qml/ButtonToolTip.qml @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @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 + } +} diff --git a/src/pentobi/qml/Comment.qml b/src/pentobi/qml/Comment.qml new file mode 100644 index 0000000..4e384f3 --- /dev/null +++ b/src/pentobi/qml/Comment.qml @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } +} diff --git a/src/pentobi/qml/ComputerDialog.qml b/src/pentobi/qml/ComputerDialog.qml new file mode 100644 index 0000000..c96dc39 --- /dev/null +++ b/src/pentobi/qml/ComputerDialog.qml @@ -0,0 +1,193 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } + } +} diff --git a/src/pentobi/qml/Controls.js b/src/pentobi/qml/Controls.js new file mode 100644 index 0000000..12317fb --- /dev/null +++ b/src/pentobi/qml/Controls.js @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @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) +} diff --git a/src/pentobi/qml/Dialog.qml b/src/pentobi/qml/Dialog.qml new file mode 100644 index 0000000..e08294f --- /dev/null +++ b/src/pentobi/qml/Dialog.qml @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +/** @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() + } +} diff --git a/src/pentobi/qml/DialogButtonBox.qml b/src/pentobi/qml/DialogButtonBox.qml new file mode 100644 index 0000000..c68c425 --- /dev/null +++ b/src/pentobi/qml/DialogButtonBox.qml @@ -0,0 +1,13 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/DialogButtonBoxOkCancel.qml b/src/pentobi/qml/DialogButtonBoxOkCancel.qml new file mode 100644 index 0000000..563a144 --- /dev/null +++ b/src/pentobi/qml/DialogButtonBoxOkCancel.qml @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/qml/DialogButtonBoxOkCancel.qml + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +import "." as Pentobi + +Pentobi.DialogButtonBox { + ButtonCancel { } + ButtonOk { } +} diff --git a/src/pentobi/qml/DialogLoader.qml b/src/pentobi/qml/DialogLoader.qml new file mode 100644 index 0000000..d99bd05 --- /dev/null +++ b/src/pentobi/qml/DialogLoader.qml @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------------- +/** @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() } +} diff --git a/src/pentobi/qml/ExportImageDialog.qml b/src/pentobi/qml/ExportImageDialog.qml new file mode 100644 index 0000000..c8a0d80 --- /dev/null +++ b/src/pentobi/qml/ExportImageDialog.qml @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @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 } + } + } +} diff --git a/src/pentobi/qml/FatalMessage.qml b/src/pentobi/qml/FatalMessage.qml new file mode 100644 index 0000000..aa8edd4 --- /dev/null +++ b/src/pentobi/qml/FatalMessage.qml @@ -0,0 +1,9 @@ +//----------------------------------------------------------------------------- +/** @file pentobi/qml/FatalMessage.qml + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +MessageDialog { + onClosed: Qt.quit() +} diff --git a/src/pentobi/qml/FileDialog.qml b/src/pentobi/qml/FileDialog.qml new file mode 100644 index 0000000..3e329f2 --- /dev/null +++ b/src/pentobi/qml/FileDialog.qml @@ -0,0 +1,296 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/GameDisplay.js b/src/pentobi/qml/GameDisplay.js new file mode 100644 index 0000000..bd622de --- /dev/null +++ b/src/pentobi/qml/GameDisplay.js @@ -0,0 +1,219 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/GameDisplayDesktop.qml b/src/pentobi/qml/GameDisplayDesktop.qml new file mode 100644 index 0000000..bfacdcf --- /dev/null +++ b/src/pentobi/qml/GameDisplayDesktop.qml @@ -0,0 +1,356 @@ +//----------------------------------------------------------------------------- +/** @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() + } +} diff --git a/src/pentobi/qml/GameDisplayMobile.qml b/src/pentobi/qml/GameDisplayMobile.qml new file mode 100644 index 0000000..2889625 --- /dev/null +++ b/src/pentobi/qml/GameDisplayMobile.qml @@ -0,0 +1,265 @@ +//----------------------------------------------------------------------------- +/** @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() + } +} diff --git a/src/pentobi/qml/GameInfoDialog.qml b/src/pentobi/qml/GameInfoDialog.qml new file mode 100644 index 0000000..c1fe63b --- /dev/null +++ b/src/pentobi/qml/GameInfoDialog.qml @@ -0,0 +1,134 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/GameVariantDialog.qml b/src/pentobi/qml/GameVariantDialog.qml new file mode 100644 index 0000000..f934cfe --- /dev/null +++ b/src/pentobi/qml/GameVariantDialog.qml @@ -0,0 +1,273 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } + } + } +} diff --git a/src/pentobi/qml/GotoMoveDialog.qml b/src/pentobi/qml/GotoMoveDialog.qml new file mode 100644 index 0000000..382e722 --- /dev/null +++ b/src/pentobi/qml/GotoMoveDialog.qml @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------- +/** @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 } + } + } +} diff --git a/src/pentobi/qml/HelpWindow.qml b/src/pentobi/qml/HelpWindow.qml new file mode 100644 index 0000000..39f8d4a --- /dev/null +++ b/src/pentobi/qml/HelpWindow.qml @@ -0,0 +1,67 @@ +//----------------------------------------------------------------------------- +/** @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" + } +} diff --git a/src/pentobi/qml/ImageSaveDialog.qml b/src/pentobi/qml/ImageSaveDialog.qml new file mode 100644 index 0000000..d3e7d35 --- /dev/null +++ b/src/pentobi/qml/ImageSaveDialog.qml @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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) + } +} diff --git a/src/pentobi/qml/InitialRatingDialog.qml b/src/pentobi/qml/InitialRatingDialog.qml new file mode 100644 index 0000000..1b87a48 --- /dev/null +++ b/src/pentobi/qml/InitialRatingDialog.qml @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------- +/** @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") } + } + } +} diff --git a/src/pentobi/qml/LineSegment.qml b/src/pentobi/qml/LineSegment.qml new file mode 100644 index 0000000..df4cb5a --- /dev/null +++ b/src/pentobi/qml/LineSegment.qml @@ -0,0 +1,179 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/Main.js b/src/pentobi/qml/Main.js new file mode 100644 index 0000000..a5479cd --- /dev/null +++ b/src/pentobi/qml/Main.js @@ -0,0 +1,785 @@ +//----------------------------------------------------------------------------- +/** @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() +} diff --git a/src/pentobi/qml/Main.qml b/src/pentobi/qml/Main.qml new file mode 100644 index 0000000..7f39404 --- /dev/null +++ b/src/pentobi/qml/Main.qml @@ -0,0 +1,531 @@ +//----------------------------------------------------------------------------- +/** @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() + } +} diff --git a/src/pentobi/qml/Menu.qml b/src/pentobi/qml/Menu.qml new file mode 100644 index 0000000..df1ff7f --- /dev/null +++ b/src/pentobi/qml/Menu.qml @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------- +/** @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) + } +} diff --git a/src/pentobi/qml/MenuComputer.qml b/src/pentobi/qml/MenuComputer.qml new file mode 100644 index 0000000..44903f9 --- /dev/null +++ b/src/pentobi/qml/MenuComputer.qml @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @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")) + } +} diff --git a/src/pentobi/qml/MenuEdit.qml b/src/pentobi/qml/MenuEdit.qml new file mode 100644 index 0000000..dfad4d5 --- /dev/null +++ b/src/pentobi/qml/MenuEdit.qml @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------------- +/** @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() + } + } +} diff --git a/src/pentobi/qml/MenuExport.qml b/src/pentobi/qml/MenuExport.qml new file mode 100644 index 0000000..e479b24 --- /dev/null +++ b/src/pentobi/qml/MenuExport.qml @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +/** @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() + } + } +} diff --git a/src/pentobi/qml/MenuGame.qml b/src/pentobi/qml/MenuGame.qml new file mode 100644 index 0000000..21e78bb --- /dev/null +++ b/src/pentobi/qml/MenuGame.qml @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +/** @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")) + } +} diff --git a/src/pentobi/qml/MenuGo.qml b/src/pentobi/qml/MenuGo.qml new file mode 100644 index 0000000..42120a6 --- /dev/null +++ b/src/pentobi/qml/MenuGo.qml @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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")) + } +} diff --git a/src/pentobi/qml/MenuHelp.qml b/src/pentobi/qml/MenuHelp.qml new file mode 100644 index 0000000..a358331 --- /dev/null +++ b/src/pentobi/qml/MenuHelp.qml @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @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() + } +} diff --git a/src/pentobi/qml/MenuItem.qml b/src/pentobi/qml/MenuItem.qml new file mode 100644 index 0000000..1700fdf --- /dev/null +++ b/src/pentobi/qml/MenuItem.qml @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/** @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) + "" + + root.text.substring(pos + 1, pos + 2) + + "" + 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 + } + } +} diff --git a/src/pentobi/qml/MenuRecentFiles.qml b/src/pentobi/qml/MenuRecentFiles.qml new file mode 100644 index 0000000..6e673f2 --- /dev/null +++ b/src/pentobi/qml/MenuRecentFiles.qml @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------------- +/** @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() + }) + } +} diff --git a/src/pentobi/qml/MenuSeparator.qml b/src/pentobi/qml/MenuSeparator.qml new file mode 100644 index 0000000..afb4029 --- /dev/null +++ b/src/pentobi/qml/MenuSeparator.qml @@ -0,0 +1,16 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/MenuTools.qml b/src/pentobi/qml/MenuTools.qml new file mode 100644 index 0000000..14d2739 --- /dev/null +++ b/src/pentobi/qml/MenuTools.qml @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @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() + }) + } +} diff --git a/src/pentobi/qml/MenuView.qml b/src/pentobi/qml/MenuView.qml new file mode 100644 index 0000000..6c786e8 --- /dev/null +++ b/src/pentobi/qml/MenuView.qml @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------------- +/** @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")) + } +} diff --git a/src/pentobi/qml/MessageDialog.qml b/src/pentobi/qml/MessageDialog.qml new file mode 100644 index 0000000..a2efcf3 --- /dev/null +++ b/src/pentobi/qml/MessageDialog.qml @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } +} diff --git a/src/pentobi/qml/MoveAnnotationDialog.qml b/src/pentobi/qml/MoveAnnotationDialog.qml new file mode 100644 index 0000000..cebe897 --- /dev/null +++ b/src/pentobi/qml/MoveAnnotationDialog.qml @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/NavigationButtons.qml b/src/pentobi/qml/NavigationButtons.qml new file mode 100644 index 0000000..121700b --- /dev/null +++ b/src/pentobi/qml/NavigationButtons.qml @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +/** @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 + } +} diff --git a/src/pentobi/qml/NavigationPanel.qml b/src/pentobi/qml/NavigationPanel.qml new file mode 100644 index 0000000..e6f7a88 --- /dev/null +++ b/src/pentobi/qml/NavigationPanel.qml @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +/** @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) + } +} diff --git a/src/pentobi/qml/NewFolderDialog.qml b/src/pentobi/qml/NewFolderDialog.qml new file mode 100644 index 0000000..31c0b3a --- /dev/null +++ b/src/pentobi/qml/NewFolderDialog.qml @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------- +/** @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 } + } + } +} diff --git a/src/pentobi/qml/OpenDialog.qml b/src/pentobi/qml/OpenDialog.qml new file mode 100644 index 0000000..b6971cc --- /dev/null +++ b/src/pentobi/qml/OpenDialog.qml @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +/** @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() + } +} diff --git a/src/pentobi/qml/PieceCallisto.qml b/src/pentobi/qml/PieceCallisto.qml new file mode 100644 index 0000000..1052c9f --- /dev/null +++ b/src/pentobi/qml/PieceCallisto.qml @@ -0,0 +1,318 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/PieceClassic.qml b/src/pentobi/qml/PieceClassic.qml new file mode 100644 index 0000000..eca0acb --- /dev/null +++ b/src/pentobi/qml/PieceClassic.qml @@ -0,0 +1,275 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/PieceFlipAnimation.qml b/src/pentobi/qml/PieceFlipAnimation.qml new file mode 100644 index 0000000..1935dd6 --- /dev/null +++ b/src/pentobi/qml/PieceFlipAnimation.qml @@ -0,0 +1,13 @@ +//----------------------------------------------------------------------------- +/** @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" +} diff --git a/src/pentobi/qml/PieceGembloQ.qml b/src/pentobi/qml/PieceGembloQ.qml new file mode 100644 index 0000000..90ea1bb --- /dev/null +++ b/src/pentobi/qml/PieceGembloQ.qml @@ -0,0 +1,276 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/PieceList.qml b/src/pentobi/qml/PieceList.qml new file mode 100644 index 0000000..b33a308 --- /dev/null +++ b/src/pentobi/qml/PieceList.qml @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } +} diff --git a/src/pentobi/qml/PieceManipulator.qml b/src/pentobi/qml/PieceManipulator.qml new file mode 100644 index 0000000..c0a9d97 --- /dev/null +++ b/src/pentobi/qml/PieceManipulator.qml @@ -0,0 +1,122 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } +} diff --git a/src/pentobi/qml/PieceNexos.qml b/src/pentobi/qml/PieceNexos.qml new file mode 100644 index 0000000..61966fa --- /dev/null +++ b/src/pentobi/qml/PieceNexos.qml @@ -0,0 +1,325 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/PieceRotationAnimation.qml b/src/pentobi/qml/PieceRotationAnimation.qml new file mode 100644 index 0000000..134d7d9 --- /dev/null +++ b/src/pentobi/qml/PieceRotationAnimation.qml @@ -0,0 +1,12 @@ +//----------------------------------------------------------------------------- +/** @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 +} diff --git a/src/pentobi/qml/PieceSelectorDesktop.qml b/src/pentobi/qml/PieceSelectorDesktop.qml new file mode 100644 index 0000000..c15b0c0 --- /dev/null +++ b/src/pentobi/qml/PieceSelectorDesktop.qml @@ -0,0 +1,134 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } +} diff --git a/src/pentobi/qml/PieceSelectorMobile.qml b/src/pentobi/qml/PieceSelectorMobile.qml new file mode 100644 index 0000000..e09609f --- /dev/null +++ b/src/pentobi/qml/PieceSelectorMobile.qml @@ -0,0 +1,251 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/PieceSwitchedFlipAnimation.qml b/src/pentobi/qml/PieceSwitchedFlipAnimation.qml new file mode 100644 index 0000000..c67017b --- /dev/null +++ b/src/pentobi/qml/PieceSwitchedFlipAnimation.qml @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +/** @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 } +} diff --git a/src/pentobi/qml/PieceTrigon.qml b/src/pentobi/qml/PieceTrigon.qml new file mode 100644 index 0000000..7a9f607 --- /dev/null +++ b/src/pentobi/qml/PieceTrigon.qml @@ -0,0 +1,312 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/QuarterSquare.qml b/src/pentobi/qml/QuarterSquare.qml new file mode 100644 index 0000000..8f0505c --- /dev/null +++ b/src/pentobi/qml/QuarterSquare.qml @@ -0,0 +1,136 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/QuestionDialog.qml b/src/pentobi/qml/QuestionDialog.qml new file mode 100644 index 0000000..7f88d85 --- /dev/null +++ b/src/pentobi/qml/QuestionDialog.qml @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } +} diff --git a/src/pentobi/qml/RatingDialog.qml b/src/pentobi/qml/RatingDialog.qml new file mode 100644 index 0000000..e7633c5 --- /dev/null +++ b/src/pentobi/qml/RatingDialog.qml @@ -0,0 +1,251 @@ +//----------------------------------------------------------------------------- +/** @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() + } + } + } + } + } + } + } + } +} diff --git a/src/pentobi/qml/RatingGraph.qml b/src/pentobi/qml/RatingGraph.qml new file mode 100644 index 0000000..4172520 --- /dev/null +++ b/src/pentobi/qml/RatingGraph.qml @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------------- +/** @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() + } +} diff --git a/src/pentobi/qml/SaveDialog.qml b/src/pentobi/qml/SaveDialog.qml new file mode 100644 index 0000000..b4f6524 --- /dev/null +++ b/src/pentobi/qml/SaveDialog.qml @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +/** @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)) + } +} diff --git a/src/pentobi/qml/ScoreDisplay.qml b/src/pentobi/qml/ScoreDisplay.qml new file mode 100644 index 0000000..646b65b --- /dev/null +++ b/src/pentobi/qml/ScoreDisplay.qml @@ -0,0 +1,131 @@ +//----------------------------------------------------------------------------- +/** @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 } + } +} diff --git a/src/pentobi/qml/ScoreElement.qml b/src/pentobi/qml/ScoreElement.qml new file mode 100644 index 0000000..cf17dd4 --- /dev/null +++ b/src/pentobi/qml/ScoreElement.qml @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +/** @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%L2".arg(bonus > 0 ? "★" : "").arg(value) + color: theme.colorText + opacity: 0.8 + font.preferShaping: false + } +} diff --git a/src/pentobi/qml/ScoreElement2.qml b/src/pentobi/qml/ScoreElement2.qml new file mode 100644 index 0000000..872f107 --- /dev/null +++ b/src/pentobi/qml/ScoreElement2.qml @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +/** @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 ? "%L1".arg(value) : "%L1".arg(value) + color: theme.colorText + opacity: 0.8 + font.preferShaping: false + } +} diff --git a/src/pentobi/qml/Square.qml b/src/pentobi/qml/Square.qml new file mode 100644 index 0000000..18b2c5a --- /dev/null +++ b/src/pentobi/qml/Square.qml @@ -0,0 +1,163 @@ +//----------------------------------------------------------------------------- +/** @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 + } + } + } +} diff --git a/src/pentobi/qml/ToolBar.qml b/src/pentobi/qml/ToolBar.qml new file mode 100644 index 0000000..cf2666d --- /dev/null +++ b/src/pentobi/qml/ToolBar.qml @@ -0,0 +1,348 @@ +//----------------------------------------------------------------------------- +/** @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 + } +} diff --git a/src/pentobi/qml/Triangle.qml b/src/pentobi/qml/Triangle.qml new file mode 100644 index 0000000..1352170 --- /dev/null +++ b/src/pentobi/qml/Triangle.qml @@ -0,0 +1,293 @@ +//----------------------------------------------------------------------------- +/** @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 } + ] + } + } + } +} diff --git a/src/pentobi/qml/i18n/qml_de.ts b/src/pentobi/qml/i18n/qml_de.ts new file mode 100644 index 0000000..bfb5c31 --- /dev/null +++ b/src/pentobi/qml/i18n/qml_de.ts @@ -0,0 +1,1744 @@ + + + + + AboutDialog + + Copyright © 2011–%1 Markus Enzenberger + Copyright © 2011–%1 Markus Enzenberger + + + Computer opponent for the board game Blokus + Computer-Gegner für das Brettspiel Blokus + + + Pentobi %1 + The argument is the application version. + Pentobi %1 + + + + Actions + + Main Variation + Hauptvariante + + + Beginning of Branch + Verzweigungsanfang + + + Settings… + Menu item Computer/Settings + Einstellungen … + + + Find Move + Zug finden + + + Next Comment + Nächster Kommentar + + + Fullscreen + Vollbild + + + Move Number… + Zugnummer … + + + Pentobi Help + Pentobi-Hilfe + + + New + Neu + + + Rated Game + Gewertetes Spiel + + + Open… + Öffnen … + + + Play + Spielen + + + Play Move + Play a single move + Zug spielen + + + Quit + Beenden + + + Save + Speichern + + + Save As… + Speichern unter … + + + Stop + Stopp + + + Undo Move + Zug rückgängig + + + Game Info + Spielinformation + + + Comment + Kommentar + + + Settings + Menu item Computer/Settings + Einstellungen + + + + AnalyzeDialog + + Analysis speed: + Analysegeschwindigkeit: + + + Fast + Schnell + + + Normal + Normal + + + Slow + Langsam + + + + AnalyzeGame + + (No analysis) + (Keine Analyse) + + + + AppearanceDialog + + Coordinates + Koordinaten + + + Show variations + Varianten zeigen + + + Light + Hell + + + Dark + Dunkel + + + Colorblind light + Farbenblind hell + + + Colorblind dark + Farbenblind dunkel + + + System + Name of theme using default system colors + System + + + Move marking: + Zugmarkierung: + + + Last with dot + Letzter mit Punkt + + + Last with number + Letzter mit Nummer + + + All with number + Alle mit Nummer + + + None + Move marking/None + Keine + + + Animations + Animationen + + + Show comment: + Kommentar zeigen: + + + Always + Show-comment mode + Immer + + + As needed + Show-comment mode + Bei Bedarf + + + Never + Show-comment mode + Nie + + + Color theme: + Farbthema: + + + Move number + Check box in appearance dialog whether to show the move number in the status bar. + Zugnummer + + + + AsciiArtSaveDialog + + Export ASCII Art + ASCII-Art exportieren + + + Text files + Textdateien + + + + BoardContextMenu + + Go to Move %1 + Gehe zu Zug %1 + + + Move Annotation + Annotierung + + + Move Annotation (%1) + The argument is the annotation symbol for the current move + Annotierung (%1) + + + + ButtonApply + + Apply + Anwenden + + + + ButtonCancel + + Cancel + Abbrechen + + + + ButtonClose + + Close + Schließen + + + + ButtonOk + + OK + OK + + + + ComputerDialog + + Computer plays: + Computer spielt: + + + Blue/Red + Blau/Rot + + + Purple + Lila + + + Green + Grün + + + Blue + Blau + + + Yellow/Green + Gelb/Grün + + + Orange + Orange + + + Yellow + Gelb + + + Red + Rot + + + Level %1 + Stufe %1 + + + + ExportImageDialog + + Image width: + Bildbreite: + + + + FileDialog + + Overwrite file? + Datei überschreiben? + + + Open + Öffnen + + + Save + Speichern + + + All files + Alle Dateien + + + + GameDisplayDesktop + + Computer is thinking… + Computer denkt … + + + Running game analysis… + Spiel wird analysiert … + + + Computer is thinking… (up to %1 seconds remaining) + Computer denkt … (maximal %1 Sekunden verbleibend) + + + Computer is thinking… (up to %1 minutes remaining) + Computer denkt … (maximal %1 Minuten verbleibend) + + + + GameInfoDialog + + Player Blue/Red: + Spieler Blau/Rot: + + + Player Purple: + Spieler Lila: + + + Player Green: + Spieler Grün: + + + Player Blue: + Spieler Blau: + + + Player Yellow/Green: + Spieler Gelb/Grün: + + + Player Orange: + Spieler Orange: + + + Player Yellow: + Spieler Gelb: + + + Player Red: + Spieler Rot: + + + Date: + Datum: + + + Time: + Bedenkzeit: + + + Event: + Veranstaltung: + + + Round: + Runde: + + + + GameModel + + Blue/Red + Blau/Rot + + + Purple + Lila + + + Green + Grün + + + Blue + Blau + + + Yellow/Green + Gelb/Grün + + + Orange + Orange + + + Yellow + Gelb + + + Red + Rot + + + Purple wins with 1 point. + Lila gewinnt mit 1 Punkt. + + + Purple wins with %L1 points. + Lila gewinnt mit %L1 Punkten. + + + Orange wins with 1 point. + Orange gewinnt mit 1 Punkt. + + + Orange wins with %L1 points. + Orange gewinnt mit %L1 Punkten. + + + Game ends in a tie. + Spiel endet unentschieden. + + + Green wins with 1 point. + Grün gewinnt mit 1 Punkt. + + + Green wins with %L1 points. + Grün gewinnt mit %L1 Punkten. + + + Blue wins with 1 point. + Blau gewinnt mit 1 Punkt. + + + Blue wins with %L1 points. + Blau gewinnt mit %L1 Punkten. + + + Green wins (tie resolved). + Grün gewinnt (Unentschieden aufgelöst). + + + Blue/Red wins with 1 point. + Blau/Rot gewinnt mit 1 Punkt. + + + Blue/Red wins with %L1 points. + Blau/Rot gewinnt mit %L1 Punkten. + + + Yellow/Green wins with 1 point. + Gelb/Grün gewinnt mit 1 Punkt. + + + Yellow/Green wins with %L1 points. + Gelb/Grün gewinnt mit %L1 Punkten. + + + Yellow/Green wins (tie resolved). + Gelb/Grün gewinnt (Unentschieden aufgelöst). + + + Blue wins. + Blau gewinnt. + + + Yellow wins. + Gelb gewinnt. + + + Red wins. + Rot gewinnt. + + + Red wins (tie resolved). + Rot gewinnt (Unentschieden aufgelöst). + + + Yellow wins (tie resolved). + Gelb gewinnt (Unentschieden aufgelöst). + + + Game ends in a tie between Blue and Yellow. + Spiel endet unentschieden zwischen Blau und Gelb. + + + Game ends in a tie between Blue and Red. + Spiel endet unentschieden zwischen Blau und Rot. + + + Game ends in a tie between Yellow and Red. + Spiel endet unentschieden zwischen Gelb und Rot. + + + Game ends in a tie between all players. + Spiel endet unentschieden zwischen allen Spielern. + + + Green wins. + Grün gewinnt. + + + Game ends in a tie between Blue, Yellow and Red. + Spiel endet unentschieden zwischen Blau, Gelb und Rot. + + + Game ends in a tie between Blue, Yellow and Green. + Spiel endet unentschieden zwischen Blau, Gelb und Grün. + + + Game ends in a tie between Blue, Red and Green. + Spiel endet unentschieden zwischen Blau, Rot und Grün. + + + Game ends in a tie between Yellow, Red and Green. + Spiel endet unentschieden zwischen Gelb, Rot und Grün. + + + Invalid Blokus SGF file. (%1) + Ungültige Blokus-SGF-Datei. (%1) + + + Clipboard is empty. + Zwischenablage ist leer. + + + Untitled + Unbenannt + + + Untitled %1 + The argument is a number, which will be increased if a file with the same name already exists + Unbenannt %1 + + + New Folder + Neuer Ordner + + + New Folder %1 + The argument is a number, which will be increased if a folder with the same name already exists + Neuer Ordner %1 + + + (Setup) + (Stellung) + + + (No moves) + (Keine Züge) + + + Move %1 + The argument is the current move number. + Zug %1 + + + Unsupported character set + Zeichensatz nicht unterstützt + + + + GameVariantDialog + + Classic + Klassisch + + + Duo + Duo + + + Junior + Junior + + + Trigon + Trigon + + + Nexos + Nexos + + + GembloQ + GembloQ + + + Callisto + Callisto + + + Players: + Spieler: + + + Colors: + Farben: + + + + GotoMoveDialog + + Move number: + Zugnummer: + + + + HelpWindow + + Pentobi Help + Pentobi-Hilfe + + + + ImageSaveDialog + + Save Image + Grafik speichern + + + PNG image files + PNG-Bilddateien + + + JPEG image files + JPEG-Bilddateien + + + + InitialRatingDialog + + Initialize your rating for this game variant. + Initialisieren Sie Ihre Wertung für diese Spielvariante. + + + Initial rating: + Anfangswertung: + + + Beginner + Anfänger + + + Expert + Experte + + + + Main + + Pentobi + Window title if no file is loaded. + Pentobi + + + Game analysis is only possible in main variation. + Spielanalyse ist nur in Hauptvariante möglich. + + + Autosaved game was changed by another instance of Pentobi. Overwrite? + Automatisch gespeichertes Spiel wurde von einer anderen Instanz von Pentobi geändert. Überschreiben? + + + Your rating has increased from %1 to %2. + Ihre Wertung hat sich von %1 auf %2 erhöht. + + + Your rating has decreased from %1 to %2. + Ihre Wertung hat sich von %1 auf %2 verringert. + + + Your rating stays at %1. + Ihre Wertung bleibt bei %1. + + + No permission to access storage + Keine Berechtigung zu Zugriff auf Speicher + + + Delete all rating information for the current game variant? + Alle Wertungsinformationen für die gegenwärtige Spielvariante löschen? + + + Delete all variations? + Alle Varianten löschen? + + + Save failed. + Speichern fehlgeschlagen. + + + End of tree was reached. Continue search from start of the tree? + Ende des Spielbaums erreicht. Suche vom Start des Spielbaums fortsetzen? + + + No comment found + Kein Kommentar gefunden + + + %1 (modified) + %1 (geändert) + + + File has been modified by another application. Reload? + Datei wurde von einer anderen Anwendung bearbeitet. Neu laden? + + + Continue computer move? + Computer-Zug fortsetzen? + + + Keep only position? + Nur Brettstellung behalten? + + + Keep only subtree? + Nur Teilbaum behalten? + + + Open failed. + Öffnen fehlgeschlagen. + + + Start rated game with Purple against Pentobi level %1? + Gewertetes Spiel mit Lila gegen Pentobi Stufe %1 beginnen? + + + Start rated game with Green against Pentobi level %1? + Gewertetes Spiel mit Grün gegen Pentobi Stufe %1 beginnen? + + + Start rated game with Blue/Red against Pentobi level %1? + Gewertetes Spiel mit Blau/Rot gegen Pentobi Stufe %1 beginnen? + + + Start rated game with Blue against Pentobi level %1? + Gewertetes Spiel mit Blau gegen Pentobi Stufe %1 beginnen? + + + Start rated game with Orange against Pentobi level %1? + Gewertetes Spiel mit Orange gegen Pentobi Stufe %1 beginnen? + + + Start rated game with Yellow/Green against Pentobi level %1? + Gewertetes Spiel mit Gelb/Grün gegen Pentobi Stufe %1 beginnen? + + + Start rated game with Yellow against Pentobi level %1? + Gewertetes Spiel mit Gelb gegen Pentobi Stufe %1 beginnen? + + + Start rated game with Red against Pentobi level %1? + Gewertetes Spiel mit Rot gegen Pentobi Stufe %1 beginnen? + + + You have not yet played rated games in this game variant. + Sie haben noch keine gewerteten Spiele in dieser Spielvariante gespielt. + + + Truncate this subtree? + Diesen Teilbaum abschneiden? + + + Truncate children? + Kindknoten abschneiden? + + + Discard game? + Spiel verwerfen? + + + Pentobi %1 (level %2) + Player name for game info in rated game. First argument is version of Pentobi, second argument is level. + Pentobi %1 (Stufe %2) + + + Human + Player name for game info in rated game. + Mensch + + + Rated game + Gewertetes Spiel + + + File has been modified by another application. Overwrite? + Datei wurde von einer anderen Anwendung bearbeitet. Überschreiben? + + + %1 - Pentobi + Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified. + %1 - Pentobi + + + Not enough memory + Nicht genügend Speicher + + + Game analysis aborted + Spielanalyse abgebrochen + + + Computer move aborted + Computer-Zug abgebrochen + + + Rating information deleted + Wertungsinformationen gelöscht + + + Variations deleted + Varianten gelöscht + + + File saved + Datei gespeichert + + + Saving image failed or unsupported image format + Grafik konnte nicht gespeichert werden oder Bildformat nicht unterstützt + + + Image saved + Grafik gespeichert + + + Creating image failed + Grafik konnte nicht erzeugt werden + + + Continuing rated game + Gewertetes Spiel wird fortgesetzt + + + Kept only position + Nur Brettstellung behalten + + + Kept only subtree + Nur Teilbaum behalten + + + Variation is now %1 + Variante ist jetzt %1 + + + Children truncated + Kindknoten abgeschnitten + + + Setup + Small-screen label for setup mode (short for "Setup Mode"). + Aufbau + + + Setup Mode + Stellungsaufbau + + + Rated + Label for ongoing rated game + Gewertet + + + Rated %1 + Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number. + Gewertet %1 + + + Rated Game %1 + Label for rated game. The argument is the game number. + Gewertetes Spiel %1 + + + Main Variation + Hauptvariante + + + Beginning of Branch + Verzweigungsanfang + + + Comment + Kommentar + + + Settings + Menu item Computer/Settings + Einstellungen + + + Find Move + Zug finden + + + Next Comment + Nächster Kommentar + + + Fullscreen + Vollbild + + + Game Info + Spielinformation + + + Move Number… + Zugnummer … + + + Pentobi Help + Pentobi-Hilfe + + + New + Neu + + + Rated Game + Gewertetes Spiel + + + Open… + Öffnen … + + + Play + Spielen + + + Play Move + Play a single move + Zug spielen + + + Quit + Beenden + + + Save + Speichern + + + Save As… + Speichern unter … + + + Stop + Stopp + + + Undo Move + Zug rückgängig + + + + MenuComputer + + Computer + Computer + + + C + Mnemonic for menu Computer. Leave empty for no mnemonic. + C + + + S + Mnemonic for menu item Computer Settings. Leave empty for no mnemonic. + E + + + P + Mnemonic for menu item Play. Leave empty for no mnemonic. + S + + + M + Mnemonic for menu item Play Move. Leave empty for no mnemonic. + Z + + + O + Mnemonic for menu item Stop. Leave empty for no mnemonic. + O + + + + MenuEdit + + Edit + Bearbeiten + + + E + Mnemonic for menu Edit. Leave empty for no mnemonic. + B + + + M + Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic. + H + + + Make Main Variation + Zu Hauptvariante machen + + + Variation Up + Short for Move Variation Up + Variante nach oben + + + U + Mnemonic for menu item Variation Up. Leave empty for no mnemonic. + O + + + Variation Down + Short for Move Variation Down + Variante nach unten + + + W + Mnemonic for menu item Variation Down. Leave empty for no mnemonic. + U + + + Delete Variations + Varianten löschen + + + D + Mnemonic for menu item Delete Variations. Leave empty for no mnemonic. + V + + + Truncate + Abschneiden + + + T + Mnemonic for menu item Truncate. Leave empty for no mnemonic. + A + + + Truncate Children + Kindknoten abschneiden + + + C + Mnemonic for menu item Truncate Children. Leave empty for no mnemonic. + K + + + Keep Position + Brettstellung behalten + + + P + Mnemonic for menu item Keep Position. Leave empty for no mnemonic. + B + + + Keep Subtree + Teilbaum behalten + + + S + Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic. + T + + + Setup Mode + Stellungsaufbau + + + O + Mnemonic for menu item Setup Mode. Leave empty for no mnemonic. + S + + + Next Color + Nächste Farbe + + + N + Mnemonic for menu item Next Color. Leave empty for no mnemonic. + F + + + Annotation… + Annotierung … + + + A + Mnemonic for menu item Annotation. Leave empty for no mnemonic. + N + + + Made main variation + Zu Hauptvariante gemacht + + + + MenuExport + + Export + Exportieren + + + E + Mnemonic for menu Export. Leave empty for no mnemonic. + E + + + M + Mnemonic for menu item Image. Leave empty for no mnemonic. + G + + + A + Mnemonic for menu item ASCII Art. Leave empty for no mnemonic. + A + + + Image… + Grafik … + + + ASCII Art… + ASCII-Art … + + + + MenuGame + + Game + Spiel + + + G + Mnemonic for menu Game. Leave empty for no mnemonic. + S + + + N + Mnemonic for menu item New. Leave empty for no mnemonic. + N + + + R + Mnemonic for menu item Rated Game. Leave empty for no mnemonic. + W + + + Game Variant… + Spielvariante … + + + V + Mnemonic for menu item Game Variant. Leave empty for no mnemonic. + V + + + I + Mnemonic for menu item Game Info. Leave empty for no mnemonic. + O + + + U + Mnemonic for menu item Undo. Leave empty for no mnemonic. + R + + + F + Mnemonic for menu item Find Move. Leave empty for no mnemonic. + Z + + + O + Mnemonic for menu item Open. Leave empty for no mnemonic. + f + + + Open Clipboard + Zwischenablage öffnen + + + C + Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic. + A + + + S + Mnemonic for menu item Save. Leave empty for no mnemonic. + S + + + A + Mnemonic for menu item Save As. Leave empty for no mnemonic. + U + + + Q + Mnemonic for menu item Quit. Leave empty for no mnemonic. + B + + + + MenuGo + + Go + Gehe zu + + + O + Mnemonic for menu Go. Leave empty for no mnemonic. + G + + + N + Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic. + N + + + M + Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic. + H + + + B + Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic. + V + + + C + Mnemonic for menu item Next Comment. Leave empty for no mnemonic. + K + + + + MenuHelp + + Help + Hilfe + + + H + Mnemonic for menu Help. Leave empty for no mnemonic. + H + + + P + Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic. + P + + + About Pentobi + Über Pentobi + + + A + Mnemonic for menu item About Pentobi. Leave empty for no mnemonic. + B + + + Report Bug + Fehler melden + + + B + Mnemonic for menu item Report Bug. Leave empty for no mnemonic. + F + + + + MenuItem + + Ctrl + Shortcut modifier key as displayed in menu item text (abbreviate if long) + Strg + + + Shift + Shortcut modifier key as displayed in menu item text (abbreviate if long) + Umschalt + + + + MenuRecentFiles + + Open Recent + Zuletzt benutzt + + + P + Mnemonic for menu Open Recent. Leave empty for no mnemonic. + T + + + %1. %2 + Format in recent files menu. First argument is the file number, second argument the file name. + %1. %2 + + + Clear List + Menu item for clearing the recent files list + Liste leeren + + + C + Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic. + L + + + + MenuTools + + Tools + Extras + + + T + Mnemonic for menu Tools. Leave empty for no mnemonic. + X + + + Rating + Wertung + + + R + Mnemonic for menu item Rating. Leave empty for no mnemonic. + W + + + Clear Rating + Wertung löschen + + + C + Mnemonic for menu item Clear Rating. Leave empty for no mnemonic. + L + + + Analyze Game + Spiel analysieren + + + A + Mnemonic for menu item Analyze Game. Leave empty for no mnemonic. + A + + + Clear Analysis + Analyse löschen + + + E + Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic. + C + + + Analyze Game… + Spiel analysieren … + + + + MenuView + + View + Ansicht + + + V + Mnemonic for menu View. Leave empty for no mnemonic. + A + + + Appearance… + Erscheinungsbild … + + + A + Mnemonic for menu Appearance. Leave empty for no mnemonic. + E + + + F + Mnemonic for menu item Fullscreen. Leave empty for no mnemonic. + V + + + C + Mnemonic for menu item View/Comment. Leave empty for no mnemonic. + K + + + Appearance + Erscheinungsbild + + + Toolbar + Werkzeugleiste + + + T + Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic. + W + + + + MoveAnnotationDialog + + Move %1 + Zug %1 + + + Very good + Sehr gut + + + Good + Gut + + + Interesting + Interessant + + + Doubtful + Zweifelhaft + + + Bad + Schlecht + + + Very Bad + Sehr schlecht + + + No annotation + Keine Annotierung + + + + NewFolderDialog + + Folder name: + Name des Ordners: + + + + OpenDialog + + Open + Öffnen + + + Blokus games + Blokus-Partien + + + + RatingDialog + + Your rating: + Ihre Wertung: + + + Game variant: + Spielvariante: + + + Classic (2) + Short for Classic (2 players) + Klassisch (2) + + + Classic (3) + Short for Classic (3 players) + Klassisch (3) + + + Classic (4) + Short for Classic (4 players) + Klassisch (4) + + + Duo + Duo + + + Junior + Junior + + + Trigon (2) + Short for Trigon (2 players) + Trigon (2) + + + Trigon (3) + Short for Trigon (3 players) + Trigon (3) + + + Trigon (4) + Short for Trigon (4 players) + Trigon (4) + + + Nexos (2) + Short for Nexos (2 players) + Nexos (2) + + + Nexos (4) + Short for Nexos (4 players) + Nexos (4) + + + Callisto (2) + Short for Callisto (2 players, 2 colors) + Callisto (2) + + + Callisto (2/4) + Short for Callisto (2 players, 4 colors) + Callisto (2/4) + + + Callisto (3) + Short for Callisto (3 players) + Callisto (3) + + + Callisto (4) + Short for Callisto (4 players) + Callisto (4) + + + GembloQ (4) + Short for GembloQ (4 players) + GembloQ (4) + + + GembloQ (2) + Short for GembloQ (2 players, 2 colors) + GembloQ (2) + + + GembloQ (2/4) + Short for GembloQ (2 players, 4 colors) + GembloQ (2/4) + + + GembloQ (3) + Short for GembloQ (3 players) + GembloQ (3) + + + Rated games: + Gewertete Spiele: + + + Best previous rating: + Beste frühere Wertung: + + + Recent development: + Aktuelle Entwicklung: + + + Game + Spiel + + + Result + Ergebnis + + + Win + Result of rated game is a win + Gewinn + + + Loss + Result of rated game is a loss + Verlust + + + Tie + 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. + Unentsch. + + + Level + Stufe + + + Your Color + Ihre Farbe + + + Date + Datum + + + Open Game %1 + Spiel %1 öffnen + + + + SaveDialog + + Save + Speichern + + + Blokus games + Blokus-Partien + + + + ToolBar + + Start a new game + Ein neues Spiel beginnen + + + Start a rated game + Ein gewertetes Spiel beginnen + + + Set the colors played by the computer + Die vom Computer gespielten Farben festlegen + + + Make the computer continue to play the current color + Den Computer die gegenwärtige Farbe weiterspielen lassen + + + Make the computer play the current color + Den Computer die gegenwärtige Farbe spielen lassen + + + Go to beginning of game + Zum Anfang des Spiels gehen + + + Go ten moves backward + Zehn Züge zurück gehen + + + Go one move backward + Einen Zug zurück gehen + + + Go one move forward + Einen Zug vorwärts gehen + + + Go ten moves forward + Zehn Züge vorwärts gehen + + + Go to end of moves + Zum Ende der Züge gehen + + + Go to previous variation + Zur vorherigen Variante gehen + + + Go to next variation + Zur nächsten Variante gehen + + + Abort game analysis + Spielanalyse abbrechen + + + Abort computer move + Computer-Zug abbrechen + + + Undo move + Tooltip for Undo button + Zug rückgängig + + + diff --git a/src/pentobi/qml/i18n/qml_en.ts b/src/pentobi/qml/i18n/qml_en.ts new file mode 100644 index 0000000..476e945 --- /dev/null +++ b/src/pentobi/qml/i18n/qml_en.ts @@ -0,0 +1,1650 @@ + + + + + AboutDialog + + Copyright © 2011–%1 Markus Enzenberger + + + + Computer opponent for the board game Blokus + + + + Pentobi %1 + The argument is the application version. + + + + + AnalyzeDialog + + Analysis speed: + + + + Fast + + + + Normal + + + + Slow + + + + + AnalyzeGame + + (No analysis) + + + + + AppearanceDialog + + Coordinates + + + + Show variations + + + + Light + + + + Dark + + + + Colorblind light + + + + Colorblind dark + + + + System + Name of theme using default system colors + + + + Move marking: + + + + Last with dot + + + + Last with number + + + + All with number + + + + None + Move marking/None + + + + Animations + + + + Show comment: + + + + Always + Show-comment mode + + + + As needed + Show-comment mode + + + + Never + Show-comment mode + + + + Color theme: + + + + Move number + Check box in appearance dialog whether to show the move number in the status bar. + + + + + AsciiArtSaveDialog + + Export ASCII Art + + + + Text files + + + + + BoardContextMenu + + Go to Move %1 + + + + Move Annotation + + + + Move Annotation (%1) + The argument is the annotation symbol for the current move + + + + + ButtonApply + + Apply + + + + + ButtonCancel + + Cancel + + + + + ButtonClose + + Close + + + + + ButtonOk + + OK + + + + + ComputerDialog + + Computer plays: + + + + Blue/Red + + + + Purple + + + + Green + + + + Blue + + + + Yellow/Green + + + + Orange + + + + Yellow + + + + Red + + + + Level %1 + + + + + ExportImageDialog + + Image width: + + + + + FileDialog + + Overwrite file? + + + + Open + + + + Save + + + + All files + + + + + GameDisplayDesktop + + Computer is thinking… + + + + Running game analysis… + + + + Computer is thinking… (up to %1 seconds remaining) + + + + Computer is thinking… (up to %1 minutes remaining) + + + + + GameInfoDialog + + Player Blue/Red: + + + + Player Purple: + + + + Player Green: + + + + Player Blue: + + + + Player Yellow/Green: + + + + Player Orange: + + + + Player Yellow: + + + + Player Red: + + + + Date: + + + + Time: + + + + Event: + + + + Round: + + + + + GameModel + + Blue/Red + + + + Purple + + + + Green + + + + Blue + + + + Yellow/Green + + + + Orange + + + + Yellow + + + + Red + + + + Purple wins with 1 point. + + + + Purple wins with %L1 points. + + + + Orange wins with 1 point. + + + + Orange wins with %L1 points. + + + + Game ends in a tie. + + + + Green wins with 1 point. + + + + Green wins with %L1 points. + + + + Blue wins with 1 point. + + + + Blue wins with %L1 points. + + + + Green wins (tie resolved). + + + + Blue/Red wins with 1 point. + + + + Blue/Red wins with %L1 points. + + + + Yellow/Green wins with 1 point. + + + + Yellow/Green wins with %L1 points. + + + + Yellow/Green wins (tie resolved). + + + + Blue wins. + + + + Yellow wins. + + + + Red wins. + + + + Red wins (tie resolved). + + + + Yellow wins (tie resolved). + + + + Game ends in a tie between Blue and Yellow. + + + + Game ends in a tie between Blue and Red. + + + + Game ends in a tie between Yellow and Red. + + + + Game ends in a tie between all players. + + + + Green wins. + + + + Game ends in a tie between Blue, Yellow and Red. + + + + Game ends in a tie between Blue, Yellow and Green. + + + + Game ends in a tie between Blue, Red and Green. + + + + Game ends in a tie between Yellow, Red and Green. + + + + Invalid Blokus SGF file. (%1) + + + + Clipboard is empty. + + + + Untitled + + + + Untitled %1 + The argument is a number, which will be increased if a file with the same name already exists + + + + New Folder + + + + New Folder %1 + The argument is a number, which will be increased if a folder with the same name already exists + + + + (Setup) + + + + (No moves) + + + + Move %1 + The argument is the current move number. + + + + Unsupported character set + + + + + GameVariantDialog + + Classic + + + + Duo + + + + Junior + + + + Trigon + + + + Nexos + + + + GembloQ + + + + Callisto + + + + Players: + + + + Colors: + + + + + GotoMoveDialog + + Move number: + + + + + HelpWindow + + Pentobi Help + + + + + ImageSaveDialog + + Save Image + + + + PNG image files + + + + JPEG image files + + + + + InitialRatingDialog + + Initialize your rating for this game variant. + + + + Initial rating: + + + + Beginner + + + + Expert + + + + + Main + + Pentobi + Window title if no file is loaded. + + + + Game analysis is only possible in main variation. + + + + Autosaved game was changed by another instance of Pentobi. Overwrite? + + + + Your rating has increased from %1 to %2. + + + + Your rating has decreased from %1 to %2. + + + + Your rating stays at %1. + + + + No permission to access storage + + + + Delete all rating information for the current game variant? + + + + Delete all variations? + + + + Save failed. + + + + End of tree was reached. Continue search from start of the tree? + + + + No comment found + + + + %1 (modified) + + + + File has been modified by another application. Reload? + + + + Continue computer move? + + + + Keep only position? + + + + Keep only subtree? + + + + Open failed. + + + + Start rated game with Purple against Pentobi level %1? + + + + Start rated game with Green against Pentobi level %1? + + + + Start rated game with Blue/Red against Pentobi level %1? + + + + Start rated game with Blue against Pentobi level %1? + + + + Start rated game with Orange against Pentobi level %1? + + + + Start rated game with Yellow/Green against Pentobi level %1? + + + + Start rated game with Yellow against Pentobi level %1? + + + + Start rated game with Red against Pentobi level %1? + + + + You have not yet played rated games in this game variant. + + + + Truncate this subtree? + + + + Truncate children? + + + + Discard game? + + + + Pentobi %1 (level %2) + Player name for game info in rated game. First argument is version of Pentobi, second argument is level. + + + + Human + Player name for game info in rated game. + + + + Rated game + + + + File has been modified by another application. Overwrite? + + + + %1 - Pentobi + Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified. + + + + Not enough memory + + + + Game analysis aborted + + + + Computer move aborted + + + + Rating information deleted + + + + Variations deleted + + + + File saved + + + + Saving image failed or unsupported image format + + + + Image saved + + + + Creating image failed + + + + Continuing rated game + + + + Kept only position + + + + Kept only subtree + + + + Variation is now %1 + + + + Children truncated + + + + Setup + Small-screen label for setup mode (short for "Setup Mode"). + + + + Setup Mode + + + + Rated + Label for ongoing rated game + + + + Rated %1 + Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number. + + + + Rated Game %1 + Label for rated game. The argument is the game number. + + + + Main Variation + + + + Beginning of Branch + + + + Comment + + + + Settings + Menu item Computer/Settings + + + + Find Move + + + + Next Comment + + + + Fullscreen + + + + Game Info + + + + Move Number… + + + + Pentobi Help + + + + New + + + + Rated Game + + + + Open… + + + + Play + + + + Play Move + Play a single move + + + + Quit + + + + Save + + + + Save As… + + + + Stop + + + + Undo Move + + + + + MenuComputer + + Computer + + + + C + Mnemonic for menu Computer. Leave empty for no mnemonic. + + + + S + Mnemonic for menu item Computer Settings. Leave empty for no mnemonic. + + + + P + Mnemonic for menu item Play. Leave empty for no mnemonic. + + + + M + Mnemonic for menu item Play Move. Leave empty for no mnemonic. + + + + O + Mnemonic for menu item Stop. Leave empty for no mnemonic. + + + + + MenuEdit + + Edit + + + + E + Mnemonic for menu Edit. Leave empty for no mnemonic. + + + + M + Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic. + + + + Make Main Variation + + + + Variation Up + Short for Move Variation Up + + + + U + Mnemonic for menu item Variation Up. Leave empty for no mnemonic. + + + + Variation Down + Short for Move Variation Down + + + + W + Mnemonic for menu item Variation Down. Leave empty for no mnemonic. + + + + Delete Variations + + + + D + Mnemonic for menu item Delete Variations. Leave empty for no mnemonic. + + + + Truncate + + + + T + Mnemonic for menu item Truncate. Leave empty for no mnemonic. + + + + Truncate Children + + + + C + Mnemonic for menu item Truncate Children. Leave empty for no mnemonic. + + + + Keep Position + + + + P + Mnemonic for menu item Keep Position. Leave empty for no mnemonic. + + + + Keep Subtree + + + + S + Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic. + + + + Setup Mode + + + + O + Mnemonic for menu item Setup Mode. Leave empty for no mnemonic. + + + + Next Color + + + + N + Mnemonic for menu item Next Color. Leave empty for no mnemonic. + + + + Annotation… + + + + A + Mnemonic for menu item Annotation. Leave empty for no mnemonic. + + + + Made main variation + + + + + MenuExport + + Export + + + + E + Mnemonic for menu Export. Leave empty for no mnemonic. + + + + M + Mnemonic for menu item Image. Leave empty for no mnemonic. + + + + A + Mnemonic for menu item ASCII Art. Leave empty for no mnemonic. + + + + Image… + + + + ASCII Art… + + + + + MenuGame + + Game + + + + G + Mnemonic for menu Game. Leave empty for no mnemonic. + + + + N + Mnemonic for menu item New. Leave empty for no mnemonic. + + + + R + Mnemonic for menu item Rated Game. Leave empty for no mnemonic. + + + + Game Variant… + + + + V + Mnemonic for menu item Game Variant. Leave empty for no mnemonic. + + + + I + Mnemonic for menu item Game Info. Leave empty for no mnemonic. + + + + U + Mnemonic for menu item Undo. Leave empty for no mnemonic. + + + + F + Mnemonic for menu item Find Move. Leave empty for no mnemonic. + + + + O + Mnemonic for menu item Open. Leave empty for no mnemonic. + + + + Open Clipboard + + + + C + Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic. + + + + S + Mnemonic for menu item Save. Leave empty for no mnemonic. + + + + A + Mnemonic for menu item Save As. Leave empty for no mnemonic. + + + + Q + Mnemonic for menu item Quit. Leave empty for no mnemonic. + + + + + MenuGo + + Go + + + + O + Mnemonic for menu Go. Leave empty for no mnemonic. + + + + N + Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic. + + + + M + Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic. + + + + B + Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic. + + + + C + Mnemonic for menu item Next Comment. Leave empty for no mnemonic. + + + + + MenuHelp + + Help + + + + H + Mnemonic for menu Help. Leave empty for no mnemonic. + + + + P + Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic. + + + + About Pentobi + + + + A + Mnemonic for menu item About Pentobi. Leave empty for no mnemonic. + + + + Report Bug + + + + B + Mnemonic for menu item Report Bug. Leave empty for no mnemonic. + + + + + MenuItem + + Ctrl + Shortcut modifier key as displayed in menu item text (abbreviate if long) + + + + Shift + Shortcut modifier key as displayed in menu item text (abbreviate if long) + + + + + MenuRecentFiles + + Open Recent + + + + P + Mnemonic for menu Open Recent. Leave empty for no mnemonic. + + + + %1. %2 + Format in recent files menu. First argument is the file number, second argument the file name. + + + + Clear List + Menu item for clearing the recent files list + + + + C + Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic. + + + + + MenuTools + + Tools + + + + T + Mnemonic for menu Tools. Leave empty for no mnemonic. + + + + Rating + + + + R + Mnemonic for menu item Rating. Leave empty for no mnemonic. + + + + Clear Rating + + + + C + Mnemonic for menu item Clear Rating. Leave empty for no mnemonic. + + + + Analyze Game + + + + A + Mnemonic for menu item Analyze Game. Leave empty for no mnemonic. + + + + Clear Analysis + + + + E + Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic. + + + + Analyze Game… + + + + + MenuView + + View + + + + V + Mnemonic for menu View. Leave empty for no mnemonic. + + + + A + Mnemonic for menu Appearance. Leave empty for no mnemonic. + + + + F + Mnemonic for menu item Fullscreen. Leave empty for no mnemonic. + + + + C + Mnemonic for menu item View/Comment. Leave empty for no mnemonic. + + + + Appearance + + + + Toolbar + + + + T + Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic. + + + + + MoveAnnotationDialog + + Move %1 + + + + Very good + + + + Good + + + + Interesting + + + + Doubtful + + + + Bad + + + + Very Bad + + + + No annotation + + + + + NewFolderDialog + + Folder name: + + + + + OpenDialog + + Open + + + + Blokus games + + + + + RatingDialog + + Your rating: + + + + Game variant: + + + + Classic (2) + Short for Classic (2 players) + + + + Classic (3) + Short for Classic (3 players) + + + + Classic (4) + Short for Classic (4 players) + + + + Duo + + + + Junior + + + + Trigon (2) + Short for Trigon (2 players) + + + + Trigon (3) + Short for Trigon (3 players) + + + + Trigon (4) + Short for Trigon (4 players) + + + + Nexos (2) + Short for Nexos (2 players) + + + + Nexos (4) + Short for Nexos (4 players) + + + + Callisto (2) + Short for Callisto (2 players, 2 colors) + + + + Callisto (2/4) + Short for Callisto (2 players, 4 colors) + + + + Callisto (3) + Short for Callisto (3 players) + + + + Callisto (4) + Short for Callisto (4 players) + + + + GembloQ (4) + Short for GembloQ (4 players) + + + + GembloQ (2) + Short for GembloQ (2 players, 2 colors) + + + + GembloQ (2/4) + Short for GembloQ (2 players, 4 colors) + + + + GembloQ (3) + Short for GembloQ (3 players) + + + + Rated games: + + + + Best previous rating: + + + + Recent development: + + + + Game + + + + Result + + + + Win + Result of rated game is a win + + + + Loss + Result of rated game is a loss + + + + Tie + 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. + + + + Level + + + + Your Color + + + + Date + + + + Open Game %1 + + + + + SaveDialog + + Save + + + + Blokus games + + + + + ToolBar + + Start a new game + + + + Start a rated game + + + + Set the colors played by the computer + + + + Make the computer continue to play the current color + + + + Make the computer play the current color + + + + Go to beginning of game + + + + Go ten moves backward + + + + Go one move backward + + + + Go one move forward + + + + Go ten moves forward + + + + Go to end of moves + + + + Go to previous variation + + + + Go to next variation + + + + Abort game analysis + + + + Abort computer move + + + + Undo move + Tooltip for Undo button + + + + diff --git a/src/pentobi/qml/i18n/qml_fr.ts b/src/pentobi/qml/i18n/qml_fr.ts new file mode 100644 index 0000000..2998d71 --- /dev/null +++ b/src/pentobi/qml/i18n/qml_fr.ts @@ -0,0 +1,1744 @@ + + + + + AboutDialog + + Copyright © 2011–%1 Markus Enzenberger + Copyright © 2011–%1 Markus Enzenberger + + + Computer opponent for the board game Blokus + Un adversaire d’ordinateur pour le jeu Blokus + + + Pentobi %1 + The argument is the application version. + Pentobi %1 + + + + Actions + + Main Variation + Variation principale + + + Beginning of Branch + Au début de la branche + + + Settings… + Menu item Computer/Settings + Configuration… + + + Find Move + Trouver un coup + + + Next Comment + Commentaire suivant + + + Fullscreen + Plein écran + + + Move Number… + Numéro de coup… + + + Pentobi Help + Aide de Pentobi + + + New + Nouveau + + + Rated Game + Partie classée + + + Open… + Ouvrir… + + + Play + Jouer + + + Play Move + Play a single move + Jouer un coup + + + Quit + Quitter + + + Save + Enregistrer + + + Save As… + Enregistrer sous… + + + Stop + Arrêter + + + Undo Move + Annuler le coup + + + Game Info + Info sur la partie + + + Comment + Commentaire + + + Settings + Menu item Computer/Settings + Configuration + + + + AnalyzeDialog + + Analysis speed: + Vitesse d’analyse : + + + Fast + Rapide + + + Normal + Normal + + + Slow + Lente + + + + AnalyzeGame + + (No analysis) + (Pas d’analyse) + + + + AppearanceDialog + + Coordinates + Coordonnées + + + Show variations + Afficher les variations + + + Light + Clair + + + Dark + Noir + + + Colorblind light + Daltonien clair + + + Colorblind dark + Daltonien noir + + + System + Name of theme using default system colors + Système + + + Move marking: + Marquage de coups : + + + Last with dot + Dernier avec point + + + Last with number + Dernier avec numéro + + + All with number + Tous avec numéro + + + None + Move marking/None + Aucune + + + Animations + Animations + + + Show comment: + Afficher le commentaire : + + + Always + Show-comment mode + Toujours + + + As needed + Show-comment mode + Comme requis + + + Never + Show-comment mode + Jamais + + + Color theme: + Thème de couleur : + + + Move number + Check box in appearance dialog whether to show the move number in the status bar. + Numéro de coup + + + + AsciiArtSaveDialog + + Export ASCII Art + Exporter art ASCII + + + Text files + Fichiers texte + + + + BoardContextMenu + + Go to Move %1 + Aller au coup %1 + + + Move Annotation + Annotation courante + + + Move Annotation (%1) + The argument is the annotation symbol for the current move + Annotation courante (%1) + + + + ButtonApply + + Apply + Appliquer + + + + ButtonCancel + + Cancel + Annuler + + + + ButtonClose + + Close + Fermer + + + + ButtonOk + + OK + OK + + + + ComputerDialog + + Computer plays: + L’ordinateur joue : + + + Blue/Red + Bleu/rouge + + + Purple + Violet + + + Green + Vert + + + Blue + Bleu + + + Yellow/Green + Jaune/vert + + + Orange + Orange + + + Yellow + Jaune + + + Red + Rouge + + + Level %1 + Niveau %1 + + + + ExportImageDialog + + Image width: + Largeur de l’image : + + + + FileDialog + + Overwrite file? + Remplacer le fichier ? + + + Open + Ouvrir + + + Save + Enregistrer + + + All files + Tous les fichiers + + + + GameDisplayDesktop + + Computer is thinking… + L’ordinateur pense… + + + Running game analysis… + Exécution de l’analyse de la partie… + + + Computer is thinking… (up to %1 seconds remaining) + L’ordinateur pense… (jusqu’à %1 secondes restantes) + + + Computer is thinking… (up to %1 minutes remaining) + L’ordinateur pense… (jusqu’à %1 minutes restantes) + + + + GameInfoDialog + + Player Blue/Red: + Joueur bleu/rouge : + + + Player Purple: + Joueur violet : + + + Player Green: + Joueur vert : + + + Player Blue: + Joueur bleu : + + + Player Yellow/Green: + Joueur jaune/vert : + + + Player Orange: + Joueur orange : + + + Player Yellow: + Joueur jaune : + + + Player Red: + Joueur rouge : + + + Date: + Date : + + + Time: + Temps : + + + Event: + Événement : + + + Round: + Round : + + + + GameModel + + Blue/Red + Bleu/rouge + + + Purple + Violet + + + Green + Vert + + + Blue + Bleu + + + Yellow/Green + Jaune/vert + + + Orange + Orange + + + Yellow + Jaune + + + Red + Rouge + + + Purple wins with 1 point. + Violet gagne avec 1 point. + + + Purple wins with %L1 points. + Violet gagne avec %L1 points. + + + Orange wins with 1 point. + Orange gagne avec 1 point. + + + Orange wins with %L1 points. + Orange gagne avec %L1 points. + + + Game ends in a tie. + La partie se termine par une égalité. + + + Green wins with 1 point. + Vert gagne avec 1 point. + + + Green wins with %L1 points. + Vert gagne avec %L1 points. + + + Blue wins with 1 point. + Bleu gagne avec 1 point. + + + Blue wins with %L1 points. + Bleu gagne avec %L1 points. + + + Green wins (tie resolved). + Vert gagne (égalité résolue). + + + Blue/Red wins with 1 point. + Bleu/rouge gagne avec 1 point. + + + Blue/Red wins with %L1 points. + Bleu/rouge gagne avec %L1 points. + + + Yellow/Green wins with 1 point. + Jaune/vert gagne avec 1 point. + + + Yellow/Green wins with %L1 points. + Jaune/vert gagne avec %L1 points. + + + Yellow/Green wins (tie resolved). + Jaune/vert gagne (égalité résolue). + + + Blue wins. + Bleu gagne. + + + Yellow wins. + Jaune gagne. + + + Red wins. + Rouge gagne. + + + Red wins (tie resolved). + Rouge gagne (égalité résolue). + + + Yellow wins (tie resolved). + Jaune gagne (égalité résolue). + + + Game ends in a tie between Blue and Yellow. + La partie se termine par une égalité entre bleu et jaune. + + + Game ends in a tie between Blue and Red. + La partie se termine par une égalité entre bleu et rouge. + + + Game ends in a tie between Yellow and Red. + La partie se termine par une égalité entre jaune et rouge. + + + Game ends in a tie between all players. + La partie se termine par une égalité entre tous les joueurs. + + + Green wins. + Vert gagne. + + + Game ends in a tie between Blue, Yellow and Red. + La partie se termine par une égalité entre bleu, jaune et rouge. + + + Game ends in a tie between Blue, Yellow and Green. + La partie se termine par une égalité entre bleu, jaune et vert. + + + Game ends in a tie between Blue, Red and Green. + La partie se termine par une égalité entre bleu, rouge et vert. + + + Game ends in a tie between Yellow, Red and Green. + La partie se termine par une égalité entre jaune, rouge et vert. + + + Invalid Blokus SGF file. (%1) + Le fichier n’est pas un fichier Blokus SGF valable. (%1) + + + Clipboard is empty. + Le presse-papiers est vide. + + + Untitled + Sans nom + + + Untitled %1 + The argument is a number, which will be increased if a file with the same name already exists + Sans nom %1 + + + New Folder + Nouveau dossier + + + New Folder %1 + The argument is a number, which will be increased if a folder with the same name already exists + Nouveau dossier %1 + + + (Setup) + (Position) + + + (No moves) + (Pas de coups) + + + Move %1 + The argument is the current move number. + Coup %1 + + + Unsupported character set + Codage des caractères non supporté + + + + GameVariantDialog + + Classic + Classique + + + Duo + Duo + + + Junior + Junior + + + Trigon + Trigon + + + Nexos + Nexos + + + GembloQ + GembloQ + + + Callisto + Callisto + + + Players: + Joueurs : + + + Colors: + Couleurs : + + + + GotoMoveDialog + + Move number: + Numéro de coup : + + + + HelpWindow + + Pentobi Help + Aide de Pentobi + + + + ImageSaveDialog + + Save Image + Enregistrer l’image + + + PNG image files + Image PNG + + + JPEG image files + Image JPEG + + + + InitialRatingDialog + + Initialize your rating for this game variant. + Initialisez votre classement pour cette variante du jeu. + + + Initial rating: + Classement initial : + + + Beginner + Débutant + + + Expert + Expert + + + + Main + + Pentobi + Window title if no file is loaded. + Pentobi + + + Game analysis is only possible in main variation. + L’analyse de la partie n’est possible que dans la variation principale. + + + Autosaved game was changed by another instance of Pentobi. Overwrite? + Jeu sauvé automatiquement a été changé par une autre instance de Pentobi. Remplacer ? + + + Your rating has increased from %1 to %2. + Votre classement a augmenté de %1 à %2. + + + Your rating has decreased from %1 to %2. + Votre classement a diminué de %1 à %2. + + + Your rating stays at %1. + Votre classement reste à %1. + + + No permission to access storage + Aucune autorisation d’accès au stockage + + + Delete all rating information for the current game variant? + Supprimer toutes les informations de classement de l’actuelle variante du jeu ? + + + Delete all variations? + Détruire toutes les variations ? + + + Save failed. + Échec de l’enregistrement. + + + End of tree was reached. Continue search from start of the tree? + La fin de l’arbre est atteinte. Continuer la recherche depuis la racine ? + + + No comment found + Aucun commentaire trouvé + + + %1 (modified) + %1 (modifié) + + + File has been modified by another application. Reload? + Le fichier a été modifié par une autre application. Recharger ? + + + Continue computer move? + Coninuer le coup de l’ordinateur ? + + + Keep only position? + Garder seulement la position ? + + + Keep only subtree? + Garder seulement le sous-arbre ? + + + Open failed. + Échec de l’ouverture. + + + Start rated game with Purple against Pentobi level %1? + Commencer une partie classée avec violet contre Pentobi niveau %1 ? + + + Start rated game with Green against Pentobi level %1? + Commencer une partie classée avec vert contre Pentobi niveau %1 ? + + + Start rated game with Blue/Red against Pentobi level %1? + Commencer une partie classée avec bleu/rouge contre Pentobi niveau %1 ? + + + Start rated game with Blue against Pentobi level %1? + Commencer une partie classée avec bleu contre Pentobi niveau %1 ? + + + Start rated game with Orange against Pentobi level %1? + Commencer une partie classée avec orange contre Pentobi niveau %1 ? + + + Start rated game with Yellow/Green against Pentobi level %1? + Commencer une partie classée avec jaune/vert contre Pentobi niveau %1 ? + + + Start rated game with Yellow against Pentobi level %1? + Commencer une partie classée avec jaune contre Pentobi niveau %1 ? + + + Start rated game with Red against Pentobi level %1? + Commencer une partie classée avec rouge contre Pentobi niveau %1 ? + + + You have not yet played rated games in this game variant. + Vous n’avez pas encore joué des parties classées dans cette variante du jeu. + + + Truncate this subtree? + Élaguer la branche actuelle ? + + + Truncate children? + Élaguer les branches filles ? + + + Discard game? + Abandonner la partie ? + + + Pentobi %1 (level %2) + Player name for game info in rated game. First argument is version of Pentobi, second argument is level. + Pentobi %1 (niveau %2) + + + Human + Player name for game info in rated game. + Personne + + + Rated game + Partie classée + + + File has been modified by another application. Overwrite? + Le fichier a été modifié par une autre application. Remplacer ? + + + %1 - Pentobi + Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified. + %1 - Pentobi + + + Not enough memory + Mémoire insuffisante + + + Game analysis aborted + Analyse de la partie abandonnée + + + Computer move aborted + Coup de l'ordinateur abandonné + + + Rating information deleted + Informations de classement supprimées + + + Variations deleted + Variations supprimées + + + File saved + Fichier enregistré + + + Saving image failed or unsupported image format + Impossible d’enregistrer l’image ou format de l’image non pris en charge + + + Image saved + Image enregistrée + + + Creating image failed + La création d’image a échoué + + + Continuing rated game + Partie classée est continuée + + + Kept only position + Gardé seulement la position + + + Kept only subtree + Gardé seulement le sous-arbre + + + Variation is now %1 + La variation est maintenant %1 + + + Children truncated + Nœuds fils coupé + + + Setup + Small-screen label for setup mode (short for "Setup Mode"). + Position + + + Setup Mode + Position + + + Rated + Label for ongoing rated game + Classée + + + Rated %1 + Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number. + Classée %1 + + + Rated Game %1 + Label for rated game. The argument is the game number. + Partie classée %1 + + + Main Variation + Variation principale + + + Beginning of Branch + Au début de la branche + + + Comment + Commentaire + + + Settings + Menu item Computer/Settings + Configuration + + + Find Move + Trouver un coup + + + Next Comment + Commentaire suivant + + + Fullscreen + Plein écran + + + Game Info + Info sur la partie + + + Move Number… + Numéro de coup… + + + Pentobi Help + Aide de Pentobi + + + New + Nouveau + + + Rated Game + Partie classée + + + Open… + Ouvrir… + + + Play + Jouer + + + Play Move + Play a single move + Jouer un coup + + + Quit + Quitter + + + Save + Enregistrer + + + Save As… + Enregistrer sous… + + + Stop + Arrêter + + + Undo Move + Annuler le coup + + + + MenuComputer + + Computer + Ordinateur + + + C + Mnemonic for menu Computer. Leave empty for no mnemonic. + O + + + S + Mnemonic for menu item Computer Settings. Leave empty for no mnemonic. + C + + + P + Mnemonic for menu item Play. Leave empty for no mnemonic. + J + + + M + Mnemonic for menu item Play Move. Leave empty for no mnemonic. + P + + + O + Mnemonic for menu item Stop. Leave empty for no mnemonic. + A + + + + MenuEdit + + Edit + Édition + + + E + Mnemonic for menu Edit. Leave empty for no mnemonic. + N + + + M + Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic. + P + + + Make Main Variation + Choisir comme variation principale + + + Variation Up + Short for Move Variation Up + Variation vers le haut + + + U + Mnemonic for menu item Variation Up. Leave empty for no mnemonic. + H + + + Variation Down + Short for Move Variation Down + Variation vers le bas + + + W + Mnemonic for menu item Variation Down. Leave empty for no mnemonic. + B + + + Delete Variations + Détruire les variations + + + D + Mnemonic for menu item Delete Variations. Leave empty for no mnemonic. + D + + + Truncate + Couper + + + T + Mnemonic for menu item Truncate. Leave empty for no mnemonic. + C + + + Truncate Children + Couper les branches filles + + + C + Mnemonic for menu item Truncate Children. Leave empty for no mnemonic. + F + + + Keep Position + Garder la position + + + P + Mnemonic for menu item Keep Position. Leave empty for no mnemonic. + G + + + Keep Subtree + Garder le sous-arbre + + + S + Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic. + S + + + Setup Mode + Position + + + O + Mnemonic for menu item Setup Mode. Leave empty for no mnemonic. + O + + + Next Color + Couleur suivante + + + N + Mnemonic for menu item Next Color. Leave empty for no mnemonic. + N + + + Annotation… + Annotation… + + + A + Mnemonic for menu item Annotation. Leave empty for no mnemonic. + A + + + Made main variation + Choisi comme variation principale + + + + MenuExport + + Export + Exporter + + + E + Mnemonic for menu Export. Leave empty for no mnemonic. + E + + + M + Mnemonic for menu item Image. Leave empty for no mnemonic. + I + + + A + Mnemonic for menu item ASCII Art. Leave empty for no mnemonic. + A + + + Image… + Image… + + + ASCII Art… + Art ASCII… + + + + MenuGame + + Game + Partie + + + G + Mnemonic for menu Game. Leave empty for no mnemonic. + P + + + N + Mnemonic for menu item New. Leave empty for no mnemonic. + N + + + R + Mnemonic for menu item Rated Game. Leave empty for no mnemonic. + C + + + Game Variant… + Variante du jeu… + + + V + Mnemonic for menu item Game Variant. Leave empty for no mnemonic. + V + + + I + Mnemonic for menu item Game Info. Leave empty for no mnemonic. + I + + + U + Mnemonic for menu item Undo. Leave empty for no mnemonic. + A + + + F + Mnemonic for menu item Find Move. Leave empty for no mnemonic. + T + + + O + Mnemonic for menu item Open. Leave empty for no mnemonic. + O + + + Open Clipboard + Ouvrir le presse-papiers + + + C + Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic. + P + + + S + Mnemonic for menu item Save. Leave empty for no mnemonic. + R + + + A + Mnemonic for menu item Save As. Leave empty for no mnemonic. + S + + + Q + Mnemonic for menu item Quit. Leave empty for no mnemonic. + Q + + + + MenuGo + + Go + Déplacement + + + O + Mnemonic for menu Go. Leave empty for no mnemonic. + D + + + N + Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic. + N + + + M + Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic. + P + + + B + Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic. + D + + + C + Mnemonic for menu item Next Comment. Leave empty for no mnemonic. + C + + + + MenuHelp + + Help + Aide + + + H + Mnemonic for menu Help. Leave empty for no mnemonic. + E + + + P + Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic. + A + + + About Pentobi + À propos de Pentobi + + + A + Mnemonic for menu item About Pentobi. Leave empty for no mnemonic. + P + + + Report Bug + Rapportez une erreur + + + B + Mnemonic for menu item Report Bug. Leave empty for no mnemonic. + R + + + + MenuItem + + Ctrl + Shortcut modifier key as displayed in menu item text (abbreviate if long) + Ctrl + + + Shift + Shortcut modifier key as displayed in menu item text (abbreviate if long) + Maj + + + + MenuRecentFiles + + Open Recent + Fichiers récents + + + P + Mnemonic for menu Open Recent. Leave empty for no mnemonic. + F + + + %1. %2 + Format in recent files menu. First argument is the file number, second argument the file name. + %1. %2 + + + Clear List + Menu item for clearing the recent files list + Effacer la liste + + + C + Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic. + E + + + + MenuTools + + Tools + Outils + + + T + Mnemonic for menu Tools. Leave empty for no mnemonic. + U + + + Rating + Classement + + + R + Mnemonic for menu item Rating. Leave empty for no mnemonic. + C + + + Clear Rating + Effacer le classement + + + C + Mnemonic for menu item Clear Rating. Leave empty for no mnemonic. + E + + + Analyze Game + Analyser la partie + + + A + Mnemonic for menu item Analyze Game. Leave empty for no mnemonic. + A + + + Clear Analysis + Effacer l’analyse + + + E + Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic. + N + + + Analyze Game… + Analyser la partie… + + + + MenuView + + View + Affichage + + + V + Mnemonic for menu View. Leave empty for no mnemonic. + A + + + Appearance… + Apparence… + + + A + Mnemonic for menu Appearance. Leave empty for no mnemonic. + E + + + F + Mnemonic for menu item Fullscreen. Leave empty for no mnemonic. + P + + + C + Mnemonic for menu item View/Comment. Leave empty for no mnemonic. + C + + + Appearance + Apparence + + + Toolbar + Barre d’outils + + + T + Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic. + B + + + + MoveAnnotationDialog + + Move %1 + Coup %1 + + + Very good + Très bon + + + Good + Bon + + + Interesting + Intéressant + + + Doubtful + Douteux + + + Bad + Mauvais + + + Very Bad + Très mauvais + + + No annotation + Aucune annotation + + + + NewFolderDialog + + Folder name: + Nom de dossier : + + + + OpenDialog + + Open + Ouvrir + + + Blokus games + Parties de Blokus + + + + RatingDialog + + Your rating: + Votre classement : + + + Game variant: + Variante du jeu : + + + Classic (2) + Short for Classic (2 players) + Classique (2) + + + Classic (3) + Short for Classic (3 players) + Classique (3) + + + Classic (4) + Short for Classic (4 players) + Classique (4) + + + Duo + Duo + + + Junior + Junior + + + Trigon (2) + Short for Trigon (2 players) + Trigon (2) + + + Trigon (3) + Short for Trigon (3 players) + Trigon (3) + + + Trigon (4) + Short for Trigon (4 players) + Trigon (4) + + + Nexos (2) + Short for Nexos (2 players) + Nexos (2) + + + Nexos (4) + Short for Nexos (4 players) + Nexos (4) + + + Callisto (2) + Short for Callisto (2 players, 2 colors) + Callisto (2) + + + Callisto (2/4) + Short for Callisto (2 players, 4 colors) + Callisto (2/4) + + + Callisto (3) + Short for Callisto (3 players) + Callisto (3) + + + Callisto (4) + Short for Callisto (4 players) + Callisto (4) + + + GembloQ (4) + Short for GembloQ (4 players) + GembloQ (4) + + + GembloQ (2) + Short for GembloQ (2 players, 2 colors) + GembloQ (2) + + + GembloQ (2/4) + Short for GembloQ (2 players, 4 colors) + GembloQ (2/4) + + + GembloQ (3) + Short for GembloQ (3 players) + GembloQ (3) + + + Rated games: + Parties classées : + + + Best previous rating: + Meilleur classement précédent : + + + Recent development: + Développement récent : + + + Game + Partie + + + Result + Résultat + + + Win + Result of rated game is a win + Victoire + + + Loss + Result of rated game is a loss + Perte + + + Tie + 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. + Egalité + + + Level + Niveau + + + Your Color + Votre couleur + + + Date + Date + + + Open Game %1 + Ouvrir la partie %1 + + + + SaveDialog + + Save + Enregistrer + + + Blokus games + Parties de Blokus + + + + ToolBar + + Start a new game + Commencer une nouvelle partie + + + Start a rated game + Commence une partie classée + + + Set the colors played by the computer + Définir les couleurs joué par l’ordinateur + + + Make the computer continue to play the current color + Faire que l’ordinateur continue à jouer la couleur actuelle + + + Make the computer play the current color + Faire que l’ordinateur joue la couleur actuelle + + + Go to beginning of game + Aller en début de partie + + + Go ten moves backward + Revenir dix coups en arrière + + + Go one move backward + Revenir un coup en arrière + + + Go one move forward + Avancer d’un coup + + + Go ten moves forward + Avancer de dix coups + + + Go to end of moves + Aller à la fin de coups + + + Go to previous variation + Aller à la variation précédente + + + Go to next variation + Aller à la variation suivante + + + Abort game analysis + Abandonner l'analyse + + + Abort computer move + Abandonner le coup de l'ordinateur + + + Undo move + Tooltip for Undo button + Annuler le coup + + + diff --git a/src/pentobi/qml/i18n/qml_nb_NO.ts b/src/pentobi/qml/i18n/qml_nb_NO.ts new file mode 100644 index 0000000..85e59ba --- /dev/null +++ b/src/pentobi/qml/i18n/qml_nb_NO.ts @@ -0,0 +1,1744 @@ + + + + + AboutDialog + + Copyright © 2011–%1 Markus Enzenberger + Copyright © 2011–%1 Markus Enzenberger + + + Computer opponent for the board game Blokus + Datamaskinmotstander for brettspillet Blokus + + + Pentobi %1 + The argument is the application version. + Pentobi %1 + + + + Actions + + Main Variation + Hovedvariasjon + + + Beginning of Branch + Begynnelse av forgreining + + + Settings… + Menu item Computer/Settings + Innstillinger + + + Find Move + Finn trekk + + + Next Comment + Neste kommentar + + + Fullscreen + Fullskjermsvisning + + + Move Number… + Trekknummer… + + + Pentobi Help + Pentobi-hjelp + + + New + Ny + + + Rated Game + Vurdert spill + + + Open… + Åpne… + + + Play + Spill + + + Play Move + Play a single move + Spill enkelt trekk + + + Quit + Avslutt + + + Save + Lagre + + + Save As… + Lagre som… + + + Stop + Stopp + + + Undo Move + Angre trekk + + + Game Info + Spillinfo + + + Comment + Kommentar + + + Settings + Menu item Computer/Settings + Innstillinger + + + + AnalyzeDialog + + Analysis speed: + Analysehastighet: + + + Fast + Rask + + + Normal + Normal + + + Slow + Treg + + + + AnalyzeGame + + (No analysis) + (Ingen analyse) + + + + AppearanceDialog + + Coordinates + Koordinater + + + Show variations + Vis variasjoner + + + Light + Lys + + + Dark + Mørk + + + Colorblind light + Fargeblind lys + + + Colorblind dark + Fargeblind mørk + + + System + Name of theme using default system colors + System + + + Move marking: + Flytt markering: + + + Last with dot + Siste med punkt + + + Last with number + Siste med nummer + + + All with number + Alle med nummer + + + None + Move marking/None + Ingen + + + Animations + Animasjoner + + + Show comment: + Vis kommentar: + + + Always + Show-comment mode + Alltid + + + As needed + Show-comment mode + Ved behov + + + Never + Show-comment mode + Aldri + + + Color theme: + Fargepalett: + + + Move number + Check box in appearance dialog whether to show the move number in the status bar. + Trekknummer + + + + AsciiArtSaveDialog + + Export ASCII Art + Eksporter ASCII-kunst + + + Text files + Tekstfiler + + + + BoardContextMenu + + Go to Move %1 + Gå til trekk %1 + + + Move Annotation + Flytt anmerkning + + + Move Annotation (%1) + The argument is the annotation symbol for the current move + Flytt anmerkning (%1) + + + + ButtonApply + + Apply + Bruk + + + + ButtonCancel + + Cancel + Avbryt + + + + ButtonClose + + Close + Lukk + + + + ButtonOk + + OK + OK + + + + ComputerDialog + + Computer plays: + Datamaskinen spiller: + + + Blue/Red + Blå/rød + + + Purple + Lilla + + + Green + Grønn + + + Blue + Blå + + + Yellow/Green + Gul/grønn + + + Orange + Oransje + + + Yellow + Gul + + + Red + Rød + + + Level %1 + Nivå %1 + + + + ExportImageDialog + + Image width: + Bildebredde: + + + + FileDialog + + Overwrite file? + Overskriv fil? + + + Open + Åpne + + + Save + Lagre + + + All files + Alle filer + + + + GameDisplayDesktop + + Computer is thinking… + Datamaskinen tenker… + + + Running game analysis… + Kjører spillanalyse… + + + Computer is thinking… (up to %1 seconds remaining) + Datamaskinen tenker… (opptil %1 sekunder gjenstår) + + + Computer is thinking… (up to %1 minutes remaining) + Datamaskinen tenker… (opptil %1 minutter gjenstår) + + + + GameInfoDialog + + Player Blue/Red: + Spiller blå/rød: + + + Player Purple: + Spiller lilla: + + + Player Green: + Spiller grønn: + + + Player Blue: + Spiller blå: + + + Player Yellow/Green: + Spiller gul/grønn: + + + Player Orange: + Spiller oransje: + + + Player Yellow: + Spiller gul: + + + Player Red: + Spiller rød: + + + Date: + Dato: + + + Time: + Tid: + + + Event: + Hendelse: + + + Round: + Runde: + + + + GameModel + + Blue/Red + Blå/rød + + + Purple + Lilla + + + Green + Grønn + + + Blue + Blå + + + Yellow/Green + Gul/grønn + + + Orange + Oransje + + + Yellow + Gul + + + Red + Rød + + + Purple wins with 1 point. + Lilla vinner med ett poeng. + + + Purple wins with %L1 points. + Lilla vinner med %L1 poeng. + + + Orange wins with 1 point. + Oransje vinner med ett poeng. + + + Orange wins with %L1 points. + Oransje vinner med %L1 poeng. + + + Game ends in a tie. + Spillet slutter uavgjort. + + + Green wins with 1 point. + Grønn vinner med ett poeng. + + + Green wins with %L1 points. + Grønn vinner med %L1 poeng. + + + Blue wins with 1 point. + Blå vinner med ett poeng. + + + Blue wins with %L1 points. + Blå vinner med %L1 poeng. + + + Green wins (tie resolved). + Grønn vinner (uavgjort tilstand løst). + + + Blue/Red wins with 1 point. + Blå/rød vinner med ett poeng. + + + Blue/Red wins with %L1 points. + Blå/rød vinner med %L1 poeng. + + + Yellow/Green wins with 1 point. + Gul/grønn vinnner med ett poeng. + + + Yellow/Green wins with %L1 points. + Gul/grønn vinner med %L1 poeng. + + + Yellow/Green wins (tie resolved). + Gul/grønn vinner (uavgjort tilstand løst). + + + Blue wins. + Blå vinner. + + + Yellow wins. + Gul vinner. + + + Red wins. + Rød vinner. + + + Red wins (tie resolved). + Rød vinner (uavgjort tilstand løst). + + + Yellow wins (tie resolved). + Gul vinner (uavgjirt tilstand løst). + + + Game ends in a tie between Blue and Yellow. + Spillet slutter uavgjort mellom blå og gul. + + + Game ends in a tie between Blue and Red. + Spillet slutter uavgjort mellom blå og rød. + + + Game ends in a tie between Yellow and Red. + Spillet slutter uavgjort mellom gul og rød. + + + Game ends in a tie between all players. + Spillet slutter uavgjort mellom alle spillere. + + + Green wins. + Grønn vinner. + + + Game ends in a tie between Blue, Yellow and Red. + Spillet slutter uavgjort mellom blå, gul og rød. + + + Game ends in a tie between Blue, Yellow and Green. + Spillet slutter uavgjort mellom blå, gul og grønn. + + + Game ends in a tie between Blue, Red and Green. + Spillet slutter uavgjort mellom blå, rød og grønn. + + + Game ends in a tie between Yellow, Red and Green. + Spillet slutter uavgjort mellom gul, rød og grønn. + + + Invalid Blokus SGF file. (%1) + Ugyldig Blokus SGF-fil. (%1) + + + Clipboard is empty. + Utklippstavlen er tom. + + + Untitled + Uten tittel + + + Untitled %1 + The argument is a number, which will be increased if a file with the same name already exists + Uten tittel %1 + + + New Folder + Ny mappe + + + New Folder %1 + The argument is a number, which will be increased if a folder with the same name already exists + Ny mappe %1 + + + (Setup) + (Oppsett) + + + (No moves) + (Ingen trekk) + + + Move %1 + The argument is the current move number. + Trekk %1 + + + Unsupported character set + Ustøttet tegnsett + + + + GameVariantDialog + + Classic + Klassisk + + + Duo + Duo + + + Junior + Junior + + + Trigon + Trigon + + + Nexos + Nexos + + + GembloQ + GembloQ + + + Callisto + Callisto + + + Players: + Spillere: + + + Colors: + Farger: + + + + GotoMoveDialog + + Move number: + Trekknummer: + + + + HelpWindow + + Pentobi Help + Pentobi-hjelp + + + + ImageSaveDialog + + Save Image + Lagre bilde + + + PNG image files + PNG-bildefiler + + + JPEG image files + JPEG-bildefiler + + + + InitialRatingDialog + + Initialize your rating for this game variant. + Hent din vurdering for denne spillvarianten. + + + Initial rating: + Startsvurdering: + + + Beginner + Begynner + + + Expert + Ekspert + + + + Main + + Pentobi + Window title if no file is loaded. + Pentobi + + + Game analysis is only possible in main variation. + Spillanalyse er kun tilgjengelig i hovedvariasjonen. + + + Autosaved game was changed by another instance of Pentobi. Overwrite? + Automatisk lagret spill ble endret av en annen kjørende utgave av Pentobi. Overskriv? + + + Your rating has increased from %1 to %2. + Din vurdering har økt fra %1 til %2. + + + Your rating has decreased from %1 to %2. + Din vurdering har sunket fra %1 til %2. + + + Your rating stays at %1. + Din vurdering forblir %1. + + + No permission to access storage + Mangler lagringstilgang + + + Delete all rating information for the current game variant? + Slett all vurderingsinformasjon fra nåværende spillvariant? + + + Delete all variations? + Slett alle variasjoner? + + + Save failed. + Lagring mislyktes. + + + End of tree was reached. Continue search from start of the tree? + Nådde slutten av treet. Fortsett søket fra starten av treet? + + + No comment found + Ingen kommentar funnet + + + %1 (modified) + %1 (endret) + + + File has been modified by another application. Reload? + Filen har blitt endret av et annet program. Last inn på nytt? + + + Continue computer move? + Fortsett datamaskintrekk? + + + Keep only position? + Kun behold posisjonen? + + + Keep only subtree? + Kun behold undertreet? + + + Open failed. + Åpning mislyktes. + + + Start rated game with Purple against Pentobi level %1? + Start vurdert spill med lilla mot Pentobi nivå %1? + + + Start rated game with Green against Pentobi level %1? + Start vurdert spill med grønn mot Pentobi nivå %1? + + + Start rated game with Blue/Red against Pentobi level %1? + Start vurdert spill med blå/rød mot Pentobi nivå %1? + + + Start rated game with Blue against Pentobi level %1? + Start vurdert spill med blå mot Pentobi nivå %1? + + + Start rated game with Orange against Pentobi level %1? + Start vurdert spill med oransje mot Pentobi nivå %1? + + + Start rated game with Yellow/Green against Pentobi level %1? + Start vurdert spill med gul/grønn mot Pentobi nivå %1? + + + Start rated game with Yellow against Pentobi level %1? + Start vurdert spill med gul mot Pentobi nivå %1? + + + Start rated game with Red against Pentobi level %1? + Start vurdert spill med rød mot Pentobi nivå %1? + + + You have not yet played rated games in this game variant. + Du har ikke spillt noen vurderte spill i denne varianten enda. + + + Truncate this subtree? + Forkort dette undertreet? + + + Truncate children? + Forkort underprosess? + + + Discard game? + Forkast spill? + + + Pentobi %1 (level %2) + Player name for game info in rated game. First argument is version of Pentobi, second argument is level. + Pentobi %1 (nivå %2) + + + Human + Player name for game info in rated game. + Menneske + + + Rated game + Vurdert spill + + + File has been modified by another application. Overwrite? + Filen har blitt endret av et annet program. Overskriv? + + + %1 - Pentobi + Window title if file is loaded. The argument is the file name prepended with a star if the file has been modified. + %1 - Pentobi + + + Not enough memory + Ikke nok minne + + + Game analysis aborted + Spillanalyse avbrutt + + + Computer move aborted + Datamaskintrekk avbrutt + + + Rating information deleted + Vurderingsinformasjon slettet + + + Variations deleted + Variasjoner slettet + + + File saved + Fil lagret + + + Saving image failed or unsupported image format + Lagring av bilde mislyktes, eller ustøttet bildeformat + + + Image saved + Bilde lagret + + + Creating image failed + Oppretting av bilde mislyktes + + + Continuing rated game + Fortsetter vurdert spill + + + Kept only position + Beholdt kun posisjonen + + + Kept only subtree + Beholdte kun undertre + + + Variation is now %1 + Varianten er nå %1 + + + Children truncated + Underprosess forkortet + + + Setup + Small-screen label for setup mode (short for "Setup Mode"). + Oppsett + + + Setup Mode + Oppsettsmodus + + + Rated + Label for ongoing rated game + Vurdert + + + Rated %1 + Small-screen label for finished rated game (short for "Rated Game"). The argument is the game number. + Vurdert %1 + + + Rated Game %1 + Label for rated game. The argument is the game number. + Vurdert spill %1 + + + Main Variation + Hovedvariasjon + + + Beginning of Branch + Begynnelse av forgreining + + + Comment + Kommentar + + + Settings + Menu item Computer/Settings + Innstillinger + + + Find Move + Finn trekk + + + Next Comment + Neste kommentar + + + Fullscreen + Fullskjermsvisning + + + Game Info + Spillinfo + + + Move Number… + Trekknummer… + + + Pentobi Help + Pentobi-hjelp + + + New + Ny + + + Rated Game + Vurdert spill + + + Open… + Åpne… + + + Play + Spill + + + Play Move + Play a single move + Spill enkelt trekk + + + Quit + Avslutt + + + Save + Lagre + + + Save As… + Lagre som… + + + Stop + Stopp + + + Undo Move + Angre trekk + + + + MenuComputer + + Computer + Datamaskin + + + C + Mnemonic for menu Computer. Leave empty for no mnemonic. + D + + + S + Mnemonic for menu item Computer Settings. Leave empty for no mnemonic. + L + + + P + Mnemonic for menu item Play. Leave empty for no mnemonic. + S + + + M + Mnemonic for menu item Play Move. Leave empty for no mnemonic. + E + + + O + Mnemonic for menu item Stop. Leave empty for no mnemonic. + O + + + + MenuEdit + + Edit + Rediger + + + E + Mnemonic for menu Edit. Leave empty for no mnemonic. + R + + + M + Mnemonic for menu item Make Main Variation. Leave empty for no mnemonic. + H + + + Make Main Variation + Gjør til hovedvariasjon + + + Variation Up + Short for Move Variation Up + Variasjon oppover + + + U + Mnemonic for menu item Variation Up. Leave empty for no mnemonic. + O + + + Variation Down + Short for Move Variation Down + Variasjon nedover + + + W + Mnemonic for menu item Variation Down. Leave empty for no mnemonic. + N + + + Delete Variations + Slett variasjoner + + + D + Mnemonic for menu item Delete Variations. Leave empty for no mnemonic. + S + + + Truncate + Forkort + + + T + Mnemonic for menu item Truncate. Leave empty for no mnemonic. + F + + + Truncate Children + Forkort underprosess + + + C + Mnemonic for menu item Truncate Children. Leave empty for no mnemonic. + U + + + Keep Position + Behold posisjon + + + P + Mnemonic for menu item Keep Position. Leave empty for no mnemonic. + B + + + Keep Subtree + Behold undertreet + + + S + Mnemonic for menu item Keep Subtree. Leave empty for no mnemonic. + T + + + Setup Mode + Oppsettsmodus + + + O + Mnemonic for menu item Setup Mode. Leave empty for no mnemonic. + M + + + Next Color + Neste farge + + + N + Mnemonic for menu item Next Color. Leave empty for no mnemonic. + E + + + Annotation… + Anmerkning… + + + A + Mnemonic for menu item Annotation. Leave empty for no mnemonic. + A + + + Made main variation + Gjort til hovedvariasjon + + + + MenuExport + + Export + Eksporter + + + E + Mnemonic for menu Export. Leave empty for no mnemonic. + E + + + M + Mnemonic for menu item Image. Leave empty for no mnemonic. + B + + + A + Mnemonic for menu item ASCII Art. Leave empty for no mnemonic. + A + + + Image… + Bilde… + + + ASCII Art… + ASCII-kunst… + + + + MenuGame + + Game + Spil + + + G + Mnemonic for menu Game. Leave empty for no mnemonic. + S + + + N + Mnemonic for menu item New. Leave empty for no mnemonic. + N + + + R + Mnemonic for menu item Rated Game. Leave empty for no mnemonic. + U + + + Game Variant… + Spillvariant… + + + V + Mnemonic for menu item Game Variant. Leave empty for no mnemonic. + V + + + I + Mnemonic for menu item Game Info. Leave empty for no mnemonic. + O + + + U + Mnemonic for menu item Undo. Leave empty for no mnemonic. + A + + + F + Mnemonic for menu item Find Move. Leave empty for no mnemonic. + F + + + O + Mnemonic for menu item Open. Leave empty for no mnemonic. + P + + + Open Clipboard + Åpne utklippstavle + + + C + Mnemonic for menu item Open Clipboard. Leave empty for no mnemonic. + K + + + S + Mnemonic for menu item Save. Leave empty for no mnemonic. + L + + + A + Mnemonic for menu item Save As. Leave empty for no mnemonic. + S + + + Q + Mnemonic for menu item Quit. Leave empty for no mnemonic. + T + + + + MenuGo + + Go + Gå + + + O + Mnemonic for menu Go. Leave empty for no mnemonic. + G + + + N + Mnemonic for menu item Go/Move Number. Leave empty for no mnemonic. + N + + + M + Mnemonic for menu item Go/Main Variation. Leave empty for no mnemonic. + H + + + B + Mnemonic for menu item Beginning Of Branch. Leave empty for no mnemonic. + F + + + C + Mnemonic for menu item Next Comment. Leave empty for no mnemonic. + K + + + + MenuHelp + + Help + Hjelp + + + H + Mnemonic for menu Help. Leave empty for no mnemonic. + H + + + P + Mnemonic for menu item Pentobi Help. Leave empty for no mnemonic. + P + + + About Pentobi + Om Pentobi + + + A + Mnemonic for menu item About Pentobi. Leave empty for no mnemonic. + O + + + Report Bug + Innrapporter feil + + + B + Mnemonic for menu item Report Bug. Leave empty for no mnemonic. + R + + + + MenuItem + + Ctrl + Shortcut modifier key as displayed in menu item text (abbreviate if long) + Ctrl + + + Shift + Shortcut modifier key as displayed in menu item text (abbreviate if long) + Shift + + + + MenuRecentFiles + + Open Recent + Åpne nylige + + + P + Mnemonic for menu Open Recent. Leave empty for no mnemonic. + Y + + + %1. %2 + Format in recent files menu. First argument is the file number, second argument the file name. + %1. %2 + + + Clear List + Menu item for clearing the recent files list + Tøm liste + + + C + Mnemonic for menu item Recent Files/Clear List. Leave empty for no mnemonic. + T + + + + MenuTools + + Tools + Verktøy + + + T + Mnemonic for menu Tools. Leave empty for no mnemonic. + E + + + Rating + Vurdering + + + R + Mnemonic for menu item Rating. Leave empty for no mnemonic. + U + + + Clear Rating + Fjern vurdering + + + C + Mnemonic for menu item Clear Rating. Leave empty for no mnemonic. + F + + + Analyze Game + Analyser spill + + + A + Mnemonic for menu item Analyze Game. Leave empty for no mnemonic. + A + + + Clear Analysis + Tøm analyse + + + E + Mnemonic for menu item Clear Analysis. Leave empty for no mnemonic. + N + + + Analyze Game… + Analyser spill… + + + + MenuView + + View + Vis + + + V + Mnemonic for menu View. Leave empty for no mnemonic. + V + + + Appearance… + Utseende… + + + A + Mnemonic for menu Appearance. Leave empty for no mnemonic. + U + + + F + Mnemonic for menu item Fullscreen. Leave empty for no mnemonic. + F + + + C + Mnemonic for menu item View/Comment. Leave empty for no mnemonic. + K + + + Appearance + Utseende + + + Toolbar + Verktøyslinje + + + T + Mnemonic for menu item View/Toolbar. Leave empty for no mnemonic. + E + + + + MoveAnnotationDialog + + Move %1 + Trekk %1 + + + Very good + Veldig god + + + Good + God + + + Interesting + Interessant + + + Doubtful + Tvilsom + + + Bad + Dårlig + + + Very Bad + Veldig dårlig + + + No annotation + Ingen tilknytning + + + + NewFolderDialog + + Folder name: + Mappenavn: + + + + OpenDialog + + Open + Åpne + + + Blokus games + Blokus-spill + + + + RatingDialog + + Your rating: + Din vurdering: + + + Game variant: + Spillvariant: + + + Classic (2) + Short for Classic (2 players) + Klassisk (2) + + + Classic (3) + Short for Classic (3 players) + Klassisk (3) + + + Classic (4) + Short for Classic (4 players) + Klassisk (4) + + + Duo + Duo + + + Junior + Junior + + + Trigon (2) + Short for Trigon (2 players) + Trigon (2) + + + Trigon (3) + Short for Trigon (3 players) + Trigon (3) + + + Trigon (4) + Short for Trigon (4 players) + Trigon (4) + + + Nexos (2) + Short for Nexos (2 players) + Nexos (2) + + + Nexos (4) + Short for Nexos (4 players) + Nexos (4) + + + Callisto (2) + Short for Callisto (2 players, 2 colors) + Callisto (2) + + + Callisto (2/4) + Short for Callisto (2 players, 4 colors) + Callisto (2/4) + + + Callisto (3) + Short for Callisto (3 players) + Callisto (3) + + + Callisto (4) + Short for Callisto (4 players) + Callisto (4) + + + GembloQ (4) + Short for GembloQ (4 players) + GembloQ (4) + + + GembloQ (2) + Short for GembloQ (2 players, 2 colors) + GembloQ (2) + + + GembloQ (2/4) + Short for GembloQ (2 players, 4 colors) + GembloQ (2/4) + + + GembloQ (3) + Short for GembloQ (3 players) + GembloQ (3) + + + Rated games: + Vurderte spill: + + + Best previous rating: + Beste tidligere vurdering: + + + Recent development: + Nylig utvikling: + + + Game + Spil + + + Result + Resultat + + + Win + Result of rated game is a win + Vunnet + + + Loss + Result of rated game is a loss + Tapt + + + Tie + 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. + Uavgjort + + + Level + Nivå + + + Your Color + Din farge + + + Date + Dato + + + Open Game %1 + Åpne spill %1 + + + + SaveDialog + + Save + Lagre + + + Blokus games + Blokus-spill + + + + ToolBar + + Start a new game + Start nytt spill + + + Start a rated game + Start vurdert spill + + + Set the colors played by the computer + Sett fargene spilt av datamaskinen + + + Make the computer continue to play the current color + Få datamaskinen til å fortsette å spille gjeldende farge + + + Make the computer play the current color + Få datamaskinen til å spille gjeldende farge + + + Go to beginning of game + Gå til begynnelsen av spillet + + + Go ten moves backward + Gå ti trekk tilbake + + + Go one move backward + Gå ett steg bakover + + + Go one move forward + Gå ett steg forover + + + Go ten moves forward + Gå ti trekk forover + + + Go to end of moves + Gå til trekkslutt + + + Go to previous variation + Gå til forrige variasjon + + + Go to next variation + Gå til neste variasjon + + + Abort game analysis + Avbryt spillanalyse + + + Abort computer move + Avbryt datamaskintrekk + + + Undo move + Tooltip for Undo button + Angre trekk + + + diff --git a/src/pentobi/qml/i18n/translations.qrc b/src/pentobi/qml/i18n/translations.qrc new file mode 100644 index 0000000..89f05df --- /dev/null +++ b/src/pentobi/qml/i18n/translations.qrc @@ -0,0 +1,7 @@ + + + qml_de.qm + qml_fr.qm + qml_nb_NO.qm + + diff --git a/src/pentobi/qml/icons/filedialog-folder.svg b/src/pentobi/qml/icons/filedialog-folder.svg new file mode 100644 index 0000000..c60410a --- /dev/null +++ b/src/pentobi/qml/icons/filedialog-folder.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/icons/filedialog-newfolder.svg b/src/pentobi/qml/icons/filedialog-newfolder.svg new file mode 100644 index 0000000..53239a8 --- /dev/null +++ b/src/pentobi/qml/icons/filedialog-newfolder.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/icons/filedialog-parent.svg b/src/pentobi/qml/icons/filedialog-parent.svg new file mode 100644 index 0000000..ae20c44 --- /dev/null +++ b/src/pentobi/qml/icons/filedialog-parent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/qml/themes/colorblind-dark/Theme.qml b/src/pentobi/qml/themes/colorblind-dark/Theme.qml new file mode 100644 index 0000000..961bc6a --- /dev/null +++ b/src/pentobi/qml/themes/colorblind-dark/Theme.qml @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------------- +/** @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" ] +} diff --git a/src/pentobi/qml/themes/colorblind-light/Theme.qml b/src/pentobi/qml/themes/colorblind-light/Theme.qml new file mode 100644 index 0000000..668e8c2 --- /dev/null +++ b/src/pentobi/qml/themes/colorblind-light/Theme.qml @@ -0,0 +1,16 @@ +//----------------------------------------------------------------------------- +/** @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" ] +} diff --git a/src/pentobi/qml/themes/dark/Theme.qml b/src/pentobi/qml/themes/dark/Theme.qml new file mode 100644 index 0000000..57c0c75 --- /dev/null +++ b/src/pentobi/qml/themes/dark/Theme.qml @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @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 } +} diff --git a/src/pentobi/qml/themes/dark/pentobi-rated-game.svg b/src/pentobi/qml/themes/dark/pentobi-rated-game.svg new file mode 100644 index 0000000..fca1d78 --- /dev/null +++ b/src/pentobi/qml/themes/dark/pentobi-rated-game.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg b/src/pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg new file mode 100644 index 0000000..bed72ba --- /dev/null +++ b/src/pentobi/qml/themes/dark/piece-manipulator-desktop-legal.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/dark/piece-manipulator-desktop.svg b/src/pentobi/qml/themes/dark/piece-manipulator-desktop.svg new file mode 100644 index 0000000..9b7e6fa --- /dev/null +++ b/src/pentobi/qml/themes/dark/piece-manipulator-desktop.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/dark/piece-manipulator-legal.svg b/src/pentobi/qml/themes/dark/piece-manipulator-legal.svg new file mode 100644 index 0000000..c72b7c3 --- /dev/null +++ b/src/pentobi/qml/themes/dark/piece-manipulator-legal.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/dark/piece-manipulator.svg b/src/pentobi/qml/themes/dark/piece-manipulator.svg new file mode 100644 index 0000000..d142c9b --- /dev/null +++ b/src/pentobi/qml/themes/dark/piece-manipulator.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/light/Theme.qml b/src/pentobi/qml/themes/light/Theme.qml new file mode 100644 index 0000000..07d61e7 --- /dev/null +++ b/src/pentobi/qml/themes/light/Theme.qml @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +/** @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" } +} diff --git a/src/pentobi/qml/themes/light/menu.svg b/src/pentobi/qml/themes/light/menu.svg new file mode 100644 index 0000000..93afa1b --- /dev/null +++ b/src/pentobi/qml/themes/light/menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-backward.svg b/src/pentobi/qml/themes/light/pentobi-backward.svg new file mode 100644 index 0000000..900de05 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-backward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-backward10.svg b/src/pentobi/qml/themes/light/pentobi-backward10.svg new file mode 100644 index 0000000..baaa638 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-backward10.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-beginning.svg b/src/pentobi/qml/themes/light/pentobi-beginning.svg new file mode 100644 index 0000000..e2cabdc --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-beginning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-computer-colors.svg b/src/pentobi/qml/themes/light/pentobi-computer-colors.svg new file mode 100644 index 0000000..8d1042f --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-computer-colors.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-end.svg b/src/pentobi/qml/themes/light/pentobi-end.svg new file mode 100644 index 0000000..29c61f4 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-end.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-forward.svg b/src/pentobi/qml/themes/light/pentobi-forward.svg new file mode 100644 index 0000000..9f319cd --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-forward10.svg b/src/pentobi/qml/themes/light/pentobi-forward10.svg new file mode 100644 index 0000000..7088c92 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-forward10.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-newgame.svg b/src/pentobi/qml/themes/light/pentobi-newgame.svg new file mode 100644 index 0000000..e18e8c9 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-newgame.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-next-variation.svg b/src/pentobi/qml/themes/light/pentobi-next-variation.svg new file mode 100644 index 0000000..a1b4279 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-next-variation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-play.svg b/src/pentobi/qml/themes/light/pentobi-play.svg new file mode 100644 index 0000000..5ec4380 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-play.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-previous-variation.svg b/src/pentobi/qml/themes/light/pentobi-previous-variation.svg new file mode 100644 index 0000000..dd264ab --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-previous-variation.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-rated-game.svg b/src/pentobi/qml/themes/light/pentobi-rated-game.svg new file mode 100644 index 0000000..1a126a0 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-rated-game.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-stop.svg b/src/pentobi/qml/themes/light/pentobi-stop.svg new file mode 100644 index 0000000..f4025c3 --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-stop.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/light/pentobi-undo.svg b/src/pentobi/qml/themes/light/pentobi-undo.svg new file mode 100644 index 0000000..faef7ae --- /dev/null +++ b/src/pentobi/qml/themes/light/pentobi-undo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg b/src/pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg new file mode 100644 index 0000000..f64faa0 --- /dev/null +++ b/src/pentobi/qml/themes/light/piece-manipulator-desktop-legal.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/light/piece-manipulator-desktop.svg b/src/pentobi/qml/themes/light/piece-manipulator-desktop.svg new file mode 100644 index 0000000..f471947 --- /dev/null +++ b/src/pentobi/qml/themes/light/piece-manipulator-desktop.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/light/piece-manipulator-legal.svg b/src/pentobi/qml/themes/light/piece-manipulator-legal.svg new file mode 100644 index 0000000..efcdc77 --- /dev/null +++ b/src/pentobi/qml/themes/light/piece-manipulator-legal.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/light/piece-manipulator.svg b/src/pentobi/qml/themes/light/piece-manipulator.svg new file mode 100644 index 0000000..dd42b88 --- /dev/null +++ b/src/pentobi/qml/themes/light/piece-manipulator.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/pentobi/qml/themes/system/Theme.qml b/src/pentobi/qml/themes/system/Theme.qml new file mode 100644 index 0000000..280ed68 --- /dev/null +++ b/src/pentobi/qml/themes/system/Theme.qml @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @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 } +} diff --git a/src/pentobi/qml/themes/themes.qrc b/src/pentobi/qml/themes/themes.qrc new file mode 100644 index 0000000..d8056b5 --- /dev/null +++ b/src/pentobi/qml/themes/themes.qrc @@ -0,0 +1,33 @@ + + +colorblind-dark/Theme.qml +colorblind-light/Theme.qml +dark/pentobi-rated-game.svg +dark/piece-manipulator-desktop.svg +dark/piece-manipulator-desktop-legal.svg +dark/piece-manipulator.svg +dark/piece-manipulator-legal.svg +dark/Theme.qml +light/menu.svg +light/pentobi-backward.svg +light/pentobi-backward10.svg +light/pentobi-beginning.svg +light/pentobi-computer-colors.svg +light/pentobi-end.svg +light/pentobi-forward.svg +light/pentobi-forward10.svg +light/pentobi-newgame.svg +light/pentobi-next-variation.svg +light/pentobi-play.svg +light/pentobi-previous-variation.svg +light/pentobi-rated-game.svg +light/pentobi-stop.svg +light/pentobi-undo.svg +light/piece-manipulator-desktop.svg +light/piece-manipulator-desktop-legal.svg +light/piece-manipulator.svg +light/piece-manipulator-legal.svg +light/Theme.qml +system/Theme.qml + + diff --git a/src/pentobi/resources.qrc b/src/pentobi/resources.qrc new file mode 100644 index 0000000..e6d284c --- /dev/null +++ b/src/pentobi/resources.qrc @@ -0,0 +1,81 @@ + + + qml/icons/filedialog-folder.svg + qml/icons/filedialog-newfolder.svg + qml/icons/filedialog-parent.svg + qml/AboutDialog.qml + qml/AnalyzeGame.qml + qml/AppearanceDialog.qml + qml/AsciiArtSaveDialog.qml + qml/Board.qml + qml/BoardContextMenu.qml + qml/BusyIndicator.qml + qml/Button.qml + qml/ButtonApply.qml + qml/ButtonCancel.qml + qml/ButtonClose.qml + qml/ButtonOk.qml + qml/ButtonToolTip.qml + qml/ComputerDialog.qml + qml/Comment.qml + qml/Controls.js + qml/Dialog.qml + qml/DialogButtonBox.qml + qml/DialogButtonBoxOkCancel.qml + qml/DialogLoader.qml + qml/ExportImageDialog.qml + qml/FatalMessage.qml + qml/FileDialog.qml + qml/GameDisplayMobile.qml + qml/GameInfoDialog.qml + qml/GameVariantDialog.qml + qml/GotoMoveDialog.qml + qml/HelpWindow.qml + qml/ImageSaveDialog.qml + qml/InitialRatingDialog.qml + qml/LineSegment.qml + qml/Main.qml + qml/MessageDialog.qml + qml/Menu.qml + qml/MenuComputer.qml + qml/MenuEdit.qml + qml/MenuExport.qml + qml/MenuGame.qml + qml/MenuGo.qml + qml/MenuHelp.qml + qml/MenuItem.qml + qml/MenuRecentFiles.qml + qml/MenuSeparator.qml + qml/MenuTools.qml + qml/MenuView.qml + qml/MoveAnnotationDialog.qml + qml/NavigationButtons.qml + qml/NavigationPanel.qml + qml/NewFolderDialog.qml + qml/OpenDialog.qml + qml/PieceCallisto.qml + qml/PieceClassic.qml + qml/PieceFlipAnimation.qml + qml/PieceGembloQ.qml + qml/PieceList.qml + qml/PieceManipulator.qml + qml/PieceNexos.qml + qml/PieceRotationAnimation.qml + qml/PieceSelectorMobile.qml + qml/PieceSwitchedFlipAnimation.qml + qml/PieceTrigon.qml + qml/QuarterSquare.qml + qml/QuestionDialog.qml + qml/RatingDialog.qml + qml/RatingGraph.qml + qml/SaveDialog.qml + qml/ScoreDisplay.qml + qml/ScoreElement.qml + qml/ScoreElement2.qml + qml/Square.qml + qml/ToolBar.qml + qml/Triangle.qml + qml/Main.js + qml/GameDisplay.js + + diff --git a/src/pentobi/resources_desktop.qrc b/src/pentobi/resources_desktop.qrc new file mode 100644 index 0000000..4aef039 --- /dev/null +++ b/src/pentobi/resources_desktop.qrc @@ -0,0 +1,7 @@ + + + qml/AnalyzeDialog.qml + qml/GameDisplayDesktop.qml + qml/PieceSelectorDesktop.qml + + diff --git a/src/pentobi_gtp/CMakeLists.txt b/src/pentobi_gtp/CMakeLists.txt new file mode 100644 index 0000000..9589f0d --- /dev/null +++ b/src/pentobi_gtp/CMakeLists.txt @@ -0,0 +1,10 @@ +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) + diff --git a/src/pentobi_gtp/Engine.cpp b/src/pentobi_gtp/Engine.cpp new file mode 100644 index 0000000..d863d27 --- /dev/null +++ b/src/pentobi_gtp/Engine.cpp @@ -0,0 +1,207 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Engine.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Engine.h" + +#include +#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 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(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(1)); + else if (name == "exploration_constant") + s.set_exploration_constant(args.parse(1)); + else if (name == "fixed_simulations") + p.set_fixed_simulations(args.parse(1)); + else if (name == "rave_child_max") + s.set_rave_child_max(args.parse(1)); + else if (name == "rave_parent_max") + s.set_rave_parent_max(args.parse(1)); + else if (name == "rave_weight") + s.set_rave_weight(args.parse(1)); + else if (name == "reuse_subtree") + s.set_reuse_subtree(args.parse(1)); + else if (name == "use_book") + p.set_use_book(args.parse(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(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(*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 diff --git a/src/pentobi_gtp/Engine.h b/src/pentobi_gtp/Engine.h new file mode 100644 index 0000000..1e4c66c --- /dev/null +++ b/src/pentobi_gtp/Engine.h @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------------- +/** @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 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 diff --git a/src/pentobi_gtp/Main.cpp b/src/pentobi_gtp/Main.cpp new file mode 100644 index 0000000..3b937b9 --- /dev/null +++ b/src/pentobi_gtp/Main.cpp @@ -0,0 +1,167 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_gtp/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#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 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("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("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("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; + } +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_kde_thumbnailer/CMakeLists.txt b/src/pentobi_kde_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..b07d1e4 --- /dev/null +++ b/src/pentobi_kde_thumbnailer/CMakeLists.txt @@ -0,0 +1,23 @@ +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}) diff --git a/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp new file mode 100644 index 0000000..f4f98de --- /dev/null +++ b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.cpp @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_kde_thumbnailer/PentobiThumbCreator.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "PentobiThumbCreator.h" + +#include +#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); +} + +//----------------------------------------------------------------------------- diff --git a/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h new file mode 100644 index 0000000..a0eb268 --- /dev/null +++ b/src/pentobi_kde_thumbnailer/PentobiThumbCreator.h @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +/** @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 + +//----------------------------------------------------------------------------- + +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 diff --git a/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop b/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop new file mode 100644 index 0000000..10cf91f --- /dev/null +++ b/src/pentobi_kde_thumbnailer/pentobi-thumbnail.desktop @@ -0,0 +1,11 @@ +[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 diff --git a/src/pentobi_thumbnailer/CMakeLists.txt b/src/pentobi_thumbnailer/CMakeLists.txt new file mode 100644 index 0000000..099f3cd --- /dev/null +++ b/src/pentobi_thumbnailer/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(pentobi-thumbnailer Main.cpp) + +target_link_libraries(pentobi-thumbnailer pentobi_thumbnail) + +install(TARGETS pentobi-thumbnailer DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/pentobi_thumbnailer/Main.cpp b/src/pentobi_thumbnailer/Main.cpp new file mode 100644 index 0000000..47fff58 --- /dev/null +++ b/src/pentobi_thumbnailer/Main.cpp @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +/** @file pentobi_thumbnailer/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include +#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 ."), + 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; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Analyze.cpp b/src/twogtp/Analyze.cpp new file mode 100644 index 0000000..4b4af3d --- /dev/null +++ b/src/twogtp/Analyze.cpp @@ -0,0 +1,134 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Analyze.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Analyze.h" + +#include +#include +#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> stat_result_player; + map 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'; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Analyze.h b/src/twogtp/Analyze.h new file mode 100644 index 0000000..2c70069 --- /dev/null +++ b/src/twogtp/Analyze.h @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +/** @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 + +using namespace std; + +//----------------------------------------------------------------------------- + +void analyze(const string& file); + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_ANALYZE_H diff --git a/src/twogtp/CMakeLists.txt b/src/twogtp/CMakeLists.txt new file mode 100644 index 0000000..ee744c7 --- /dev/null +++ b/src/twogtp/CMakeLists.txt @@ -0,0 +1,23 @@ +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 + ) + diff --git a/src/twogtp/FdStream.cpp b/src/twogtp/FdStream.cpp new file mode 100644 index 0000000..93928d0 --- /dev/null +++ b/src/twogtp/FdStream.cpp @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/FdStream.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "FdStream.h" + +#include +#include + +//----------------------------------------------------------------------------- + +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(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); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/FdStream.h b/src/twogtp/FdStream.h new file mode 100644 index 0000000..631bf5b --- /dev/null +++ b/src/twogtp/FdStream.h @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +/** @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 +#include + +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 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 diff --git a/src/twogtp/GtpConnection.cpp b/src/twogtp/GtpConnection.cpp new file mode 100644 index 0000000..d42675b --- /dev/null +++ b/src/twogtp/GtpConnection.cpp @@ -0,0 +1,166 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/GtpConnection.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "GtpConnection.h" + +#include +#include +#include +#include "FdStream.h" +#include "libboardgame_util/Log.h" + +//----------------------------------------------------------------------------- + +namespace { + +[[noreturn]] void terminate_child(const string& message) +{ + LIBBOARDGAME_LOG(message); + exit(1); +} + +vector split_args(const string& s) +{ + vector 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(fd2[0]); + m_out = make_unique(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 argv; + argv.reserve(args.size() + 1); + for (auto& a : args) + argv.push_back(const_cast(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(); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/GtpConnection.h b/src/twogtp/GtpConnection.h new file mode 100644 index 0000000..3c21150 --- /dev/null +++ b/src/twogtp/GtpConnection.h @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include + +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 m_in; + + unique_ptr m_out; +}; + +//----------------------------------------------------------------------------- + +#endif // TWOGTP_GTP_CONNECTION_H diff --git a/src/twogtp/Main.cpp b/src/twogtp/Main.cpp new file mode 100644 index 0000000..a9bcf9b --- /dev/null +++ b/src/twogtp/Main.cpp @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Main.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#include +#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 result(0); + try + { + vector 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("nugames", 1); + auto nu_threads = opt.get("threads", 1); + auto variant_string = opt.get("game", "classic"); + auto save_interval = opt.get("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> 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(black, white, variant, + nu_games, output, quiet, + log_prefix, fast_open); + twogtp->set_save_interval(save_interval); + twogtps.push_back(twogtp); + } + vector 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; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Output.cpp b/src/twogtp/Output.cpp new file mode 100644 index 0000000..66f95b7 --- /dev/null +++ b/src/twogtp/Output.cpp @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/Output.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "Output.h" + +#include +#include +#include +#include +#include +#include +#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& is_real_move) +{ + { + lock_guard 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 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 lock(m_mutex); + unsigned n = m_next; + do + ++m_next; + while (m_games.count(m_next) != 0); + return n; +} + +void Output::save() +{ + lock_guard 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"); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/Output.h b/src/twogtp/Output.h new file mode 100644 index 0000000..c5fe7c6 --- /dev/null +++ b/src/twogtp/Output.h @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------------- +/** @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 +#include +#include +#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& 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 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 diff --git a/src/twogtp/OutputTree.cpp b/src/twogtp/OutputTree.cpp new file mode 100644 index 0000000..b20dd8d --- /dev/null +++ b/src/twogtp/OutputTree.cpp @@ -0,0 +1,237 @@ +//----------------------------------------------------------------------------- +/** @file twogtp/OutputTree.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "OutputTree.h" + +#include +#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 count; + array avg_result; + array 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::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& s1, + ArrayList& 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 count; + array avg_result; + array 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& is_real_move) +{ + if (bd.has_setup()) + throw runtime_error("OutputTree: setup not supported"); + + // Find the canonical representation + ArrayList sequence; + for (auto& transform : m_transforms) + { + ArrayList 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 distribution(0, 1); + if (distribution(m_random) < 1.0 / sum) + { + play_real = true; + return; + } + auto random = static_cast(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(); +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/OutputTree.h b/src/twogtp/OutputTree.h new file mode 100644 index 0000000..c2736c4 --- /dev/null +++ b/src/twogtp/OutputTree.h @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +/** @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 +#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& is_real_move); + +private: + using PointTransform = libboardgame_base::PointTransform; + + + PentobiTree m_tree; + + vector> m_transforms; + + vector> 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 diff --git a/src/twogtp/TwoGtp.cpp b/src/twogtp/TwoGtp.cpp new file mode 100644 index 0000000..f8cc7b7 --- /dev/null +++ b/src/twogtp/TwoGtp.cpp @@ -0,0 +1,190 @@ +//----------------------------------------------------------------------------- +/** @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 points; + for (Color::IntType i = 0; i < m_bd.get_nu_colors(); ++i) + points[i] = m_bd.get_points(Color(i)); + array 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 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(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; +} + +//----------------------------------------------------------------------------- diff --git a/src/twogtp/TwoGtp.h b/src/twogtp/TwoGtp.h new file mode 100644 index 0000000..a233633 --- /dev/null +++ b/src/twogtp/TwoGtp.h @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------- +/** @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 +#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 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 diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt new file mode 100644 index 0000000..88fb251 --- /dev/null +++ b/src/unittest/CMakeLists.txt @@ -0,0 +1,10 @@ +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() diff --git a/src/unittest/libboardgame_base/CMakeLists.txt b/src/unittest/libboardgame_base/CMakeLists.txt new file mode 100644 index 0000000..6514b1c --- /dev/null +++ b/src/unittest/libboardgame_base/CMakeLists.txt @@ -0,0 +1,14 @@ +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) diff --git a/src/unittest/libboardgame_base/MarkerTest.cpp b/src/unittest/libboardgame_base/MarkerTest.cpp new file mode 100644 index 0000000..e580461 --- /dev/null +++ b/src/unittest/libboardgame_base/MarkerTest.cpp @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +/** @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; + +//----------------------------------------------------------------------------- + +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::digits > 32) + return; + Marker m; + m.setup_for_overflow_test(numeric_limits::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(); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/PointTransformTest.cpp b/src/unittest/libboardgame_base/PointTransformTest.cpp new file mode 100644 index 0000000..3ff2497 --- /dev/null +++ b/src/unittest/libboardgame_base/PointTransformTest.cpp @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +/** @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; + +//----------------------------------------------------------------------------- + +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 transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) == p); + } + { + libboardgame_base::PointTransfRot180 transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) + == geo.get_point(7, 6)); + } + { + libboardgame_base::PointTransfRot270Refl transform; + LIBBOARDGAME_CHECK(transform.get_transformed(p, geo) + == geo.get_point(2, 1)); + } +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/RatingTest.cpp b/src/unittest/libboardgame_base/RatingTest.cpp new file mode 100644 index 0000000..54bad28 --- /dev/null +++ b/src/unittest/libboardgame_base/RatingTest.cpp @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/RectGeometryTest.cpp b/src/unittest/libboardgame_base/RectGeometryTest.cpp new file mode 100644 index 0000000..5a12200 --- /dev/null +++ b/src/unittest/libboardgame_base/RectGeometryTest.cpp @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------------- +/** @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; +using RectGeometry = libboardgame_base::RectGeometry; +using PointList = libboardgame_util::ArrayList; + +//----------------------------------------------------------------------------- + +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))); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_base/StringRepTest.cpp b/src/unittest/libboardgame_base/StringRepTest.cpp new file mode 100644 index 0000000..5d43e0f --- /dev/null +++ b/src/unittest/libboardgame_base/StringRepTest.cpp @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------- +/** @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)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/ArgumentsTest.cpp b/src/unittest/libboardgame_gtp/ArgumentsTest.cpp new file mode 100644 index 0000000..641227c --- /dev/null +++ b/src/unittest/libboardgame_gtp/ArgumentsTest.cpp @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------------- +/** @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(0)); + } + { + CmdLine line("command 1"); + Arguments args(line); + LIBBOARDGAME_CHECK(args.parse(0)); + } + { + CmdLine line("command 2"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } + { + CmdLine line("command arg1"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } + { + CmdLine line("command"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + } +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_float) +{ + CmdLine line("command abc 5.5"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(0), Failure); + LIBBOARDGAME_CHECK_CLOSE(5.5f, args.parse(1), 1e-4f); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_int) +{ + CmdLine line("command 5 arg"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse(0)); + LIBBOARDGAME_CHECK_THROW(args.parse(1), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_min_int) +{ + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse_min(0, 3)); + LIBBOARDGAME_CHECK_THROW(args.parse_min(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(0, 3, 10)); + LIBBOARDGAME_CHECK_THROW(args.parse_min_max(0, 0, 4), Failure); + LIBBOARDGAME_CHECK_THROW(args.parse_min_max(0, 10, 20), Failure); +} + +LIBBOARDGAME_TEST_CASE(gtp_arguments_single_int) +{ + { + CmdLine line("command 5"); + Arguments args(line); + LIBBOARDGAME_CHECK_EQUAL(5, args.parse()); + } + { + CmdLine line("command 5 10"); + Arguments args(line); + LIBBOARDGAME_CHECK_THROW(args.parse(), 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))); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/CMakeLists.txt b/src/unittest/libboardgame_gtp/CMakeLists.txt new file mode 100644 index 0000000..2ccd7aa --- /dev/null +++ b/src/unittest/libboardgame_gtp/CMakeLists.txt @@ -0,0 +1,13 @@ +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) diff --git a/src/unittest/libboardgame_gtp/CmdLineTest.cpp b/src/unittest/libboardgame_gtp/CmdLineTest.cpp new file mode 100644 index 0000000..5a5e92f --- /dev/null +++ b/src/unittest/libboardgame_gtp/CmdLineTest.cpp @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +/** @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()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/EngineTest.cpp b/src/unittest/libboardgame_gtp/EngineTest.cpp new file mode 100644 index 0000000..771cfe3 --- /dev/null +++ b/src/unittest/libboardgame_gtp/EngineTest.cpp @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +/** @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)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_gtp/ResponseTest.cpp b/src/unittest/libboardgame_gtp/ResponseTest.cpp new file mode 100644 index 0000000..59d37ab --- /dev/null +++ b/src/unittest/libboardgame_gtp/ResponseTest.cpp @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +/** @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()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_mcts/CMakeLists.txt b/src/unittest/libboardgame_mcts/CMakeLists.txt new file mode 100644 index 0000000..9ac9b61 --- /dev/null +++ b/src/unittest/libboardgame_mcts/CMakeLists.txt @@ -0,0 +1,10 @@ +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) diff --git a/src/unittest/libboardgame_mcts/NodeTest.cpp b/src/unittest/libboardgame_mcts/NodeTest.cpp new file mode 100644 index 0000000..e994f8c --- /dev/null +++ b/src/unittest/libboardgame_mcts/NodeTest.cpp @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @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 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 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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/CMakeLists.txt b/src/unittest/libboardgame_sgf/CMakeLists.txt new file mode 100644 index 0000000..7f0ce5e --- /dev/null +++ b/src/unittest/libboardgame_sgf/CMakeLists.txt @@ -0,0 +1,13 @@ +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) diff --git a/src/unittest/libboardgame_sgf/SgfNodeTest.cpp b/src/unittest/libboardgame_sgf/SgfNodeTest.cpp new file mode 100644 index 0000000..e9cb37b --- /dev/null +++ b/src/unittest/libboardgame_sgf/SgfNodeTest.cpp @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/SgfNodeTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include +#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(); + 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(); + 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)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/SgfTreeTest.cpp b/src/unittest/libboardgame_sgf/SgfTreeTest.cpp new file mode 100644 index 0000000..5ce447c --- /dev/null +++ b/src/unittest/libboardgame_sgf/SgfTreeTest.cpp @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +/** @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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/SgfUtilTest.cpp b/src/unittest/libboardgame_sgf/SgfUtilTest.cpp new file mode 100644 index 0000000..e40815d --- /dev/null +++ b/src/unittest/libboardgame_sgf/SgfUtilTest.cpp @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +/** @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(); + auto& child = root->create_new_child(); + vector 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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_sgf/TreeReaderTest.cpp b/src/unittest/libboardgame_sgf/TreeReaderTest.cpp new file mode 100644 index 0000000..3569a85 --- /dev/null +++ b/src/unittest/libboardgame_sgf/TreeReaderTest.cpp @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libboardgame_sgf/TreeReaderTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "libboardgame_sgf/TreeReader.h" + +#include +#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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/ArrayListTest.cpp b/src/unittest/libboardgame_util/ArrayListTest.cpp new file mode 100644 index 0000000..fee8b80 --- /dev/null +++ b/src/unittest/libboardgame_util/ArrayListTest.cpp @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------- +/** @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 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 l1{ 1, 2, 3 }; + ArrayList l2{ 1, 2, 3 }; + LIBBOARDGAME_CHECK(l1 == l2); + l2.push_back(4); + LIBBOARDGAME_CHECK(! (l1 == l2)); + l2 = ArrayList({ 2, 1, 3 }); + LIBBOARDGAME_CHECK(! (l1 == l2)); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_pop_back) +{ + ArrayList l({ 5 }); + int i = l.pop_back(); + LIBBOARDGAME_CHECK_EQUAL(5, i); + LIBBOARDGAME_CHECK(l.empty()); +} + +LIBBOARDGAME_TEST_CASE(util_array_list_remove) +{ + ArrayList 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]); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/CMakeLists.txt b/src/unittest/libboardgame_util/CMakeLists.txt new file mode 100644 index 0000000..fcff0db --- /dev/null +++ b/src/unittest/libboardgame_util/CMakeLists.txt @@ -0,0 +1,13 @@ +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) diff --git a/src/unittest/libboardgame_util/OptionsTest.cpp b/src/unittest/libboardgame_util/OptionsTest.cpp new file mode 100644 index 0000000..339b07a --- /dev/null +++ b/src/unittest/libboardgame_util/OptionsTest.cpp @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------- +/** @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 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(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 specs = { "first:" }; + const char* argv[] = + { nullptr, "--first", "firstval", "--", "--arg1" }; + auto argc = static_cast(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 specs = { "first:" }; + const char* argv[] = { nullptr, "--first" }; + auto argc = static_cast(sizeof(argv) / sizeof(argv[0])); + LIBBOARDGAME_CHECK_THROW(Options opt(argc, argv, specs), runtime_error); +} + +LIBBOARDGAME_TEST_CASE(libboardgame_util_options_nospace) +{ + vector specs = { "first|a:", "second|b:" }; + const char* argv[] = { nullptr, "-abc" }; + auto argc = static_cast(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 specs = { "first|a", "second|b:" }; + const char* argv[] = { nullptr, "-ab", "c" }; + auto argc = static_cast(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 specs = { "first:", "second:" }; + const char* argv[] = { nullptr, "--first", "10", "--second", "foo" }; + auto argc = static_cast(sizeof(argv) / sizeof(argv[0])); + Options opt(argc, argv, specs); + LIBBOARDGAME_CHECK_EQUAL(opt.get("first"), 10); + LIBBOARDGAME_CHECK_THROW(opt.get("second"), runtime_error); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/StatisticsTest.cpp b/src/unittest/libboardgame_util/StatisticsTest.cpp new file mode 100644 index 0000000..32fcaeb --- /dev/null +++ b/src/unittest/libboardgame_util/StatisticsTest.cpp @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +/** @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 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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libboardgame_util/StringUtilTest.cpp b/src/unittest/libboardgame_util/StringUtilTest.cpp new file mode 100644 index 0000000..0ee52e9 --- /dev/null +++ b/src/unittest/libboardgame_util/StringUtilTest.cpp @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------------- +/** @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 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 v = split("", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 0u); + } + { + vector v = split("a,", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(v[0], "a"); + LIBBOARDGAME_CHECK_EQUAL(v[1], ""); + } + { + vector v = split(",a", ','); + LIBBOARDGAME_CHECK_EQUAL(v.size(), 2u); + LIBBOARDGAME_CHECK_EQUAL(v[0], ""); + LIBBOARDGAME_CHECK_EQUAL(v[1], "a"); + } + { + vector 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(""), ""); +} + +//---------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardConstTest.cpp b/src/unittest/libpentobi_base/BoardConstTest.cpp new file mode 100644 index 0000000..07960f5 --- /dev/null +++ b/src/unittest/libpentobi_base/BoardConstTest.cpp @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------------- +/** @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()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardTest.cpp b/src/unittest/libpentobi_base/BoardTest.cpp new file mode 100644 index 0000000..8852d84 --- /dev/null +++ b/src/unittest/libpentobi_base/BoardTest.cpp @@ -0,0 +1,165 @@ +//----------------------------------------------------------------------------- +/** @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(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(Variant::classic); + auto moves = make_unique(); + auto marker = make_unique(); + 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(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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/BoardUpdaterTest.cpp b/src/unittest/libpentobi_base/BoardUpdaterTest.cpp new file mode 100644 index 0000000..04b6286 --- /dev/null +++ b/src/unittest/libpentobi_base/BoardUpdaterTest.cpp @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------------- +/** @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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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)); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/CMakeLists.txt b/src/unittest/libpentobi_base/CMakeLists.txt new file mode 100644 index 0000000..c384d4c --- /dev/null +++ b/src/unittest/libpentobi_base/CMakeLists.txt @@ -0,0 +1,15 @@ +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) diff --git a/src/unittest/libpentobi_base/GameTest.cpp b/src/unittest/libpentobi_base/GameTest.cpp new file mode 100644 index 0000000..4bad9c7 --- /dev/null +++ b/src/unittest/libpentobi_base/GameTest.cpp @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +/** @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 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()); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/PentobiSgfUtilTest.cpp b/src/unittest/libpentobi_base/PentobiSgfUtilTest.cpp new file mode 100644 index 0000000..1ac8b04 --- /dev/null +++ b/src/unittest/libpentobi_base/PentobiSgfUtilTest.cpp @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/** @file unittest/libpentobi_base/PentobiSgfUtilTest.cpp + @author Markus Enzenberger + @copyright GNU General Public License version 3 or later */ +//----------------------------------------------------------------------------- + +#include "libpentobi_base/PentobiSgfUtil.h" + +#include +#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); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_base/PentobiTreeTest.cpp b/src/unittest/libpentobi_base/PentobiTreeTest.cpp new file mode 100644 index 0000000..6d8e6fc --- /dev/null +++ b/src/unittest/libpentobi_base/PentobiTreeTest.cpp @@ -0,0 +1,218 @@ +//----------------------------------------------------------------------------- +/** @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 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 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 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 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 root = reader.get_tree_transfer_ownership(); + LIBBOARDGAME_CHECK_THROW(PentobiTree tree(root), MissingProperty); +} + +//----------------------------------------------------------------------------- diff --git a/src/unittest/libpentobi_mcts/CMakeLists.txt b/src/unittest/libpentobi_mcts/CMakeLists.txt new file mode 100644 index 0000000..e560cbf --- /dev/null +++ b/src/unittest/libpentobi_mcts/CMakeLists.txt @@ -0,0 +1,10 @@ +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) diff --git a/src/unittest/libpentobi_mcts/SearchTest.cpp b/src/unittest/libpentobi_mcts/SearchTest.cpp new file mode 100644 index 0000000..24f42de --- /dev/null +++ b/src/unittest/libpentobi_mcts/SearchTest.cpp @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------------- +/** @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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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(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 root = reader.get_tree_transfer_ownership(); + PentobiTree tree(root); + auto bd = make_unique(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(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()); +} + +//-----------------------------------------------------------------------------