Repository: fohristiwhirl/nibbler Branch: master Commit: fb29b0467172 Files: 60 Total size: 883.4 KB Directory structure: gitextract_lqka34fb/ ├── LICENSE.md ├── README.md └── files/ ├── .editorconfig ├── .gitignore ├── misc/ │ ├── chess960.txt │ ├── escape_and_missing_quotes.pgn │ ├── nibbler_menu_translations_template.json │ ├── pathological.pgn │ ├── pgn.txt │ ├── scraps.js │ ├── state_logic.txt │ └── uci.txt ├── res/ │ └── linux/ │ └── nibbler.desktop ├── scripts/ │ ├── builder.py │ ├── install.sh │ └── install_mac.sh └── src/ ├── main.js ├── modules/ │ ├── alert_main.js │ ├── background.js │ ├── config_io.js │ ├── custom_uci.js │ ├── debork_json.js │ ├── empty.js │ ├── engineconfig_io.js │ ├── images.js │ ├── messages.js │ ├── running_as_electron.js │ ├── stringify.js │ ├── translate.js │ └── translations.js ├── nibbler.css ├── nibbler.html ├── package.json └── renderer/ ├── 10_globals.js ├── 20_utils.js ├── 30_point.js ├── 31_sliders.js ├── 40_position.js ├── 41_fen.js ├── 42_perft.js ├── 43_chess960.js ├── 50_table.js ├── 51_node.js ├── 52_sorted_moves.js ├── 55_winrate_graph.js ├── 60_pgn_utils.js ├── 61_pgn_parse.js ├── 63_polyglot.js ├── 65_loaders.js ├── 71_tree_handler.js ├── 72_tree_draw.js ├── 75_looker.js ├── 80_info.js ├── 81_arrows.js ├── 82_infobox.js ├── 83_statusbox.js ├── 90_engine.js ├── 95_hub.js ├── 97_drag.js └── 99_start.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE.md ================================================ ### 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 . ================================================ FILE: README.md ================================================ # Nibbler Nibbler is a real-time analysis GUI for [Leela Chess Zero](http://lczero.org/play/quickstart/) (Lc0), which runs Leela in the background and constantly displays opinions about the current position. You can also compel the engine to evaluate one or more specific moves. Nibbler is loosely inspired by [Lizzie](https://github.com/featurecat/lizzie) and [Sabaki](https://github.com/SabakiHQ/Sabaki). These days, Nibbler more-or-less works with traditional engines like [Stockfish](https://stockfishchess.org/), too. (Ensure `MultiPV` is `1`, `Threads` (CPU) is set, and `Hash` is set (more is better), for maximum strength.) For prebuilt binary releases, see the [Releases](https://github.com/rooklift/nibbler/releases) section. For help, the [Discord](https://discordapp.com/invite/pKujYxD) may be your best bet, or open an issue here. ![Screenshot](https://user-images.githubusercontent.com/16438795/270297798-a432ea17-3601-4143-bddb-97420a0d6e6c.png) ## Features * Display Leela's top choices graphically. * Winrate graph. * Optionally shows Leela statistics like N, P, Q, S, U, V, and WDL for each move. * UCI `searchmoves` functionality. * Automatic full-game analysis. * Play against Leela from any position. * Leela self-play from any position. * PGN loading via menu, clipboard, or drag-and-drop. * Supports PGN variations of arbitrary depth. * FEN loading. * Chess 960. ## Installation - Windows / Linux Some Windows and Linux standalone releases are uploaded to the [Releases](https://github.com/rooklift/nibbler/releases) section from time to time. *Alternatively*, it is possible to run Nibbler from source. This requires Electron, but has no other dependencies. If you have Electron installed (e.g. `npm install -g electron`) you can likely enter the `/src` directory, then do `electron .` to run it. Nibbler should be compatible with at least version 5 and above. You could also build a standalone app. See comments inside the Python script `builder.py` for info. ## Linux install script Linux users can make use of the following *one-liner* to install the latest version of Nibbler: ```bash curl -L https://raw.githubusercontent.com/rooklift/nibbler/master/files/scripts/install.sh | bash ``` ## Installation - Mac Mac builds have been made by [twoplan](https://github.com/twoplan/Nibbler-for-macOS) and [Jac-Zac](https://github.com/Jac-Zac/Nibbler_MacOS) and [Zamana](https://github.com/Zamana/nibbler) - the last of which is probably the most up-to-date. ## Mac install script Alternatively, MacOS users can run the following *one-liner* to assemble Nibbler locally. This (hopefully) removes any codesigning issues (Gatekeeper refusing to open unauthorized apps) by building Nibbler on-the-fly, though I can't test it myself: ```bash curl -L https://raw.githubusercontent.com/rooklift/nibbler/master/files/scripts/install_mac.sh | bash ``` ## Advanced engine options Most people won't need them, but all of Leela's engine options can be set in two ways: * Leela automatically loads options from a file called `lc0.config` at startup - see [here](https://lczero.org/play/configuration/flags/#config-file). * Nibbler will send UCI options specified in Nibbler's own `engines.json` file (which you can find via the Dev menu). ## Hints and tips An option to enable the UCI `searchmoves` feature is available in the Analysis menu. Once enabled, one or more moves can be specified as moves to focus on; Leela will ignore other moves. This is useful when you think Leela isn't giving a certain move enough attention. Leela forgets much of the evaluation if the position changes. To mitigate this, an option in the Analysis menu allows you to hover over a PV (on the right) and see it play out on the board, without changing the position we're actually analysing. You might prefer to halt Leela while doing this, so that the PVs don't change while you're looking at them. Leela running out of RAM can be a problem if searches go on too long. You might like to set a reasonable node limit (in the Engine menu), perhaps 10 million or so. ## Thanks Thanks to everyone in Discord and GitHub who's offered advice and suggestions; and thanks to all Lc0 devs and GPU-hours contributors! The pieces are from [Lichess](https://lichess.org/). Icon design by [ciriousjoker](https://github.com/ciriousjoker) based on [this](https://www.svgrepo.com/svg/155301/chess). ================================================ FILE: files/.editorconfig ================================================ [*] indent_style = tab indent_size = 4 trim_trailing_whitespace = true ================================================ FILE: files/.gitignore ================================================ .DS_Store scripts/dist scripts/electron_zipped scripts/update_my_installation.py ================================================ FILE: files/misc/chess960.txt ================================================ https://www.mark-weeks.com/cfaa/chess960/c960strt.htm I've verified Nibbler produces exactly the same results given a Chess960 number. 000 BBQNNRKR bbqnnrkr 959 RKRNNQBB 001 BQNBNRKR bqnbnrkr 955 RKRNBNQB 002 BQNNRBKR bqnnrbkr 951 RKBRNNQB 003 BQNNRKRB bqnnrkrb 947 BRKRNNQB 004 QBBNNRKR qbbnnrkr 958 RKRNNBBQ 005 QNBBNRKR qnbbnrkr 954 RKRNBBNQ 006 QNBNRBKR qnbnrbkr 950 RKBRNBNQ 007 QNBNRKRB qnbnrkrb 946 BRKRNBNQ 008 QBNNBRKR qbnnbrkr 957 RKRBNNBQ 009 QNNBBRKR qnnbbrkr 953 RKRBBNNQ 010 QNNRBBKR qnnrbbkr 949 RKBBRNNQ 011 QNNRBKRB qnnrbkrb 945 BRKBRNNQ 012 QBNNRKBR qbnnrkbr 956 RBKRNNBQ 013 QNNBRKBR qnnbrkbr 952 RBKRBNNQ 014 QNNRKBBR qnnrkbbr 948 RBBKRNNQ 015 QNNRKRBB qnnrkrbb 944 BBRKRNNQ 016 BBNQNRKR bbnqnrkr 943 RKRNQNBB 017 BNQBNRKR bnqbnrkr 939 RKRNBQNB 018 BNQNRBKR bnqnrbkr 935 RKBRNQNB 019 BNQNRKRB bnqnrkrb 931 BRKRNQNB 020 NBBQNRKR nbbqnrkr 942 RKRNQBBN 021 NQBBNRKR nqbbnrkr 938 RKRNBBQN 022 NQBNRBKR nqbnrbkr 934 RKBRNBQN 023 NQBNRKRB nqbnrkrb 930 BRKRNBQN 024 NBQNBRKR nbqnbrkr 941 RKRBNQBN 025 NQNBBRKR nqnbbrkr 937 RKRBBNQN 026 NQNRBBKR nqnrbbkr 933 RKBBRNQN 027 NQNRBKRB nqnrbkrb 929 BRKBRNQN 028 NBQNRKBR nbqnrkbr 940 RBKRNQBN 029 NQNBRKBR nqnbrkbr 936 RBKRBNQN 030 NQNRKBBR nqnrkbbr 932 RBBKRNQN 031 NQNRKRBB nqnrkrbb 928 BBRKRNQN 032 BBNNQRKR bbnnqrkr 927 RKRQNNBB 033 BNNBQRKR bnnbqrkr 923 RKRQBNNB 034 BNNQRBKR bnnqrbkr 919 RKBRQNNB 035 BNNQRKRB bnnqrkrb 915 BRKRQNNB 036 NBBNQRKR nbbnqrkr 926 RKRQNBBN 037 NNBBQRKR nnbbqrkr 922 RKRQBBNN 038 NNBQRBKR nnbqrbkr 918 RKBRQBNN 039 NNBQRKRB nnbqrkrb 914 BRKRQBNN 040 NBNQBRKR nbnqbrkr 925 RKRBQNBN 041 NNQBBRKR nnqbbrkr 921 RKRBBQNN 042 NNQRBBKR nnqrbbkr 917 RKBBRQNN 043 NNQRBKRB nnqrbkrb 913 BRKBRQNN 044 NBNQRKBR nbnqrkbr 924 RBKRQNBN 045 NNQBRKBR nnqbrkbr 920 RBKRBQNN 046 NNQRKBBR nnqrkbbr 916 RBBKRQNN 047 NNQRKRBB nnqrkrbb 912 BBRKRQNN 048 BBNNRQKR bbnnrqkr 911 RKQRNNBB 049 BNNBRQKR bnnbrqkr 907 RKQRBNNB 050 BNNRQBKR bnnrqbkr 903 RKBQRNNB 051 BNNRQKRB bnnrqkrb 899 BRKQRNNB 052 NBBNRQKR nbbnrqkr 910 RKQRNBBN 053 NNBBRQKR nnbbrqkr 906 RKQRBBNN 054 NNBRQBKR nnbrqbkr 902 RKBQRBNN 055 NNBRQKRB nnbrqkrb 898 BRKQRBNN 056 NBNRBQKR nbnrbqkr 909 RKQBRNBN 057 NNRBBQKR nnrbbqkr 905 RKQBBRNN 058 NNRQBBKR nnrqbbkr 901 RKBBQRNN 059 NNRQBKRB nnrqbkrb 897 BRKBQRNN 060 NBNRQKBR nbnrqkbr 908 RBKQRNBN 061 NNRBQKBR nnrbqkbr 904 RBKQBRNN 062 NNRQKBBR nnrqkbbr 900 RBBKQRNN 063 NNRQKRBB nnrqkrbb 896 BBRKQRNN 064 BBNNRKQR bbnnrkqr 895 RQKRNNBB 065 BNNBRKQR bnnbrkqr 891 RQKRBNNB 066 BNNRKBQR bnnrkbqr 887 RQBKRNNB 067 BNNRKQRB bnnrkqrb 883 BRQKRNNB 068 NBBNRKQR nbbnrkqr 894 RQKRNBBN 069 NNBBRKQR nnbbrkqr 890 RQKRBBNN 070 NNBRKBQR nnbrkbqr 886 RQBKRBNN 071 NNBRKQRB nnbrkqrb 882 BRQKRBNN 072 NBNRBKQR nbnrbkqr 893 RQKBRNBN 073 NNRBBKQR nnrbbkqr 889 RQKBBRNN 074 NNRKBBQR nnrkbbqr 885 RQBBKRNN 075 NNRKBQRB nnrkbqrb 881 BRQBKRNN 076 NBNRKQBR nbnrkqbr 892 RBQKRNBN 077 NNRBKQBR nnrbkqbr 888 RBQKBRNN 078 NNRKQBBR nnrkqbbr 884 RBBQKRNN 079 NNRKQRBB nnrkqrbb 880 BBRQKRNN 080 BBNNRKRQ bbnnrkrq 879 QRKRNNBB 081 BNNBRKRQ bnnbrkrq 875 QRKRBNNB 082 BNNRKBRQ bnnrkbrq 871 QRBKRNNB 083 BNNRKRQB bnnrkrqb 867 BQRKRNNB 084 NBBNRKRQ nbbnrkrq 878 QRKRNBBN 085 NNBBRKRQ nnbbrkrq 874 QRKRBBNN 086 NNBRKBRQ nnbrkbrq 870 QRBKRBNN 087 NNBRKRQB nnbrkrqb 866 BQRKRBNN 088 NBNRBKRQ nbnrbkrq 877 QRKBRNBN 089 NNRBBKRQ nnrbbkrq 873 QRKBBRNN 090 NNRKBBRQ nnrkbbrq 869 QRBBKRNN 091 NNRKBRQB nnrkbrqb 865 BQRBKRNN 092 NBNRKRBQ nbnrkrbq 876 QBRKRNBN 093 NNRBKRBQ nnrbkrbq 872 QBRKBRNN 094 NNRKRBBQ nnrkrbbq 868 QBBRKRNN 095 NNRKRQBB nnrkrqbb 864 BBQRKRNN 096 BBQNRNKR bbqnrnkr 863 RKNRNQBB 097 BQNBRNKR bqnbrnkr 859 RKNRBNQB 098 BQNRNBKR bqnrnbkr 855 RKBNRNQB 099 BQNRNKRB bqnrnkrb 851 BRKNRNQB 100 QBBNRNKR qbbnrnkr 862 RKNRNBBQ 101 QNBBRNKR qnbbrnkr 858 RKNRBBNQ 102 QNBRNBKR qnbrnbkr 854 RKBNRBNQ 103 QNBRNKRB qnbrnkrb 850 BRKNRBNQ 104 QBNRBNKR qbnrbnkr 861 RKNBRNBQ 105 QNRBBNKR qnrbbnkr 857 RKNBBRNQ 106 QNRNBBKR qnrnbbkr 853 RKBBNRNQ 107 QNRNBKRB qnrnbkrb 849 BRKBNRNQ 108 QBNRNKBR qbnrnkbr 860 RBKNRNBQ 109 QNRBNKBR qnrbnkbr 856 RBKNBRNQ 110 QNRNKBBR qnrnkbbr 852 RBBKNRNQ 111 QNRNKRBB qnrnkrbb 848 BBRKNRNQ 112 BBNQRNKR bbnqrnkr 847 RKNRQNBB 113 BNQBRNKR bnqbrnkr 843 RKNRBQNB 114 BNQRNBKR bnqrnbkr 839 RKBNRQNB 115 BNQRNKRB bnqrnkrb 835 BRKNRQNB 116 NBBQRNKR nbbqrnkr 846 RKNRQBBN 117 NQBBRNKR nqbbrnkr 842 RKNRBBQN 118 NQBRNBKR nqbrnbkr 838 RKBNRBQN 119 NQBRNKRB nqbrnkrb 834 BRKNRBQN 120 NBQRBNKR nbqrbnkr 845 RKNBRQBN 121 NQRBBNKR nqrbbnkr 841 RKNBBRQN 122 NQRNBBKR nqrnbbkr 837 RKBBNRQN 123 NQRNBKRB nqrnbkrb 833 BRKBNRQN 124 NBQRNKBR nbqrnkbr 844 RBKNRQBN 125 NQRBNKBR nqrbnkbr 840 RBKNBRQN 126 NQRNKBBR nqrnkbbr 836 RBBKNRQN 127 NQRNKRBB nqrnkrbb 832 BBRKNRQN 128 BBNRQNKR bbnrqnkr 831 RKNQRNBB 129 BNRBQNKR bnrbqnkr 827 RKNQBRNB 130 BNRQNBKR bnrqnbkr 823 RKBNQRNB 131 BNRQNKRB bnrqnkrb 819 BRKNQRNB 132 NBBRQNKR nbbrqnkr 830 RKNQRBBN 133 NRBBQNKR nrbbqnkr 826 RKNQBBRN 134 NRBQNBKR nrbqnbkr 822 RKBNQBRN 135 NRBQNKRB nrbqnkrb 818 BRKNQBRN 136 NBRQBNKR nbrqbnkr 829 RKNBQRBN 137 NRQBBNKR nrqbbnkr 825 RKNBBQRN 138 NRQNBBKR nrqnbbkr 821 RKBBNQRN 139 NRQNBKRB nrqnbkrb 817 BRKBNQRN 140 NBRQNKBR nbrqnkbr 828 RBKNQRBN 141 NRQBNKBR nrqbnkbr 824 RBKNBQRN 142 NRQNKBBR nrqnkbbr 820 RBBKNQRN 143 NRQNKRBB nrqnkrbb 816 BBRKNQRN 144 BBNRNQKR bbnrnqkr 815 RKQNRNBB 145 BNRBNQKR bnrbnqkr 811 RKQNBRNB 146 BNRNQBKR bnrnqbkr 807 RKBQNRNB 147 BNRNQKRB bnrnqkrb 803 BRKQNRNB 148 NBBRNQKR nbbrnqkr 814 RKQNRBBN 149 NRBBNQKR nrbbnqkr 810 RKQNBBRN 150 NRBNQBKR nrbnqbkr 806 RKBQNBRN 151 NRBNQKRB nrbnqkrb 802 BRKQNBRN 152 NBRNBQKR nbrnbqkr 813 RKQBNRBN 153 NRNBBQKR nrnbbqkr 809 RKQBBNRN 154 NRNQBBKR nrnqbbkr 805 RKBBQNRN 155 NRNQBKRB nrnqbkrb 801 BRKBQNRN 156 NBRNQKBR nbrnqkbr 812 RBKQNRBN 157 NRNBQKBR nrnbqkbr 808 RBKQBNRN 158 NRNQKBBR nrnqkbbr 804 RBBKQNRN 159 NRNQKRBB nrnqkrbb 800 BBRKQNRN 160 BBNRNKQR bbnrnkqr 799 RQKNRNBB 161 BNRBNKQR bnrbnkqr 795 RQKNBRNB 162 BNRNKBQR bnrnkbqr 791 RQBKNRNB 163 BNRNKQRB bnrnkqrb 787 BRQKNRNB 164 NBBRNKQR nbbrnkqr 798 RQKNRBBN 165 NRBBNKQR nrbbnkqr 794 RQKNBBRN 166 NRBNKBQR nrbnkbqr 790 RQBKNBRN 167 NRBNKQRB nrbnkqrb 786 BRQKNBRN 168 NBRNBKQR nbrnbkqr 797 RQKBNRBN 169 NRNBBKQR nrnbbkqr 793 RQKBBNRN 170 NRNKBBQR nrnkbbqr 789 RQBBKNRN 171 NRNKBQRB nrnkbqrb 785 BRQBKNRN 172 NBRNKQBR nbrnkqbr 796 RBQKNRBN 173 NRNBKQBR nrnbkqbr 792 RBQKBNRN 174 NRNKQBBR nrnkqbbr 788 RBBQKNRN 175 NRNKQRBB nrnkqrbb 784 BBRQKNRN 176 BBNRNKRQ bbnrnkrq 783 QRKNRNBB 177 BNRBNKRQ bnrbnkrq 779 QRKNBRNB 178 BNRNKBRQ bnrnkbrq 775 QRBKNRNB 179 BNRNKRQB bnrnkrqb 771 BQRKNRNB 180 NBBRNKRQ nbbrnkrq 782 QRKNRBBN 181 NRBBNKRQ nrbbnkrq 778 QRKNBBRN 182 NRBNKBRQ nrbnkbrq 774 QRBKNBRN 183 NRBNKRQB nrbnkrqb 770 BQRKNBRN 184 NBRNBKRQ nbrnbkrq 781 QRKBNRBN 185 NRNBBKRQ nrnbbkrq 777 QRKBBNRN 186 NRNKBBRQ nrnkbbrq 773 QRBBKNRN 187 NRNKBRQB nrnkbrqb 769 BQRBKNRN 188 NBRNKRBQ nbrnkrbq 780 QBRKNRBN 189 NRNBKRBQ nrnbkrbq 776 QBRKBNRN 190 NRNKRBBQ nrnkrbbq 772 QBBRKNRN 191 NRNKRQBB nrnkrqbb 768 BBQRKNRN 192 BBQNRKNR bbqnrknr 671 RNKRNQBB 193 BQNBRKNR bqnbrknr 667 RNKRBNQB 194 BQNRKBNR bqnrkbnr 663 RNBKRNQB 195 BQNRKNRB bqnrknrb 659 BRNKRNQB 196 QBBNRKNR qbbnrknr 670 RNKRNBBQ 197 QNBBRKNR qnbbrknr 666 RNKRBBNQ 198 QNBRKBNR qnbrkbnr 662 RNBKRBNQ 199 QNBRKNRB qnbrknrb 658 BRNKRBNQ 200 QBNRBKNR qbnrbknr 669 RNKBRNBQ 201 QNRBBKNR qnrbbknr 665 RNKBBRNQ 202 QNRKBBNR qnrkbbnr 661 RNBBKRNQ 203 QNRKBNRB qnrkbnrb 657 BRNBKRNQ 204 QBNRKNBR qbnrknbr 668 RBNKRNBQ 205 QNRBKNBR qnrbknbr 664 RBNKBRNQ 206 QNRKNBBR qnrknbbr 660 RBBNKRNQ 207 QNRKNRBB qnrknrbb 656 BBRNKRNQ 208 BBNQRKNR bbnqrknr 655 RNKRQNBB 209 BNQBRKNR bnqbrknr 651 RNKRBQNB 210 BNQRKBNR bnqrkbnr 647 RNBKRQNB 211 BNQRKNRB bnqrknrb 643 BRNKRQNB 212 NBBQRKNR nbbqrknr 654 RNKRQBBN 213 NQBBRKNR nqbbrknr 650 RNKRBBQN 214 NQBRKBNR nqbrkbnr 646 RNBKRBQN 215 NQBRKNRB nqbrknrb 642 BRNKRBQN 216 NBQRBKNR nbqrbknr 653 RNKBRQBN 217 NQRBBKNR nqrbbknr 649 RNKBBRQN 218 NQRKBBNR nqrkbbnr 645 RNBBKRQN 219 NQRKBNRB nqrkbnrb 641 BRNBKRQN 220 NBQRKNBR nbqrknbr 652 RBNKRQBN 221 NQRBKNBR nqrbknbr 648 RBNKBRQN 222 NQRKNBBR nqrknbbr 644 RBBNKRQN 223 NQRKNRBB nqrknrbb 640 BBRNKRQN 224 BBNRQKNR bbnrqknr 639 RNKQRNBB 225 BNRBQKNR bnrbqknr 635 RNKQBRNB 226 BNRQKBNR bnrqkbnr 631 RNBKQRNB 227 BNRQKNRB bnrqknrb 627 BRNKQRNB 228 NBBRQKNR nbbrqknr 638 RNKQRBBN 229 NRBBQKNR nrbbqknr 634 RNKQBBRN 230 NRBQKBNR nrbqkbnr 630 RNBKQBRN 231 NRBQKNRB nrbqknrb 626 BRNKQBRN 232 NBRQBKNR nbrqbknr 637 RNKBQRBN 233 NRQBBKNR nrqbbknr 633 RNKBBQRN 234 NRQKBBNR nrqkbbnr 629 RNBBKQRN 235 NRQKBNRB nrqkbnrb 625 BRNBKQRN 236 NBRQKNBR nbrqknbr 636 RBNKQRBN 237 NRQBKNBR nrqbknbr 632 RBNKBQRN 238 NRQKNBBR nrqknbbr 628 RBBNKQRN 239 NRQKNRBB nrqknrbb 624 BBRNKQRN 240 BBNRKQNR bbnrkqnr 623 RNQKRNBB 241 BNRBKQNR bnrbkqnr 619 RNQKBRNB 242 BNRKQBNR bnrkqbnr 615 RNBQKRNB 243 BNRKQNRB bnrkqnrb 611 BRNQKRNB 244 NBBRKQNR nbbrkqnr 622 RNQKRBBN 245 NRBBKQNR nrbbkqnr 618 RNQKBBRN 246 NRBKQBNR nrbkqbnr 614 RNBQKBRN 247 NRBKQNRB nrbkqnrb 610 BRNQKBRN 248 NBRKBQNR nbrkbqnr 621 RNQBKRBN 249 NRKBBQNR nrkbbqnr 617 RNQBBKRN 250 NRKQBBNR nrkqbbnr 613 RNBBQKRN 251 NRKQBNRB nrkqbnrb 609 BRNBQKRN 252 NBRKQNBR nbrkqnbr 620 RBNQKRBN 253 NRKBQNBR nrkbqnbr 616 RBNQBKRN 254 NRKQNBBR nrkqnbbr 612 RBBNQKRN 255 NRKQNRBB nrkqnrbb 608 BBRNQKRN 256 BBNRKNQR bbnrknqr 607 RQNKRNBB 257 BNRBKNQR bnrbknqr 603 RQNKBRNB 258 BNRKNBQR bnrknbqr 599 RQBNKRNB 259 BNRKNQRB bnrknqrb 595 BRQNKRNB 260 NBBRKNQR nbbrknqr 606 RQNKRBBN 261 NRBBKNQR nrbbknqr 602 RQNKBBRN 262 NRBKNBQR nrbknbqr 598 RQBNKBRN 263 NRBKNQRB nrbknqrb 594 BRQNKBRN 264 NBRKBNQR nbrkbnqr 605 RQNBKRBN 265 NRKBBNQR nrkbbnqr 601 RQNBBKRN 266 NRKNBBQR nrknbbqr 597 RQBBNKRN 267 NRKNBQRB nrknbqrb 593 BRQBNKRN 268 NBRKNQBR nbrknqbr 604 RBQNKRBN 269 NRKBNQBR nrkbnqbr 600 RBQNBKRN 270 NRKNQBBR nrknqbbr 596 RBBQNKRN 271 NRKNQRBB nrknqrbb 592 BBRQNKRN 272 BBNRKNRQ bbnrknrq 591 QRNKRNBB 273 BNRBKNRQ bnrbknrq 587 QRNKBRNB 274 BNRKNBRQ bnrknbrq 583 QRBNKRNB 275 BNRKNRQB bnrknrqb 579 BQRNKRNB 276 NBBRKNRQ nbbrknrq 590 QRNKRBBN 277 NRBBKNRQ nrbbknrq 586 QRNKBBRN 278 NRBKNBRQ nrbknbrq 582 QRBNKBRN 279 NRBKNRQB nrbknrqb 578 BQRNKBRN 280 NBRKBNRQ nbrkbnrq 589 QRNBKRBN 281 NRKBBNRQ nrkbbnrq 585 QRNBBKRN 282 NRKNBBRQ nrknbbrq 581 QRBBNKRN 283 NRKNBRQB nrknbrqb 577 BQRBNKRN 284 NBRKNRBQ nbrknrbq 588 QBRNKRBN 285 NRKBNRBQ nrkbnrbq 584 QBRNBKRN 286 NRKNRBBQ nrknrbbq 580 QBBRNKRN 287 NRKNRQBB nrknrqbb 576 BBQRNKRN 288 BBQNRKRN bbqnrkrn 383 NRKRNQBB 289 BQNBRKRN bqnbrkrn 379 NRKRBNQB 290 BQNRKBRN bqnrkbrn 375 NRBKRNQB 291 BQNRKRNB bqnrkrnb 371 BNRKRNQB 292 QBBNRKRN qbbnrkrn 382 NRKRNBBQ 293 QNBBRKRN qnbbrkrn 378 NRKRBBNQ 294 QNBRKBRN qnbrkbrn 374 NRBKRBNQ 295 QNBRKRNB qnbrkrnb 370 BNRKRBNQ 296 QBNRBKRN qbnrbkrn 381 NRKBRNBQ 297 QNRBBKRN qnrbbkrn 377 NRKBBRNQ 298 QNRKBBRN qnrkbbrn 373 NRBBKRNQ 299 QNRKBRNB qnrkbrnb 369 BNRBKRNQ 300 QBNRKRBN qbnrkrbn 380 NBRKRNBQ 301 QNRBKRBN qnrbkrbn 376 NBRKBRNQ 302 QNRKRBBN qnrkrbbn 372 NBBRKRNQ 303 QNRKRNBB qnrkrnbb 368 BBNRKRNQ 304 BBNQRKRN bbnqrkrn 367 NRKRQNBB 305 BNQBRKRN bnqbrkrn 363 NRKRBQNB 306 BNQRKBRN bnqrkbrn 359 NRBKRQNB 307 BNQRKRNB bnqrkrnb 355 BNRKRQNB 308 NBBQRKRN nbbqrkrn 366 NRKRQBBN 309 NQBBRKRN nqbbrkrn 362 NRKRBBQN 310 NQBRKBRN nqbrkbrn 358 NRBKRBQN 311 NQBRKRNB nqbrkrnb 354 BNRKRBQN 312 NBQRBKRN nbqrbkrn 365 NRKBRQBN 313 NQRBBKRN nqrbbkrn 361 NRKBBRQN 314 NQRKBBRN nqrkbbrn 357 NRBBKRQN 315 NQRKBRNB nqrkbrnb 353 BNRBKRQN 316 NBQRKRBN nbqrkrbn 364 NBRKRQBN 317 NQRBKRBN nqrbkrbn 360 NBRKBRQN 318 NQRKRBBN nqrkrbbn 356 NBBRKRQN 319 NQRKRNBB nqrkrnbb 352 BBNRKRQN 320 BBNRQKRN bbnrqkrn 351 NRKQRNBB 321 BNRBQKRN bnrbqkrn 347 NRKQBRNB 322 BNRQKBRN bnrqkbrn 343 NRBKQRNB 323 BNRQKRNB bnrqkrnb 339 BNRKQRNB 324 NBBRQKRN nbbrqkrn 350 NRKQRBBN 325 NRBBQKRN nrbbqkrn 346 NRKQBBRN 326 NRBQKBRN nrbqkbrn 342 NRBKQBRN 327 NRBQKRNB nrbqkrnb 338 BNRKQBRN 328 NBRQBKRN nbrqbkrn 349 NRKBQRBN 329 NRQBBKRN nrqbbkrn 345 NRKBBQRN 330 NRQKBBRN nrqkbbrn 341 NRBBKQRN 331 NRQKBRNB nrqkbrnb 337 BNRBKQRN 332 NBRQKRBN nbrqkrbn 348 NBRKQRBN 333 NRQBKRBN nrqbkrbn 344 NBRKBQRN 334 NRQKRBBN nrqkrbbn 340 NBBRKQRN 335 NRQKRNBB nrqkrnbb 336 BBNRKQRN 336 BBNRKQRN bbnrkqrn 335 NRQKRNBB 337 BNRBKQRN bnrbkqrn 331 NRQKBRNB 338 BNRKQBRN bnrkqbrn 327 NRBQKRNB 339 BNRKQRNB bnrkqrnb 323 BNRQKRNB 340 NBBRKQRN nbbrkqrn 334 NRQKRBBN 341 NRBBKQRN nrbbkqrn 330 NRQKBBRN 342 NRBKQBRN nrbkqbrn 326 NRBQKBRN 343 NRBKQRNB nrbkqrnb 322 BNRQKBRN 344 NBRKBQRN nbrkbqrn 333 NRQBKRBN 345 NRKBBQRN nrkbbqrn 329 NRQBBKRN 346 NRKQBBRN nrkqbbrn 325 NRBBQKRN 347 NRKQBRNB nrkqbrnb 321 BNRBQKRN 348 NBRKQRBN nbrkqrbn 332 NBRQKRBN 349 NRKBQRBN nrkbqrbn 328 NBRQBKRN 350 NRKQRBBN nrkqrbbn 324 NBBRQKRN 351 NRKQRNBB nrkqrnbb 320 BBNRQKRN 352 BBNRKRQN bbnrkrqn 319 NQRKRNBB 353 BNRBKRQN bnrbkrqn 315 NQRKBRNB 354 BNRKRBQN bnrkrbqn 311 NQBRKRNB 355 BNRKRQNB bnrkrqnb 307 BNQRKRNB 356 NBBRKRQN nbbrkrqn 318 NQRKRBBN 357 NRBBKRQN nrbbkrqn 314 NQRKBBRN 358 NRBKRBQN nrbkrbqn 310 NQBRKBRN 359 NRBKRQNB nrbkrqnb 306 BNQRKBRN 360 NBRKBRQN nbrkbrqn 317 NQRBKRBN 361 NRKBBRQN nrkbbrqn 313 NQRBBKRN 362 NRKRBBQN nrkrbbqn 309 NQBBRKRN 363 NRKRBQNB nrkrbqnb 305 BNQBRKRN 364 NBRKRQBN nbrkrqbn 316 NBQRKRBN 365 NRKBRQBN nrkbrqbn 312 NBQRBKRN 366 NRKRQBBN nrkrqbbn 308 NBBQRKRN 367 NRKRQNBB nrkrqnbb 304 BBNQRKRN 368 BBNRKRNQ bbnrkrnq 303 QNRKRNBB 369 BNRBKRNQ bnrbkrnq 299 QNRKBRNB 370 BNRKRBNQ bnrkrbnq 295 QNBRKRNB 371 BNRKRNQB bnrkrnqb 291 BQNRKRNB 372 NBBRKRNQ nbbrkrnq 302 QNRKRBBN 373 NRBBKRNQ nrbbkrnq 298 QNRKBBRN 374 NRBKRBNQ nrbkrbnq 294 QNBRKBRN 375 NRBKRNQB nrbkrnqb 290 BQNRKBRN 376 NBRKBRNQ nbrkbrnq 301 QNRBKRBN 377 NRKBBRNQ nrkbbrnq 297 QNRBBKRN 378 NRKRBBNQ nrkrbbnq 293 QNBBRKRN 379 NRKRBNQB nrkrbnqb 289 BQNBRKRN 380 NBRKRNBQ nbrkrnbq 300 QBNRKRBN 381 NRKBRNBQ nrkbrnbq 296 QBNRBKRN 382 NRKRNBBQ nrkrnbbq 292 QBBNRKRN 383 NRKRNQBB nrkrnqbb 288 BBQNRKRN 384 BBQRNNKR bbqrnnkr 767 RKNNRQBB 385 BQRBNNKR bqrbnnkr 763 RKNNBRQB 386 BQRNNBKR bqrnnbkr 759 RKBNNRQB 387 BQRNNKRB bqrnnkrb 755 BRKNNRQB 388 QBBRNNKR qbbrnnkr 766 RKNNRBBQ 389 QRBBNNKR qrbbnnkr 762 RKNNBBRQ 390 QRBNNBKR qrbnnbkr 758 RKBNNBRQ 391 QRBNNKRB qrbnnkrb 754 BRKNNBRQ 392 QBRNBNKR qbrnbnkr 765 RKNBNRBQ 393 QRNBBNKR qrnbbnkr 761 RKNBBNRQ 394 QRNNBBKR qrnnbbkr 757 RKBBNNRQ 395 QRNNBKRB qrnnbkrb 753 BRKBNNRQ 396 QBRNNKBR qbrnnkbr 764 RBKNNRBQ 397 QRNBNKBR qrnbnkbr 760 RBKNBNRQ 398 QRNNKBBR qrnnkbbr 756 RBBKNNRQ 399 QRNNKRBB qrnnkrbb 752 BBRKNNRQ 400 BBRQNNKR bbrqnnkr 751 RKNNQRBB 401 BRQBNNKR brqbnnkr 747 RKNNBQRB 402 BRQNNBKR brqnnbkr 743 RKBNNQRB 403 BRQNNKRB brqnnkrb 739 BRKNNQRB 404 RBBQNNKR rbbqnnkr 750 RKNNQBBR 405 RQBBNNKR rqbbnnkr 746 RKNNBBQR 406 RQBNNBKR rqbnnbkr 742 RKBNNBQR 407 RQBNNKRB rqbnnkrb 738 BRKNNBQR 408 RBQNBNKR rbqnbnkr 749 RKNBNQBR 409 RQNBBNKR rqnbbnkr 745 RKNBBNQR 410 RQNNBBKR rqnnbbkr 741 RKBBNNQR 411 RQNNBKRB rqnnbkrb 737 BRKBNNQR 412 RBQNNKBR rbqnnkbr 748 RBKNNQBR 413 RQNBNKBR rqnbnkbr 744 RBKNBNQR 414 RQNNKBBR rqnnkbbr 740 RBBKNNQR 415 RQNNKRBB rqnnkrbb 736 BBRKNNQR 416 BBRNQNKR bbrnqnkr 735 RKNQNRBB 417 BRNBQNKR brnbqnkr 731 RKNQBNRB 418 BRNQNBKR brnqnbkr 727 RKBNQNRB 419 BRNQNKRB brnqnkrb 723 BRKNQNRB 420 RBBNQNKR rbbnqnkr 734 RKNQNBBR 421 RNBBQNKR rnbbqnkr 730 RKNQBBNR 422 RNBQNBKR rnbqnbkr 726 RKBNQBNR 423 RNBQNKRB rnbqnkrb 722 BRKNQBNR 424 RBNQBNKR rbnqbnkr 733 RKNBQNBR 425 RNQBBNKR rnqbbnkr 729 RKNBBQNR 426 RNQNBBKR rnqnbbkr 725 RKBBNQNR 427 RNQNBKRB rnqnbkrb 721 BRKBNQNR 428 RBNQNKBR rbnqnkbr 732 RBKNQNBR 429 RNQBNKBR rnqbnkbr 728 RBKNBQNR 430 RNQNKBBR rnqnkbbr 724 RBBKNQNR 431 RNQNKRBB rnqnkrbb 720 BBRKNQNR 432 BBRNNQKR bbrnnqkr 719 RKQNNRBB 433 BRNBNQKR brnbnqkr 715 RKQNBNRB 434 BRNNQBKR brnnqbkr 711 RKBQNNRB 435 BRNNQKRB brnnqkrb 707 BRKQNNRB 436 RBBNNQKR rbbnnqkr 718 RKQNNBBR 437 RNBBNQKR rnbbnqkr 714 RKQNBBNR 438 RNBNQBKR rnbnqbkr 710 RKBQNBNR 439 RNBNQKRB rnbnqkrb 706 BRKQNBNR 440 RBNNBQKR rbnnbqkr 717 RKQBNNBR 441 RNNBBQKR rnnbbqkr 713 RKQBBNNR 442 RNNQBBKR rnnqbbkr 709 RKBBQNNR 443 RNNQBKRB rnnqbkrb 705 BRKBQNNR 444 RBNNQKBR rbnnqkbr 716 RBKQNNBR 445 RNNBQKBR rnnbqkbr 712 RBKQBNNR 446 RNNQKBBR rnnqkbbr 708 RBBKQNNR 447 RNNQKRBB rnnqkrbb 704 BBRKQNNR 448 BBRNNKQR bbrnnkqr 703 RQKNNRBB 449 BRNBNKQR brnbnkqr 699 RQKNBNRB 450 BRNNKBQR brnnkbqr 695 RQBKNNRB 451 BRNNKQRB brnnkqrb 691 BRQKNNRB 452 RBBNNKQR rbbnnkqr 702 RQKNNBBR 453 RNBBNKQR rnbbnkqr 698 RQKNBBNR 454 RNBNKBQR rnbnkbqr 694 RQBKNBNR 455 RNBNKQRB rnbnkqrb 690 BRQKNBNR 456 RBNNBKQR rbnnbkqr 701 RQKBNNBR 457 RNNBBKQR rnnbbkqr 697 RQKBBNNR 458 RNNKBBQR rnnkbbqr 693 RQBBKNNR 459 RNNKBQRB rnnkbqrb 689 BRQBKNNR 460 RBNNKQBR rbnnkqbr 700 RBQKNNBR 461 RNNBKQBR rnnbkqbr 696 RBQKBNNR 462 RNNKQBBR rnnkqbbr 692 RBBQKNNR 463 RNNKQRBB rnnkqrbb 688 BBRQKNNR 464 BBRNNKRQ bbrnnkrq 687 QRKNNRBB 465 BRNBNKRQ brnbnkrq 683 QRKNBNRB 466 BRNNKBRQ brnnkbrq 679 QRBKNNRB 467 BRNNKRQB brnnkrqb 675 BQRKNNRB 468 RBBNNKRQ rbbnnkrq 686 QRKNNBBR 469 RNBBNKRQ rnbbnkrq 682 QRKNBBNR 470 RNBNKBRQ rnbnkbrq 678 QRBKNBNR 471 RNBNKRQB rnbnkrqb 674 BQRKNBNR 472 RBNNBKRQ rbnnbkrq 685 QRKBNNBR 473 RNNBBKRQ rnnbbkrq 681 QRKBBNNR 474 RNNKBBRQ rnnkbbrq 677 QRBBKNNR 475 RNNKBRQB rnnkbrqb 673 BQRBKNNR 476 RBNNKRBQ rbnnkrbq 684 QBRKNNBR 477 RNNBKRBQ rnnbkrbq 680 QBRKBNNR 478 RNNKRBBQ rnnkrbbq 676 QBBRKNNR 479 RNNKRQBB rnnkrqbb 672 BBQRKNNR 480 BBQRNKNR bbqrnknr 575 RNKNRQBB 481 BQRBNKNR bqrbnknr 571 RNKNBRQB 482 BQRNKBNR bqrnkbnr 567 RNBKNRQB 483 BQRNKNRB bqrnknrb 563 BRNKNRQB 484 QBBRNKNR qbbrnknr 574 RNKNRBBQ 485 QRBBNKNR qrbbnknr 570 RNKNBBRQ 486 QRBNKBNR qrbnkbnr 566 RNBKNBRQ 487 QRBNKNRB qrbnknrb 562 BRNKNBRQ 488 QBRNBKNR qbrnbknr 573 RNKBNRBQ 489 QRNBBKNR qrnbbknr 569 RNKBBNRQ 490 QRNKBBNR qrnkbbnr 565 RNBBKNRQ 491 QRNKBNRB qrnkbnrb 561 BRNBKNRQ 492 QBRNKNBR qbrnknbr 572 RBNKNRBQ 493 QRNBKNBR qrnbknbr 568 RBNKBNRQ 494 QRNKNBBR qrnknbbr 564 RBBNKNRQ 495 QRNKNRBB qrnknrbb 560 BBRNKNRQ 496 BBRQNKNR bbrqnknr 559 RNKNQRBB 497 BRQBNKNR brqbnknr 555 RNKNBQRB 498 BRQNKBNR brqnkbnr 551 RNBKNQRB 499 BRQNKNRB brqnknrb 547 BRNKNQRB 500 RBBQNKNR rbbqnknr 558 RNKNQBBR 501 RQBBNKNR rqbbnknr 554 RNKNBBQR 502 RQBNKBNR rqbnkbnr 550 RNBKNBQR 503 RQBNKNRB rqbnknrb 546 BRNKNBQR 504 RBQNBKNR rbqnbknr 557 RNKBNQBR 505 RQNBBKNR rqnbbknr 553 RNKBBNQR 506 RQNKBBNR rqnkbbnr 549 RNBBKNQR 507 RQNKBNRB rqnkbnrb 545 BRNBKNQR 508 RBQNKNBR rbqnknbr 556 RBNKNQBR 509 RQNBKNBR rqnbknbr 552 RBNKBNQR 510 RQNKNBBR rqnknbbr 548 RBBNKNQR 511 RQNKNRBB rqnknrbb 544 BBRNKNQR 512 BBRNQKNR bbrnqknr 543 RNKQNRBB 513 BRNBQKNR brnbqknr 539 RNKQBNRB 514 BRNQKBNR brnqkbnr 535 RNBKQNRB 515 BRNQKNRB brnqknrb 531 BRNKQNRB 516 RBBNQKNR rbbnqknr 542 RNKQNBBR 517 RNBBQKNR rnbbqknr 538 RNKQBBNR 518 RNBQKBNR rnbqkbnr 534 RNBKQBNR 519 RNBQKNRB rnbqknrb 530 BRNKQBNR 520 RBNQBKNR rbnqbknr 541 RNKBQNBR 521 RNQBBKNR rnqbbknr 537 RNKBBQNR 522 RNQKBBNR rnqkbbnr 533 RNBBKQNR 523 RNQKBNRB rnqkbnrb 529 BRNBKQNR 524 RBNQKNBR rbnqknbr 540 RBNKQNBR 525 RNQBKNBR rnqbknbr 536 RBNKBQNR 526 RNQKNBBR rnqknbbr 532 RBBNKQNR 527 RNQKNRBB rnqknrbb 528 BBRNKQNR 528 BBRNKQNR bbrnkqnr 527 RNQKNRBB 529 BRNBKQNR brnbkqnr 523 RNQKBNRB 530 BRNKQBNR brnkqbnr 519 RNBQKNRB 531 BRNKQNRB brnkqnrb 515 BRNQKNRB 532 RBBNKQNR rbbnkqnr 526 RNQKNBBR 533 RNBBKQNR rnbbkqnr 522 RNQKBBNR 534 RNBKQBNR rnbkqbnr 518 RNBQKBNR 535 RNBKQNRB rnbkqnrb 514 BRNQKBNR 536 RBNKBQNR rbnkbqnr 525 RNQBKNBR 537 RNKBBQNR rnkbbqnr 521 RNQBBKNR 538 RNKQBBNR rnkqbbnr 517 RNBBQKNR 539 RNKQBNRB rnkqbnrb 513 BRNBQKNR 540 RBNKQNBR rbnkqnbr 524 RBNQKNBR 541 RNKBQNBR rnkbqnbr 520 RBNQBKNR 542 RNKQNBBR rnkqnbbr 516 RBBNQKNR 543 RNKQNRBB rnkqnrbb 512 BBRNQKNR 544 BBRNKNQR bbrnknqr 511 RQNKNRBB 545 BRNBKNQR brnbknqr 507 RQNKBNRB 546 BRNKNBQR brnknbqr 503 RQBNKNRB 547 BRNKNQRB brnknqrb 499 BRQNKNRB 548 RBBNKNQR rbbnknqr 510 RQNKNBBR 549 RNBBKNQR rnbbknqr 506 RQNKBBNR 550 RNBKNBQR rnbknbqr 502 RQBNKBNR 551 RNBKNQRB rnbknqrb 498 BRQNKBNR 552 RBNKBNQR rbnkbnqr 509 RQNBKNBR 553 RNKBBNQR rnkbbnqr 505 RQNBBKNR 554 RNKNBBQR rnknbbqr 501 RQBBNKNR 555 RNKNBQRB rnknbqrb 497 BRQBNKNR 556 RBNKNQBR rbnknqbr 508 RBQNKNBR 557 RNKBNQBR rnkbnqbr 504 RBQNBKNR 558 RNKNQBBR rnknqbbr 500 RBBQNKNR 559 RNKNQRBB rnknqrbb 496 BBRQNKNR 560 BBRNKNRQ bbrnknrq 495 QRNKNRBB 561 BRNBKNRQ brnbknrq 491 QRNKBNRB 562 BRNKNBRQ brnknbrq 487 QRBNKNRB 563 BRNKNRQB brnknrqb 483 BQRNKNRB 564 RBBNKNRQ rbbnknrq 494 QRNKNBBR 565 RNBBKNRQ rnbbknrq 490 QRNKBBNR 566 RNBKNBRQ rnbknbrq 486 QRBNKBNR 567 RNBKNRQB rnbknrqb 482 BQRNKBNR 568 RBNKBNRQ rbnkbnrq 493 QRNBKNBR 569 RNKBBNRQ rnkbbnrq 489 QRNBBKNR 570 RNKNBBRQ rnknbbrq 485 QRBBNKNR 571 RNKNBRQB rnknbrqb 481 BQRBNKNR 572 RBNKNRBQ rbnknrbq 492 QBRNKNBR 573 RNKBNRBQ rnkbnrbq 488 QBRNBKNR 574 RNKNRBBQ rnknrbbq 484 QBBRNKNR 575 RNKNRQBB rnknrqbb 480 BBQRNKNR 576 BBQRNKRN bbqrnkrn 287 NRKNRQBB 577 BQRBNKRN bqrbnkrn 283 NRKNBRQB 578 BQRNKBRN bqrnkbrn 279 NRBKNRQB 579 BQRNKRNB bqrnkrnb 275 BNRKNRQB 580 QBBRNKRN qbbrnkrn 286 NRKNRBBQ 581 QRBBNKRN qrbbnkrn 282 NRKNBBRQ 582 QRBNKBRN qrbnkbrn 278 NRBKNBRQ 583 QRBNKRNB qrbnkrnb 274 BNRKNBRQ 584 QBRNBKRN qbrnbkrn 285 NRKBNRBQ 585 QRNBBKRN qrnbbkrn 281 NRKBBNRQ 586 QRNKBBRN qrnkbbrn 277 NRBBKNRQ 587 QRNKBRNB qrnkbrnb 273 BNRBKNRQ 588 QBRNKRBN qbrnkrbn 284 NBRKNRBQ 589 QRNBKRBN qrnbkrbn 280 NBRKBNRQ 590 QRNKRBBN qrnkrbbn 276 NBBRKNRQ 591 QRNKRNBB qrnkrnbb 272 BBNRKNRQ 592 BBRQNKRN bbrqnkrn 271 NRKNQRBB 593 BRQBNKRN brqbnkrn 267 NRKNBQRB 594 BRQNKBRN brqnkbrn 263 NRBKNQRB 595 BRQNKRNB brqnkrnb 259 BNRKNQRB 596 RBBQNKRN rbbqnkrn 270 NRKNQBBR 597 RQBBNKRN rqbbnkrn 266 NRKNBBQR 598 RQBNKBRN rqbnkbrn 262 NRBKNBQR 599 RQBNKRNB rqbnkrnb 258 BNRKNBQR 600 RBQNBKRN rbqnbkrn 269 NRKBNQBR 601 RQNBBKRN rqnbbkrn 265 NRKBBNQR 602 RQNKBBRN rqnkbbrn 261 NRBBKNQR 603 RQNKBRNB rqnkbrnb 257 BNRBKNQR 604 RBQNKRBN rbqnkrbn 268 NBRKNQBR 605 RQNBKRBN rqnbkrbn 264 NBRKBNQR 606 RQNKRBBN rqnkrbbn 260 NBBRKNQR 607 RQNKRNBB rqnkrnbb 256 BBNRKNQR 608 BBRNQKRN bbrnqkrn 255 NRKQNRBB 609 BRNBQKRN brnbqkrn 251 NRKQBNRB 610 BRNQKBRN brnqkbrn 247 NRBKQNRB 611 BRNQKRNB brnqkrnb 243 BNRKQNRB 612 RBBNQKRN rbbnqkrn 254 NRKQNBBR 613 RNBBQKRN rnbbqkrn 250 NRKQBBNR 614 RNBQKBRN rnbqkbrn 246 NRBKQBNR 615 RNBQKRNB rnbqkrnb 242 BNRKQBNR 616 RBNQBKRN rbnqbkrn 253 NRKBQNBR 617 RNQBBKRN rnqbbkrn 249 NRKBBQNR 618 RNQKBBRN rnqkbbrn 245 NRBBKQNR 619 RNQKBRNB rnqkbrnb 241 BNRBKQNR 620 RBNQKRBN rbnqkrbn 252 NBRKQNBR 621 RNQBKRBN rnqbkrbn 248 NBRKBQNR 622 RNQKRBBN rnqkrbbn 244 NBBRKQNR 623 RNQKRNBB rnqkrnbb 240 BBNRKQNR 624 BBRNKQRN bbrnkqrn 239 NRQKNRBB 625 BRNBKQRN brnbkqrn 235 NRQKBNRB 626 BRNKQBRN brnkqbrn 231 NRBQKNRB 627 BRNKQRNB brnkqrnb 227 BNRQKNRB 628 RBBNKQRN rbbnkqrn 238 NRQKNBBR 629 RNBBKQRN rnbbkqrn 234 NRQKBBNR 630 RNBKQBRN rnbkqbrn 230 NRBQKBNR 631 RNBKQRNB rnbkqrnb 226 BNRQKBNR 632 RBNKBQRN rbnkbqrn 237 NRQBKNBR 633 RNKBBQRN rnkbbqrn 233 NRQBBKNR 634 RNKQBBRN rnkqbbrn 229 NRBBQKNR 635 RNKQBRNB rnkqbrnb 225 BNRBQKNR 636 RBNKQRBN rbnkqrbn 236 NBRQKNBR 637 RNKBQRBN rnkbqrbn 232 NBRQBKNR 638 RNKQRBBN rnkqrbbn 228 NBBRQKNR 639 RNKQRNBB rnkqrnbb 224 BBNRQKNR 640 BBRNKRQN bbrnkrqn 223 NQRKNRBB 641 BRNBKRQN brnbkrqn 219 NQRKBNRB 642 BRNKRBQN brnkrbqn 215 NQBRKNRB 643 BRNKRQNB brnkrqnb 211 BNQRKNRB 644 RBBNKRQN rbbnkrqn 222 NQRKNBBR 645 RNBBKRQN rnbbkrqn 218 NQRKBBNR 646 RNBKRBQN rnbkrbqn 214 NQBRKBNR 647 RNBKRQNB rnbkrqnb 210 BNQRKBNR 648 RBNKBRQN rbnkbrqn 221 NQRBKNBR 649 RNKBBRQN rnkbbrqn 217 NQRBBKNR 650 RNKRBBQN rnkrbbqn 213 NQBBRKNR 651 RNKRBQNB rnkrbqnb 209 BNQBRKNR 652 RBNKRQBN rbnkrqbn 220 NBQRKNBR 653 RNKBRQBN rnkbrqbn 216 NBQRBKNR 654 RNKRQBBN rnkrqbbn 212 NBBQRKNR 655 RNKRQNBB rnkrqnbb 208 BBNQRKNR 656 BBRNKRNQ bbrnkrnq 207 QNRKNRBB 657 BRNBKRNQ brnbkrnq 203 QNRKBNRB 658 BRNKRBNQ brnkrbnq 199 QNBRKNRB 659 BRNKRNQB brnkrnqb 195 BQNRKNRB 660 RBBNKRNQ rbbnkrnq 206 QNRKNBBR 661 RNBBKRNQ rnbbkrnq 202 QNRKBBNR 662 RNBKRBNQ rnbkrbnq 198 QNBRKBNR 663 RNBKRNQB rnbkrnqb 194 BQNRKBNR 664 RBNKBRNQ rbnkbrnq 205 QNRBKNBR 665 RNKBBRNQ rnkbbrnq 201 QNRBBKNR 666 RNKRBBNQ rnkrbbnq 197 QNBBRKNR 667 RNKRBNQB rnkrbnqb 193 BQNBRKNR 668 RBNKRNBQ rbnkrnbq 204 QBNRKNBR 669 RNKBRNBQ rnkbrnbq 200 QBNRBKNR 670 RNKRNBBQ rnkrnbbq 196 QBBNRKNR 671 RNKRNQBB rnkrnqbb 192 BBQNRKNR 672 BBQRKNNR bbqrknnr 479 RNNKRQBB 673 BQRBKNNR bqrbknnr 475 RNNKBRQB 674 BQRKNBNR bqrknbnr 471 RNBNKRQB 675 BQRKNNRB bqrknnrb 467 BRNNKRQB 676 QBBRKNNR qbbrknnr 478 RNNKRBBQ 677 QRBBKNNR qrbbknnr 474 RNNKBBRQ 678 QRBKNBNR qrbknbnr 470 RNBNKBRQ 679 QRBKNNRB qrbknnrb 466 BRNNKBRQ 680 QBRKBNNR qbrkbnnr 477 RNNBKRBQ 681 QRKBBNNR qrkbbnnr 473 RNNBBKRQ 682 QRKNBBNR qrknbbnr 469 RNBBNKRQ 683 QRKNBNRB qrknbnrb 465 BRNBNKRQ 684 QBRKNNBR qbrknnbr 476 RBNNKRBQ 685 QRKBNNBR qrkbnnbr 472 RBNNBKRQ 686 QRKNNBBR qrknnbbr 468 RBBNNKRQ 687 QRKNNRBB qrknnrbb 464 BBRNNKRQ 688 BBRQKNNR bbrqknnr 463 RNNKQRBB 689 BRQBKNNR brqbknnr 459 RNNKBQRB 690 BRQKNBNR brqknbnr 455 RNBNKQRB 691 BRQKNNRB brqknnrb 451 BRNNKQRB 692 RBBQKNNR rbbqknnr 462 RNNKQBBR 693 RQBBKNNR rqbbknnr 458 RNNKBBQR 694 RQBKNBNR rqbknbnr 454 RNBNKBQR 695 RQBKNNRB rqbknnrb 450 BRNNKBQR 696 RBQKBNNR rbqkbnnr 461 RNNBKQBR 697 RQKBBNNR rqkbbnnr 457 RNNBBKQR 698 RQKNBBNR rqknbbnr 453 RNBBNKQR 699 RQKNBNRB rqknbnrb 449 BRNBNKQR 700 RBQKNNBR rbqknnbr 460 RBNNKQBR 701 RQKBNNBR rqkbnnbr 456 RBNNBKQR 702 RQKNNBBR rqknnbbr 452 RBBNNKQR 703 RQKNNRBB rqknnrbb 448 BBRNNKQR 704 BBRKQNNR bbrkqnnr 447 RNNQKRBB 705 BRKBQNNR brkbqnnr 443 RNNQBKRB 706 BRKQNBNR brkqnbnr 439 RNBNQKRB 707 BRKQNNRB brkqnnrb 435 BRNNQKRB 708 RBBKQNNR rbbkqnnr 446 RNNQKBBR 709 RKBBQNNR rkbbqnnr 442 RNNQBBKR 710 RKBQNBNR rkbqnbnr 438 RNBNQBKR 711 RKBQNNRB rkbqnnrb 434 BRNNQBKR 712 RBKQBNNR rbkqbnnr 445 RNNBQKBR 713 RKQBBNNR rkqbbnnr 441 RNNBBQKR 714 RKQNBBNR rkqnbbnr 437 RNBBNQKR 715 RKQNBNRB rkqnbnrb 433 BRNBNQKR 716 RBKQNNBR rbkqnnbr 444 RBNNQKBR 717 RKQBNNBR rkqbnnbr 440 RBNNBQKR 718 RKQNNBBR rkqnnbbr 436 RBBNNQKR 719 RKQNNRBB rkqnnrbb 432 BBRNNQKR 720 BBRKNQNR bbrknqnr 431 RNQNKRBB 721 BRKBNQNR brkbnqnr 427 RNQNBKRB 722 BRKNQBNR brknqbnr 423 RNBQNKRB 723 BRKNQNRB brknqnrb 419 BRNQNKRB 724 RBBKNQNR rbbknqnr 430 RNQNKBBR 725 RKBBNQNR rkbbnqnr 426 RNQNBBKR 726 RKBNQBNR rkbnqbnr 422 RNBQNBKR 727 RKBNQNRB rkbnqnrb 418 BRNQNBKR 728 RBKNBQNR rbknbqnr 429 RNQBNKBR 729 RKNBBQNR rknbbqnr 425 RNQBBNKR 730 RKNQBBNR rknqbbnr 421 RNBBQNKR 731 RKNQBNRB rknqbnrb 417 BRNBQNKR 732 RBKNQNBR rbknqnbr 428 RBNQNKBR 733 RKNBQNBR rknbqnbr 424 RBNQBNKR 734 RKNQNBBR rknqnbbr 420 RBBNQNKR 735 RKNQNRBB rknqnrbb 416 BBRNQNKR 736 BBRKNNQR bbrknnqr 415 RQNNKRBB 737 BRKBNNQR brkbnnqr 411 RQNNBKRB 738 BRKNNBQR brknnbqr 407 RQBNNKRB 739 BRKNNQRB brknnqrb 403 BRQNNKRB 740 RBBKNNQR rbbknnqr 414 RQNNKBBR 741 RKBBNNQR rkbbnnqr 410 RQNNBBKR 742 RKBNNBQR rkbnnbqr 406 RQBNNBKR 743 RKBNNQRB rkbnnqrb 402 BRQNNBKR 744 RBKNBNQR rbknbnqr 413 RQNBNKBR 745 RKNBBNQR rknbbnqr 409 RQNBBNKR 746 RKNNBBQR rknnbbqr 405 RQBBNNKR 747 RKNNBQRB rknnbqrb 401 BRQBNNKR 748 RBKNNQBR rbknnqbr 412 RBQNNKBR 749 RKNBNQBR rknbnqbr 408 RBQNBNKR 750 RKNNQBBR rknnqbbr 404 RBBQNNKR 751 RKNNQRBB rknnqrbb 400 BBRQNNKR 752 BBRKNNRQ bbrknnrq 399 QRNNKRBB 753 BRKBNNRQ brkbnnrq 395 QRNNBKRB 754 BRKNNBRQ brknnbrq 391 QRBNNKRB 755 BRKNNRQB brknnrqb 387 BQRNNKRB 756 RBBKNNRQ rbbknnrq 398 QRNNKBBR 757 RKBBNNRQ rkbbnnrq 394 QRNNBBKR 758 RKBNNBRQ rkbnnbrq 390 QRBNNBKR 759 RKBNNRQB rkbnnrqb 386 BQRNNBKR 760 RBKNBNRQ rbknbnrq 397 QRNBNKBR 761 RKNBBNRQ rknbbnrq 393 QRNBBNKR 762 RKNNBBRQ rknnbbrq 389 QRBBNNKR 763 RKNNBRQB rknnbrqb 385 BQRBNNKR 764 RBKNNRBQ rbknnrbq 396 QBRNNKBR 765 RKNBNRBQ rknbnrbq 392 QBRNBNKR 766 RKNNRBBQ rknnrbbq 388 QBBRNNKR 767 RKNNRQBB rknnrqbb 384 BBQRNNKR 768 BBQRKNRN bbqrknrn 191 NRNKRQBB 769 BQRBKNRN bqrbknrn 187 NRNKBRQB 770 BQRKNBRN bqrknbrn 183 NRBNKRQB 771 BQRKNRNB bqrknrnb 179 BNRNKRQB 772 QBBRKNRN qbbrknrn 190 NRNKRBBQ 773 QRBBKNRN qrbbknrn 186 NRNKBBRQ 774 QRBKNBRN qrbknbrn 182 NRBNKBRQ 775 QRBKNRNB qrbknrnb 178 BNRNKBRQ 776 QBRKBNRN qbrkbnrn 189 NRNBKRBQ 777 QRKBBNRN qrkbbnrn 185 NRNBBKRQ 778 QRKNBBRN qrknbbrn 181 NRBBNKRQ 779 QRKNBRNB qrknbrnb 177 BNRBNKRQ 780 QBRKNRBN qbrknrbn 188 NBRNKRBQ 781 QRKBNRBN qrkbnrbn 184 NBRNBKRQ 782 QRKNRBBN qrknrbbn 180 NBBRNKRQ 783 QRKNRNBB qrknrnbb 176 BBNRNKRQ 784 BBRQKNRN bbrqknrn 175 NRNKQRBB 785 BRQBKNRN brqbknrn 171 NRNKBQRB 786 BRQKNBRN brqknbrn 167 NRBNKQRB 787 BRQKNRNB brqknrnb 163 BNRNKQRB 788 RBBQKNRN rbbqknrn 174 NRNKQBBR 789 RQBBKNRN rqbbknrn 170 NRNKBBQR 790 RQBKNBRN rqbknbrn 166 NRBNKBQR 791 RQBKNRNB rqbknrnb 162 BNRNKBQR 792 RBQKBNRN rbqkbnrn 173 NRNBKQBR 793 RQKBBNRN rqkbbnrn 169 NRNBBKQR 794 RQKNBBRN rqknbbrn 165 NRBBNKQR 795 RQKNBRNB rqknbrnb 161 BNRBNKQR 796 RBQKNRBN rbqknrbn 172 NBRNKQBR 797 RQKBNRBN rqkbnrbn 168 NBRNBKQR 798 RQKNRBBN rqknrbbn 164 NBBRNKQR 799 RQKNRNBB rqknrnbb 160 BBNRNKQR 800 BBRKQNRN bbrkqnrn 159 NRNQKRBB 801 BRKBQNRN brkbqnrn 155 NRNQBKRB 802 BRKQNBRN brkqnbrn 151 NRBNQKRB 803 BRKQNRNB brkqnrnb 147 BNRNQKRB 804 RBBKQNRN rbbkqnrn 158 NRNQKBBR 805 RKBBQNRN rkbbqnrn 154 NRNQBBKR 806 RKBQNBRN rkbqnbrn 150 NRBNQBKR 807 RKBQNRNB rkbqnrnb 146 BNRNQBKR 808 RBKQBNRN rbkqbnrn 157 NRNBQKBR 809 RKQBBNRN rkqbbnrn 153 NRNBBQKR 810 RKQNBBRN rkqnbbrn 149 NRBBNQKR 811 RKQNBRNB rkqnbrnb 145 BNRBNQKR 812 RBKQNRBN rbkqnrbn 156 NBRNQKBR 813 RKQBNRBN rkqbnrbn 152 NBRNBQKR 814 RKQNRBBN rkqnrbbn 148 NBBRNQKR 815 RKQNRNBB rkqnrnbb 144 BBNRNQKR 816 BBRKNQRN bbrknqrn 143 NRQNKRBB 817 BRKBNQRN brkbnqrn 139 NRQNBKRB 818 BRKNQBRN brknqbrn 135 NRBQNKRB 819 BRKNQRNB brknqrnb 131 BNRQNKRB 820 RBBKNQRN rbbknqrn 142 NRQNKBBR 821 RKBBNQRN rkbbnqrn 138 NRQNBBKR 822 RKBNQBRN rkbnqbrn 134 NRBQNBKR 823 RKBNQRNB rkbnqrnb 130 BNRQNBKR 824 RBKNBQRN rbknbqrn 141 NRQBNKBR 825 RKNBBQRN rknbbqrn 137 NRQBBNKR 826 RKNQBBRN rknqbbrn 133 NRBBQNKR 827 RKNQBRNB rknqbrnb 129 BNRBQNKR 828 RBKNQRBN rbknqrbn 140 NBRQNKBR 829 RKNBQRBN rknbqrbn 136 NBRQBNKR 830 RKNQRBBN rknqrbbn 132 NBBRQNKR 831 RKNQRNBB rknqrnbb 128 BBNRQNKR 832 BBRKNRQN bbrknrqn 127 NQRNKRBB 833 BRKBNRQN brkbnrqn 123 NQRNBKRB 834 BRKNRBQN brknrbqn 119 NQBRNKRB 835 BRKNRQNB brknrqnb 115 BNQRNKRB 836 RBBKNRQN rbbknrqn 126 NQRNKBBR 837 RKBBNRQN rkbbnrqn 122 NQRNBBKR 838 RKBNRBQN rkbnrbqn 118 NQBRNBKR 839 RKBNRQNB rkbnrqnb 114 BNQRNBKR 840 RBKNBRQN rbknbrqn 125 NQRBNKBR 841 RKNBBRQN rknbbrqn 121 NQRBBNKR 842 RKNRBBQN rknrbbqn 117 NQBBRNKR 843 RKNRBQNB rknrbqnb 113 BNQBRNKR 844 RBKNRQBN rbknrqbn 124 NBQRNKBR 845 RKNBRQBN rknbrqbn 120 NBQRBNKR 846 RKNRQBBN rknrqbbn 116 NBBQRNKR 847 RKNRQNBB rknrqnbb 112 BBNQRNKR 848 BBRKNRNQ bbrknrnq 111 QNRNKRBB 849 BRKBNRNQ brkbnrnq 107 QNRNBKRB 850 BRKNRBNQ brknrbnq 103 QNBRNKRB 851 BRKNRNQB brknrnqb 099 BQNRNKRB 852 RBBKNRNQ rbbknrnq 110 QNRNKBBR 853 RKBBNRNQ rkbbnrnq 106 QNRNBBKR 854 RKBNRBNQ rkbnrbnq 102 QNBRNBKR 855 RKBNRNQB rkbnrnqb 098 BQNRNBKR 856 RBKNBRNQ rbknbrnq 109 QNRBNKBR 857 RKNBBRNQ rknbbrnq 105 QNRBBNKR 858 RKNRBBNQ rknrbbnq 101 QNBBRNKR 859 RKNRBNQB rknrbnqb 097 BQNBRNKR 860 RBKNRNBQ rbknrnbq 108 QBNRNKBR 861 RKNBRNBQ rknbrnbq 104 QBNRBNKR 862 RKNRNBBQ rknrnbbq 100 QBBNRNKR 863 RKNRNQBB rknrnqbb 096 BBQNRNKR 864 BBQRKRNN bbqrkrnn 095 NNRKRQBB 865 BQRBKRNN bqrbkrnn 091 NNRKBRQB 866 BQRKRBNN bqrkrbnn 087 NNBRKRQB 867 BQRKRNNB bqrkrnnb 083 BNNRKRQB 868 QBBRKRNN qbbrkrnn 094 NNRKRBBQ 869 QRBBKRNN qrbbkrnn 090 NNRKBBRQ 870 QRBKRBNN qrbkrbnn 086 NNBRKBRQ 871 QRBKRNNB qrbkrnnb 082 BNNRKBRQ 872 QBRKBRNN qbrkbrnn 093 NNRBKRBQ 873 QRKBBRNN qrkbbrnn 089 NNRBBKRQ 874 QRKRBBNN qrkrbbnn 085 NNBBRKRQ 875 QRKRBNNB qrkrbnnb 081 BNNBRKRQ 876 QBRKRNBN qbrkrnbn 092 NBNRKRBQ 877 QRKBRNBN qrkbrnbn 088 NBNRBKRQ 878 QRKRNBBN qrkrnbbn 084 NBBNRKRQ 879 QRKRNNBB qrkrnnbb 080 BBNNRKRQ 880 BBRQKRNN bbrqkrnn 079 NNRKQRBB 881 BRQBKRNN brqbkrnn 075 NNRKBQRB 882 BRQKRBNN brqkrbnn 071 NNBRKQRB 883 BRQKRNNB brqkrnnb 067 BNNRKQRB 884 RBBQKRNN rbbqkrnn 078 NNRKQBBR 885 RQBBKRNN rqbbkrnn 074 NNRKBBQR 886 RQBKRBNN rqbkrbnn 070 NNBRKBQR 887 RQBKRNNB rqbkrnnb 066 BNNRKBQR 888 RBQKBRNN rbqkbrnn 077 NNRBKQBR 889 RQKBBRNN rqkbbrnn 073 NNRBBKQR 890 RQKRBBNN rqkrbbnn 069 NNBBRKQR 891 RQKRBNNB rqkrbnnb 065 BNNBRKQR 892 RBQKRNBN rbqkrnbn 076 NBNRKQBR 893 RQKBRNBN rqkbrnbn 072 NBNRBKQR 894 RQKRNBBN rqkrnbbn 068 NBBNRKQR 895 RQKRNNBB rqkrnnbb 064 BBNNRKQR 896 BBRKQRNN bbrkqrnn 063 NNRQKRBB 897 BRKBQRNN brkbqrnn 059 NNRQBKRB 898 BRKQRBNN brkqrbnn 055 NNBRQKRB 899 BRKQRNNB brkqrnnb 051 BNNRQKRB 900 RBBKQRNN rbbkqrnn 062 NNRQKBBR 901 RKBBQRNN rkbbqrnn 058 NNRQBBKR 902 RKBQRBNN rkbqrbnn 054 NNBRQBKR 903 RKBQRNNB rkbqrnnb 050 BNNRQBKR 904 RBKQBRNN rbkqbrnn 061 NNRBQKBR 905 RKQBBRNN rkqbbrnn 057 NNRBBQKR 906 RKQRBBNN rkqrbbnn 053 NNBBRQKR 907 RKQRBNNB rkqrbnnb 049 BNNBRQKR 908 RBKQRNBN rbkqrnbn 060 NBNRQKBR 909 RKQBRNBN rkqbrnbn 056 NBNRBQKR 910 RKQRNBBN rkqrnbbn 052 NBBNRQKR 911 RKQRNNBB rkqrnnbb 048 BBNNRQKR 912 BBRKRQNN bbrkrqnn 047 NNQRKRBB 913 BRKBRQNN brkbrqnn 043 NNQRBKRB 914 BRKRQBNN brkrqbnn 039 NNBQRKRB 915 BRKRQNNB brkrqnnb 035 BNNQRKRB 916 RBBKRQNN rbbkrqnn 046 NNQRKBBR 917 RKBBRQNN rkbbrqnn 042 NNQRBBKR 918 RKBRQBNN rkbrqbnn 038 NNBQRBKR 919 RKBRQNNB rkbrqnnb 034 BNNQRBKR 920 RBKRBQNN rbkrbqnn 045 NNQBRKBR 921 RKRBBQNN rkrbbqnn 041 NNQBBRKR 922 RKRQBBNN rkrqbbnn 037 NNBBQRKR 923 RKRQBNNB rkrqbnnb 033 BNNBQRKR 924 RBKRQNBN rbkrqnbn 044 NBNQRKBR 925 RKRBQNBN rkrbqnbn 040 NBNQBRKR 926 RKRQNBBN rkrqnbbn 036 NBBNQRKR 927 RKRQNNBB rkrqnnbb 032 BBNNQRKR 928 BBRKRNQN bbrkrnqn 031 NQNRKRBB 929 BRKBRNQN brkbrnqn 027 NQNRBKRB 930 BRKRNBQN brkrnbqn 023 NQBNRKRB 931 BRKRNQNB brkrnqnb 019 BNQNRKRB 932 RBBKRNQN rbbkrnqn 030 NQNRKBBR 933 RKBBRNQN rkbbrnqn 026 NQNRBBKR 934 RKBRNBQN rkbrnbqn 022 NQBNRBKR 935 RKBRNQNB rkbrnqnb 018 BNQNRBKR 936 RBKRBNQN rbkrbnqn 029 NQNBRKBR 937 RKRBBNQN rkrbbnqn 025 NQNBBRKR 938 RKRNBBQN rkrnbbqn 021 NQBBNRKR 939 RKRNBQNB rkrnbqnb 017 BNQBNRKR 940 RBKRNQBN rbkrnqbn 028 NBQNRKBR 941 RKRBNQBN rkrbnqbn 024 NBQNBRKR 942 RKRNQBBN rkrnqbbn 020 NBBQNRKR 943 RKRNQNBB rkrnqnbb 016 BBNQNRKR 944 BBRKRNNQ bbrkrnnq 015 QNNRKRBB 945 BRKBRNNQ brkbrnnq 011 QNNRBKRB 946 BRKRNBNQ brkrnbnq 007 QNBNRKRB 947 BRKRNNQB brkrnnqb 003 BQNNRKRB 948 RBBKRNNQ rbbkrnnq 014 QNNRKBBR 949 RKBBRNNQ rkbbrnnq 010 QNNRBBKR 950 RKBRNBNQ rkbrnbnq 006 QNBNRBKR 951 RKBRNNQB rkbrnnqb 002 BQNNRBKR 952 RBKRBNNQ rbkrbnnq 013 QNNBRKBR 953 RKRBBNNQ rkrbbnnq 009 QNNBBRKR 954 RKRNBBNQ rkrnbbnq 005 QNBBNRKR 955 RKRNBNQB rkrnbnqb 001 BQNBNRKR 956 RBKRNNBQ rbkrnnbq 012 QBNNRKBR 957 RKRBNNBQ rkrbnnbq 008 QBNNBRKR 958 RKRNNBBQ rkrnnbbq 004 QBBNNRKR 959 RKRNNQBB rkrnnqbb 000 BBQNNRKR ================================================ FILE: files/misc/escape_and_missing_quotes.pgn ================================================ [Event "Test \"quotes\" \\ and escaping and missing quotes in the FEN tag"] [Site "local"] [Date "2021.04.01"] [Round "1"] [White "Jimmy \"The Pawn\" Smith"] [Black "Lc0 \\ Stockfish"] [Result "*"] [FEN rnbqkb1r/ppp1pppp/5n2/3p4/3P1B2/5N2/PPP1PPPP/RN1QKB1R b KQkq - 3 3] [SetUp "1"] 3... c5 {EV: 47.0%, N: 63.14% of 63.4k} 4. e3 {EV: 53.3%, N: 95.14% of 66.9k} e6 {EV: 47.4%, N: 67.90% of 115k} 5. Nbd2 {EV: 53.1%, N: 67.58% of 96.2k} Qb6 {EV: 47.8%, N: 77.32% of 198k} 6. Rb1 {EV: 52.7%, N: 90.85% of 166k} * ================================================ FILE: files/misc/nibbler_menu_translations_template.json ================================================ { "LANGUAGE_NAME_FIXME": { // The language name itself, in its own language. "File": "TODO", // As in: "file menu" "About": "TODO", "New game": "TODO", "New 960 game": "TODO", // New game of Chess960 "Open PGN...": "TODO", // Portable game notation file "Load FEN / PGN from clipboard": "TODO", // Examines clipboard for valid FEN or PGN data "Save this game...": "TODO", // Saves as PGN "Write PGN to clipboard": "TODO", "PGN saved statistics": "TODO", // Has submenu of which statistics to save to PGN variations... "EV": "TODO", // ... expected value "Centipawns": "TODO", // ... "N (%)": "TODO", // ... N (node count, percent of total) "N (absolute)": "TODO", // ... N (absolute count) "...out of total": "TODO", // ... display total N "Depth (A/B only)": "TODO", // ... search depth, only displayed if alphabeta-style engine "Cut": "TODO", "Copy": "TODO", "Paste": "TODO", "Quit": "TODO", "Tree": "TODO", // Game tree manipulation functions "Play engine choice": "TODO", // Adds engine's choice of move to game tree / goes to it if already present "1st": "TODO", // ... i.e. play engine's top choice "2nd": "TODO", // ... i.e. play engine's 2nd choice "3rd": "TODO", // ... i.e. play engine's 3rd choice "4th": "TODO", // ... i.e. play engine's 3rd choice "Root": "TODO", // Move to root node in game tree "End": "TODO", // Move to final node in (this line of) game tree "Backward": "TODO", // Backward one move in game tree "Forward": "TODO", // Forward one move in game tree "Previous sibling": "TODO", // Go to previous sibling in game tree (if extant) "Next sibling": "TODO", // Go to next sibling in game tree (if extant) "Return to main line": "TODO", "Promote line to main line": "TODO", "Promote line by 1 level": "TODO", "Delete node": "TODO", "Delete children": "TODO", // i.e. child nodes in game tree "Delete siblings": "TODO", "Delete ALL other lines": "TODO", "Show PGN games list": "TODO", // For when a PGN file has multiple games "Escape": "TODO", // Escape key function - "escape" out of this screen to main screen "Analysis": "TODO", "Go": "TODO", // Starts the engine running "Go and lock engine": "TODO", // Starts the engine running on current position, and does NOT change the engine position even if GUI position changes "Return to locked position": "TODO", // Returns to the position the engine is analysing, IF the "go and lock engine" item was used "Halt": "TODO", // Stops the engine "Auto-evaluate line": "TODO", // Makes engine analyse a whole line of the game tree "Auto-evaluate line, backwards": "TODO", // Makes engine analyse a whole line of the game tree, backwards "Show focus (searchmoves) buttons": "TODO", // Shows buttons next to each legal move to allow the engine to focus analysis on selected moves "Clear focus": "TODO", // Deselects all such buttons "Invert focus": "TODO", // Inverts all such buttons "Winrate POV": "TODO", // Winrate (really EV) displayed from whose point of view? "Current": "TODO", // ... displayed from current player's "White": "TODO", // ... displayed from White's point of view "Black": "TODO", // ... displayed from Black's point of view "Centipawn POV": "TODO", // Centipawn scores displayed from whose point of view? (submenu not in this list) "Win / draw / loss POV": "TODO", // WDL statistic displayed from whose point of view? (submenu not in this list) "PV clicks": "TODO", // What to do when user clicks on a displayed principal variation (PV) "Do nothing": "TODO", // ... nothing "Go there": "TODO", // ... move to that position "Add to tree": "TODO", // ... add PV to game tree "Write infobox to clipboard": "TODO", "Forget all analysis": "TODO", "Display": "TODO", // Has large submenu of display options "Flip board": "TODO", "Arrows": "TODO", "Piece-click spotlight": "TODO", // Displays arrows showing legal moves, when a piece is clicked "Always show actual move (if known)": "TODO", // Displays arrow representing move actually played in the game record (if known) "...with unique colour": "TODO", // ... displays that arrow with a unique colour "...with outline": "TODO", // ... and an outline "Arrowhead type": "TODO", // Arrows display what statistic? "Winrate": "TODO", // ... (really expected value) "Node %": "TODO", // ... What % of examined nodes (examined by the engine) were this move? "Policy": "TODO", // Policy / prior "MultiPV rank": "TODO", // Ordinal rank in engine's preferred moves "Moves Left Head": "TODO", // Output of engine neural network head estimating "moves left before game ends" "Arrow filter (Lc0)": "TODO", // Filter criteria for displaying / not displaying arrows (when using Lc0 engine) "All moves": "TODO", // ... always display all arrows "Top move": "TODO", // ... display arrow for top move only "Arrow filter (others)": "TODO", // Filter criteria for displaying / not displaying arrows (when using Stockfish or similar) "Diff < 15%": "TODO", // ... EV was within 15% of best move "Diff < 10%": "TODO", // ... EV was within 10% of best move "Diff < 5%": "TODO", // ... EV was within 5% of best move "Infobox stats": "TODO", // Stats to display in info pane "N - nodes (%)": "TODO", // ... "N - nodes (absolute)": "TODO", // ... "P - policy": "TODO", // ... "V - static evaluation": "TODO", // ... "Q - evaluation": "TODO", // ... "U - uncertainty": "TODO", // ... "S - search priority": "TODO", // ... "M - moves left": "TODO", // ... "WDL - win / draw / loss": "TODO", // ... "Linebreak before stats": "TODO", "PV move numbers": "TODO", // Display move numbers when writing principal variation e.g. 40. Kc2 Qf1 41. Rd8 etc etc "Online API": "TODO", // Access an online API to display real-world game outcome stats "None": "TODO", // ... Don't access API "ChessDB.cn evals": "TODO", // ... Use ChessDB.cn "Lichess results (masters)": "TODO", // ... Use results from Lichess master player database "Lichess results (plebs)": "TODO", // ... Use results from Lichess user database "Allow API after move 25": "TODO", "Draw PV on mouseover": "TODO", "Draw PV method": "TODO", // How to draw a PV when mouse-over-ing part of it "Animate": "TODO", // Animate a series of moves "Single move": "TODO", // Show exactly the position after the move being hovered over "Final position": "TODO", // Show the final position in the sequence "Pieces": "TODO", // Icons / images to use for piece display "Choose pieces folder...": "TODO", // ... Select folder containing such images "Default": "TODO", // ... Just use default images "About custom pieces": "TODO", // Displays text explaining how to create custom images "Background": "TODO", // Background to use for board display "Choose background image...": "TODO", // ... Select image file for background "Book frequency arrows": "TODO", // Dispay arrows representing move frequency in opening book file "Lichess frequency arrows": "TODO", // Dispay arrows representing move frequency in Lichess database "Sizes": "TODO", // Adjust sizes of displayed things "Infobox font": "TODO", // Adjust SIZE of font in info pane "Move history font": "TODO", // Adjust SIZE of font in game tree pane "Board": "TODO", // Adjust SIZE of board "Giant": "TODO", // ... "Large": "TODO", // ... "Medium": "TODO", // ... "Small": "TODO", // ... "Graph": "TODO", // Adjust SIZE (height) of graph displaying winrate "Graph lines": "TODO", // Adjust SIZE of graph line "I want other size options!": "TODO", // Displays text explaining how to set custom size options "Engine": "TODO", // Top level menu item for engine settings "Choose engine...": "TODO", // Choose engine via file picker "Choose known engine...": "TODO", // Choose engine from a list of previously used engines "Weights": "TODO", // Choose neural net weight file "Lc0 WeightsFile...": "TODO", // ... For Lc0 "Stockfish EvalFile...": "TODO", // ... For Stockfish "Set to ": "TODO", // ... Just allow engine to use default "Backend": "TODO", // Backend choices for Lc0 engine (submenu not in this list) "Choose Syzygy path...": "TODO", // Find endgame tablebase folder "Unset": "TODO", // Clear endgame tablebase folder "Limit - normal": "TODO", // Node limit for engine in normal circumstances "Unlimited": "TODO", // ... no node limit (note: other options exist but are not in this list) "Up slightly": "TODO", // ... increase node limit slightly "Down slightly": "TODO", // ... decrease node limit slightly "Limit - auto-eval / play": "TODO", // Node limit for engine when playing or evaluating a game "Limit by time instead of nodes": "TODO", // Consider node limit setting as a time limit in milliseconds instead "Threads": "TODO", // How many threads engine should used "Warning about threads": "TODO", // ... Displays a text warning about when not to use too many threads "Hash": "TODO", // Hash size (submenu not in this list) "I want other hash options!": "TODO", // ... Displays text about how to have a custom hash setting "MultiPV": "TODO", // How many lines a Stockfish-like engine should consider at once (multipv setting) "Contempt Mode": "TODO", "White analysis": "TODO", // ... Contempt from White's point of view "Black analysis": "TODO", // ... Contempt from Black's point of view "Contempt": "TODO", // Numerical contempt value (submenu not in this list) "WDL Calibration Elo": "TODO", // Technical contempt setting "Use default WDL": "TODO", // ... Technical contempt setting "WDL Eval Objectivity": "TODO", // Technical contempt setting "Yes": "TODO", // ... "No": "TODO", // ... "Score Type": "TODO", // Technical contempt setting "Custom scripts": "TODO", "How to add scripts": "TODO", // ... Displays text explanation "Show scripts folder": "TODO", // ... Displays folder in filesystem "Restart engine": "TODO", "Soft engine reset": "TODO", // Tells engine to reset cache etc, without actually restarting "Play": "TODO", // Top level menu item for playing against the engine "Play this colour": "TODO", // Engine itself plays current colour from now on "Start self-play": "TODO", // Engine plays against itself "Use Polyglot book...": "TODO", "Use PGN book...": "TODO", "Unload book / abort load": "TODO", "Book depth limit": "TODO", // What depth to stop using the book "Temperature": "TODO", // Temperature for Lc0 neural network "Temp Decay Moves": "TODO", // Technical temperature-related setting (numerical options present, but not in this list) "Infinite": "TODO", // ... "About play modes": "TODO", // Displays text "Dev": "TODO", // Top level menu for Dev / technical settings "Toggle Developer Tools": "TODO", "Toggle Debug CSS": "TODO", // Shows red border around HTML elements "Permanently enable save": "TODO", // Allows save function to be used "Show config.json": "TODO", // Displays config file in filesystem "Show engines.json": "TODO", // Displays config file in filesystem "Reload engines.json (and restart engine)": "TODO", "Random move": "TODO", // Makes a random move from legal possibilities "Disable hardware acceleration for GUI": "TODO", "Spin rate": "TODO", // How frequently to update GUI "Frenetic": "TODO", // ... "Fast": "TODO", // ... "Normal": "TODO", // ... "Relaxed": "TODO", // ... "Lazy": "TODO", // ... "Show engine state": "TODO", // Shows debug info about engine "List sent options": "TODO", // Shows a list of all options ever sent to the engine "Show error log": "TODO", // Shows engine stderr "Hacks and kludges": "TODO", // Submenu with specific hacks "Allow arbitrary scripts": "TODO", // ... "Accept any file size": "TODO", // ... "Allow stopped analysis": "TODO", // ... "Never hide focus buttons": "TODO", // ... "Never grayout move info": "TODO", // ... "Use lowerbound / upperbound info": "TODO", // ... "Suppress ucinewgame": "TODO", // ... do not send ucinewgame token to engine "Log RAM state to console": "TODO", "Fire GC": "TODO", // Call JS garbage collector "Logging": "TODO", "Use logfile...": "TODO", // ... "Disable logging": "TODO", // ... "Clear log when opening": "TODO", // ... "Use unique logfile each time": "TODO", // ... "Log illegal moves": "TODO", // ... "Log positions": "TODO", // ... "Log info lines": "TODO", // ... "...including useless lines": "TODO", // ... "Language": "TODO", "RESTART_REQUIRED": "TODO" // Special item - translate from "The GUI must now be restarted." } } ================================================ FILE: files/misc/pathological.pgn ================================================ [Event "Jeopardy Match 12"] [Site "brinanSee"] [Date "2019.06.15"] [Round "4"] [White "lc0.net.42518"] [Black "Stockfish.dev 19061000"] [Result "1/2-1/2"] [ECO "D31"] [Opening "Semi-Slav"] [Variation "Marshall Gambit, 8.Ne2"] [TimeControl "5100+1"] [Termination "normal"] [PlyCount "304"] 1. d4 (d4 Nf6 c4 e6 g3 Bb4 Bd2 Be7 Bg2 d5 Nf3 O-O O-O c6 Qc2 Nbd7 Rd1 b6 Bf4 Ba6 Nbd2 Nh5 Be3 Rc8 Rac1 h6 Ne5 Nhf6 Qa4 Nb8 Qb3 Nbd7 h3 Bb7 Qa4 a6 cxd5) {+0.27/13 89} d5 {0.00/1} 2. c4 (c4 e6 Nc3 Nf6 cxd5 exd5 Bg5 Be7 e3 h6 Bh4 O-O Bd3 c6 Nge2 Ne4 Bxe7 Qxe7 Bxe4 dxe4 O-O Nd7 d5 Nf6 dxc6 bxc6 Qa4 Rb8) {+0.32/13 52} e6 {0.00/1 0} 3. Nc3 (Nc3 Nf6 cxd5 exd5 Bg5 Be7 e3 h6 Bh4 O-O Bd3 c6 Nge2 Ne4 Bxe7 Qxe7 Bxe4 dxe4 O-O Nd7 d5 f5 Nf4 Ne5 Qb3 Qf7 Rfd1) {+0.32/15 62} c6 {0.00/1 0} 4. e4 (e4 dxe4 Nxe4 Bb4 Bd2 Qxd4 Bxb4 Qxe4 Ne2 Ne7 Qd2 c5 Bxc5 Nbc6 Rd1 O-O Qf4 Qxf4 Nxf4 b6 Ba3 Re8 f3 e5 Nd5 Nxd5 cxd5 Nd4 Kf2 Rd8 f4 Bg4 Rd2 Rxd5 Bc4 Rd7 fxe5 Rc8 b3 b5) {+0.42/18 94} dxe4 {0.00/1 0} 5. Nxe4 (Nxe4 Bb4 Bd2 Qxd4 Bxb4 Qxe4 Ne2 Nd7 Qd6 Qe5 O-O-O Qxd6 Bxd6 Nh6 Ng3 f5 Bd3 Kf7 Rhe1 Re8 Bc2 g6 h3 Kg7 Bb4 e5 f4 e4) {+0.41/20 41} Bb4+ {0.00/1 0} 6. Bd2 (Bd2 Qxd4 Bxb4 Qxe4 Ne2 Nd7 Qd6 Qe5 O-O-O Qxd6 Bxd6 b6 g4 Bb7 Bg2 O-O-O Rhe1 Ngf6 g5 Ng4 Ng3 c5 Bxb7 Kxb7 Ne4 f5 gxf6 Ndxf6 Nxf6 gxf6 Rxe6 Nxf2 Rd2 Ng4) {+0.38/20 63} Qxd4 {0.00/1} 7. Bxb4 (Bxb4 Qxe4 Ne2 Nd7 Qd6 Qe5 O-O-O Qxd6 Bxd6 b6 g4 Bb7 Bg2 O-O-O Rhe1 Ngf6 g5 Ng4 Ng3 c5 Bxb7 Kxb7 Ne4 f5 gxf6 Ndxf6 Nxf6 gxf6 Rxe6 Nxf2 Rd2 Ng4) {+0.38/15 3} Qxe4+ {0.00/1 0} 8. Ne2 (Ne2 Nd7 Qd6 Qe5 O-O-O Qxd6 Bxd6 Nh6 Ng3 f5 Bd3 g6 Rhe1 Kf7 Bc2 Re8 f4 Kg8 Bb4 Nf7 Bc3 Kf8 Nf1 e5 fxe5 Ndxe5 b3) {+0.32/18 117} Nd7 {0.00/1 0} 9. Qd6 (Qd6 Qe5 O-O-O Qxd6 Bxd6 Nh6 Ng3 f5 Bd3 g6 Rhe1 Kf7 Bc2 Re8 f4 b6 Ne2 Ba6 b3 c5 Ng1 Bb7 Nh3 Kg8 Ng5) {+0.28/18 58} Qe5 {0.00/1 0} 10. O-O-O (O-O-O Qxd6 Bxd6 Nh6 Ng3 f5 Bd3 g6 Rhe1 Kf7 Nh1 Re8 f3 Kg8 Nf2 Nf7 Bb4 e5 Bc2 a5 Bc3 Nc5 b3 b6 h4 Ra7 h5 Rae7) {+0.23/16 154} Qxd6 {0.00/1 0} 11. Bxd6 (Bxd6 Nh6 Ng3 f5 Bd3 g6 Rhe1 Kf7 Nh1 Re8 f3 Kg8 Nf2 e5 Bc2 Nf7 Bb4 a5 Bc3 Nc5 b3 b6 h4 Ra7 h5 Rae7) {+0.22/15 19} Nh6 {0.00/1 0} 12. Ng3 (Ng3 f5 Bd3 g6 Rhe1 Kf7 Nh1 Re8 f3 Kg8 Bb4 e5 Nf2 Nf7 Bc2 a5 Bc3 Nc5 b3 b6 h4 Ra7 h5 Rae7 hxg6) {+0.19/14 175} f5 {0.00/1 0} 13. Bd3 (Bd3 g6 Rhe1 Kf7 Nh1 Re8 f3 Kg8 Bb4 a5 Bc3 e5 Nf2 Nf7 Bc2 Nc5 b3 b6 h4 Ra7 h5 Rae7 hxg6 hxg6) {+0.17/14 89} g6 {0.00/1 0} 14. Rhe1 (Rhe1 Kf7 Nh1 Re8 f3 Kg8 Bb4 a5 Bc3 e5 Nf2 Nf7 Bc2 Nc5 Bd4 Nd7 Bc3 Nc5 Bd4 Nd7 Bc3) {+0.14/14 146} Kf7 {0.00/1 0} 15. Nh1 (Nh1 Re8 f3 Kg8 Bb4 a5 Bc3 e5 Nf2 Nf7 Bc2 Nc5 b3 b6 h4 Ra7 h5 Rae7 hxg6 hxg6 g4 Kf8 Rg1) {+0.12/13 271} a5 (a5 f3 Re8 Ba3 Kg7 h3 Nf7 g4 Nf6 b3 Ng5 Bb2 Kf7 f4 Nxh3 gxf5 gxf5 Rf1 Rg8 Rf3 Ng1 Rff1) {0.00/41 680} 16. Bf4 (Bf4 Ng4 f3 Ngf6 Nf2 e5 Bd2 Re8 Bc3 a4 Bc2 b5 g4 bxc4 gxf5 gxf5 Bxf5 Nc5 Bc2 a3 bxa3 Nd5 Bb4 Nxb4 axb4 Na6 a3) {+0.33/12 86} Ng8 (Ng8 f3 Ngf6 Nf2 Re8 Bg5 a4 g4 b5 cxb5 cxb5 gxf5 gxf5 Bxb5 Ra5 Bxf6 Rxb5 Bc3 e5 Rd6 Re6 Rxe6 Kxe6 Nd3 Bb7 Nxe5 Nxe5 f4 Rc5 Rxe5 Rxe5 Bxe5 Bd5 b3 axb3 axb3 Bxb3 Kb2 Bd5 Kc3 h6 Kb4) {+0.05/35 68} 17. f3 (f3 Ngf6 Nf2 e5 Bd2 Re8 Bc3 a4 h3 h5 h4 e4 fxe4 Nc5 e5 Ng4 Nh3 Be6 Bb4 Nxd3 Rxd3 Bxc4 Rd7 Kg8 Bc3 b5 a3 c5 Rd6 b4 axb4) {+0.36/14 57} Ngf6 (Ngf6 Nf2 Re8 Bd6 b6 g4 Ba6 Nh3 Kg8 Ng5 fxg4 fxg4 e5 h3 b5 Ne4 bxc4 Nxf6 Nxf6 Bc2 Bb7 Bxe5 Nd5 Bd4 Ba6 a3 Rad8 Bc5 Rxe1 Rxe1 Nf4 Bb6 Rd3 Be3 Ng2 Bxd3 cxd3 Bd2 Nxe1 Bxe1 Kf7 Bxa5 Ke6 Kd2) {+0.01/38 53} 18. Nf2 (Nf2 e5 Bd2 Re8 Bc3 a4 h3 h5 h4 e4 fxe4 Nc5 e5 Ng4 Nh3 Be6 Bb4 Nxd3 Rxd3 Bxc4 Rd7 Kg8 Bc3 Bxa2 Nf4 Be6 Rxb7 Nf2 Rc7 a3 bxa3) {+0.40/15 14} Re8 (Re8 Bd6 b6 g4 Ba6 Nh3 Kg8 Ng5 fxg4 fxg4 e5 h3 b5 Ne4 bxc4 Nxf6 Nxf6 Bc2 Nd5 Bxe5 Bb7 Bd4 Rad8 Bc5 Rxe1 Rxe1 Kf7 Rf1 Kg8 Re1) {0.00/37 51} 19. Bd6 (Bd6 b5 cxb5 cxb5 Bxb5 Ba6 Ba4 Rec8 Kb1 Nb6 Bb3 Nfd5 Nh3 h6 Nf4 Bc4 Bxc4 Rxc4 Nd3 Nd7 Rc1 Rd4 Red1 Rd8 Bg3 Ne3 Rd2 Rd5 Bc7 Rc8 Bxa5 Nc4 Rdc2 Na3 bxa3 Rxc2 Kxc2) {+0.23/15 128} b6 (b6 g4 Ba6 Nh3 Kg8 Ng5 fxg4 fxg4 Nxg4 Bf1 e5 Bc7 Bc8 Rd6 Ra7 Rxc6 Bb7 Rd6 Bc8) {0.00/41 0} 20. g4 (g4 Ba6 Bf1 Ra7 Nh3 e5 gxf5 gxf5 Bd3 h6 b3 e4 fxe4 Nxe4 Bxe4 Rxe4 Rxe4 fxe4 Rf1 Ke6 Bg3 b5 Nf4 Ke7 Re1 Nf6 Bh4 Kf7 Rf1) {+0.40/12 109} Ba6 (Ba6 h4 h5 gxh5 Nxh5 Nh3 e5 Ng5 Kg7 Ba3 b5 cxb5 cxb5 b4 axb4 Bxb4 Ra7 Bc2 Bc8 Bb3 Nb6 Bd6 Kf6 Kb1 Nc4 Bxc4 bxc4 Rd5 e4 fxe4 Kg7 Ka1 Ra6 Re5 Rxe5 Bxe5 Kf8 Kb2 Rb6 Kc2 Ra6 Rd1 Ke7 Kc3 Rxa2) {-0.23/36 65} 21. b3 (b3 Bb7 g5 Nh5 Be2 a4 Bc7 Bc8 f4 e5 fxe5 axb3 axb3 Nf4 Bf3 Ra7 Bd6 Ra2 Nd3 Nxd3 Rxd3 Ra1 Kd2 Rxe1 Kxe1 Nxe5 Bxe5 Rxe5 Kf2 f4 Rd6 Rxg5 Rxc6 Be6 Rxb6) {+0.37/14 165} b5 (b5 Nh3 Kg8 Ng5 fxg4 fxg4 bxc4 bxc4 Nxg4 Bg3 Nc5 Bc2 e5 h3 Nh6 Rd6 Nf7 Nxf7 Kxf7 Rxe5 Rxe5 Bxe5 Bxc4 Rf6 Ke7 Rxc6 Nd3 Bxd3 Bxd3 Bc3 Kd7 Rc5 Bf5 Rxa5 Rxa5 Bxa5 Bxh3 a4 Kc6 Be1) {+0.40/32 44} 22. g5 (g5 bxc4 bxc4 Ng8 c5 Ne7 Bxa6 Rxa6 Nd3 Nd5 Kd2 Nb4 a3 Nxd3 Kxd3 Ra7 Rb1 e5 Kc4 e4 fxe4 Ne5 Bxe5 Rxe5 exf5 Rxf5 Rf1 Re7 Rxf5 gxf5 Rb6 Re6 Kd3 Kg6) {+0.16/13 98} Nh5 (Nh5 Bf1 bxc4 bxc4 Ra7 Be5 c5 Rd6 Re7 Bc3 Nf4 Red1 e5 Nd3 Nxd3 R1xd3 Bxc4 Rxd7 Raxd7 Rxd7 Bxf1 Rxe7 Kxe7 Bxe5 Ke6 f4 Bc4 Kb2 Kd5 a4 Ke4 Kc3 Bd5 h4 Kf3 Bc7 Kg4 Bd6 Kg3 Bc7 Kxh4 Bxa5 Kg3) {+0.58/33 52} 23. c5 (c5 e5 b4 Nf4 Bc2 Ng2 Re2 Nf4 Red2 Ne6 Bb3 axb4 Re1 Kg7 Nd3 Nxg5 Nxe5 Nh3 Rdd1 Nxe5 Bxe5 Kh6 Rd4 f4 Bf7 Red8 Rd6 Rxd6) {+0.57/16 89} e5 (e5 Bc2 b4 Nd3 Bxd3 Bxd3 Ra7 Bc4 Kg7 Rd4 h6 h4 Kh7 Kb2 Kg7) {0.00/43 188} 24. b4 (b4 Nf4 Bc2 Ng2 Re2 Nf4 Red2 axb4 Nd3 Ne6 Bb3 Kg7 Re1 Nd4 Bxe5 Nxe5 Nxe5 Nxb3 axb3 Bc8 Kb2 f4 Rde2 Bf5 Nxc6 Rxe2 Rxe2 Rc8 Nxb4 Rxc5 Re7 Kf8 Rxh7 Be6 h4 Bf7 Nd3 Rf5 Kc3 Kg8 Rh6 Kg7 b4 Bc4 Nc5 Bd5 Ne4 Bxe4 fxe4 Rf8 Kd2 Re8 Kd3 f3 h5) {+0.57/33 0} Nf4 (Nf4 Bc2 Ng2 Re2 Nf4) {0.00/42 151} 25. Bc2 (Bc2 Ng2 Re2 Nf4 Red2 axb4 Nd3 Ne6 Bb3 Kg7 Re1 Nd4 Bxe5 Nxe5 Nxe5 Nxb3 axb3 Bc8 Kb2 f4 Rde2 Bf5 Nxc6 Rxe2 Rxe2 Rc8 Nxb4 Rxc5 Re7 Kf8 Rxh7 Be6 h4 Bf7 Nd3 Rf5 Kc3 Kg8 Rh6 Kg7 b4 Bc4 Nc5 Bd5 Ne4 Bxe4 fxe4 Rf8 Kd2 f3 Ke1 Rf4 h5 gxh5 Rxh5) {+0.55/24 61} Ng2 (Ng2 Re2 Nf4 Ree1) {0.00/43 14} 26. Re2 (Re2 Nf4 Ree1 Ng2 Re2 Nf4 Red2 axb4 Nd3 Ne6 Bb3 Kg7 Re1 Nd4 Bxe5 Nxe5 Nxe5 Nxb3 axb3 Bc8 Kb2 f4 Rde2 Bf5 Nxc6 Rxe2 Rxe2 Rc8 Nxb4 Rxc5 Re7 Kf8 Rxh7 Be6 h4 Bf7 Nd3 Rf5 Kc3 Kg8 Rh6 Kg7 b4 Bc4 Nc5 Bd5 Ne4 Bxe4 fxe4 Rf8 Kd2 f3 Ke1 Rf4) {+0.55/22 19} Nf4 (Nf4 Ree1 Ng2 Re2) {0.00/45 57} 27. Red2 (Red2 axb4 Nd3 Ne6 Bb3 Kg7 Re1 Nd4 Nxe5 Nxb3 axb3 Bc8 Kb2 Nxe5 Bxe5 Kg8 Rd6 Be6 Rxc6 Bd5 Rc7 Bxf3 Rg7 Kf8 Rxh7 Be4 Bf6 f4 Rh4 Bf5 Rxe8 Rxe8 Rxf4 Re2 Ka1 Rc2 Rxb4 Rxc5 Kb2 Rc2 Ka3 Bd3 Rd4 Bf5 h4 Re2 Rd5) {+0.53/26 250} a4 (a4 Nd3 Nd5 Re1 Re6 a3 Rae8 Rde2 Bc8 h4 e4 fxe4 Rxe4 Rxe4 fxe4 Nf4 Nxf4 Bxf4 e3 Bb1 Ne5 Ba2 Nc4 Bxc4 bxc4 Rxe3 Rxe3 Bxe3 Ke6 Kd2 Kd5 Kc3 Bf5 Bc1 Bd3 Bd2 Be2 Be3 Bf3 Bd4 Bg4 Bf6 Be2 Kd2 Bf3 Ke3 Bg4 Kf4 Be6 Bc3 Bd7 Ke3 Bf5 Kf4) {0.00/42 311} 28. a3 (a3 Nd5 Bb1 Nc3 Ba2 Nxa2 Rxa2 Re6 Re2 Rae8 Nd3 Bc8 h4 Kg8 Rde1 e4 Nf4 Ne5 fxe4 fxe4 Nxe6 Nd3 Kd2 Bxe6 Rf1 Bf5 Kc3 Kf7 Kd4 Kg7 h5) {+1.00/14 112} Bc8 (Bc8 Re1 Ng2 Rg1 Nf4 Bb1 Nf8 Re1 Be6 Rxe5 Rac8 Nd3 Nxd3 Bxd3 Bb3 Rde2 Rxe5 Bxe5 Ne6 f4 Rd8 Re3 Bd5 Kc2 Rd7 Bd6 Rd8 Kc3 Re8 Be5 Rd8 Rh3 Kg8 Bd6 Re8 Rg3 Kf7 Be5 Re7 Re3 Re8 Kc2 Bb3 Kd2 Bd5 Re2 Bb3 Kc3 Rd8 Rd2 Bd5 Be2 Be4 Rxd8 Nxd8 h4) {-0.50/42 440} 29. Bb1 (Bb1 Nf8 Re1 Be6 Rxe5 Kg8 Kb2 Rad8 h4 Nd5 Rde2 Nf4 Re1 Ng2 R1e2 Nf4 Rd2 Nd5 Ba2 Nf4 Rxe6 N4xe6 Nd3 Kg7 Be5 Kg8 Bf6 Rd7 Kc3 h6 Ne5 Rxd2 Kxd2 hxg5 hxg5 Nh7 Nxc6 Nxf6 gxf6 Kf7) {+1.08/20 82} Nf8 (Nf8 Re1 Be6 Rxe5 Rac8 Nd3 Nxd3 Bxd3 Bb3 f4 Rcd8 Ree2 Ne6 Rf2 Rd7 h4 Nd8 Be5 Ne6 Be2 Rxd2 Kxd2 Bd5 Rh2 Re7 Bd3 Bb3 Ke3 Nf8 Re2 Nd7 Kd4 Nxe5 Rxe5 Rd7 Kc3 Bd5 h5 Rd8 h6 Rd7 Bc2 Re7 Kd4 Rd7 Ke3 Bc4 Kf3 Bd5 Kg3) {-0.54/41 150} 30. Re1 (Re1 Be6 Rxe5 Kg8 Kb2 Rad8 h4 Nd5 Ba2 Nf4 Rxe6 N4xe6 Nd3 Kg7 Ne5 Rc8 f4 h6 Bxf8 Nxf8 Kc3 Rc7 Rd6 Ree7 Kd3 Re8 Nxc6 hxg5 hxg5) {+1.14/17 0} Be6 (Be6 Rxe5 Rac8 Nd3 Nxd3 Bxd3 Bb3 f4 Ne6 Re3 Ng7 Ree2 Rcd8 Kb2 Rd7 Rxe8 Nxe8 Be5 Ke6 Kc3 Nc7 Bxc7 Rxc7 Be2 Bd5 Rd3 h5 gxh6 Rh7 Rh3 Kf7 Bf1 Kg8 Bd3 Re7 h7 Rxh7 Rxh7 Kxh7 Be2 Kg7 Kd4 Be4 Ke5 Kf7 h4 Ke7 h5 gxh5 Bxh5 Kd8 Kf6 Kc7 Kg5) {-0.67/42 179} 31. Rxe5 (Rxe5 Kg8 Kb2 Rad8 h4 Nd5 Ba2 Nf4 Rxe6 N4xe6 Nd3 Kh8 Ne5 Rc8 Bxf8 Nxf8 Nf7 Kg7 Nd6 Rcd8 Nxe8 Rxe8 Rd6 Re2 Kb1 Re1 Kc2 Re2 Rd2 Re3 Kb2 Rxf3 Rd6 Rf2 Kb1 Rf1 Kc2 Rf2 Rd2 Rxd2 Kxd2 Nd7 Ke3 Ne5) {+1.19/19 0} Rac8 (Rac8 Nd3 Nxd3 Bxd3 Bb3 f4 Ne6 Re3 Ng7 Ree2 Rcd8 Kb2 Rd7 Rxe8 Nxe8 Be5 Ke6 Kc3 Nc7 Bxc7 Rxc7 Be2 Bd5 Rd3 h5 gxh6 Rh7 Rh3 Kf7 Bf1 Kg8 Kd4 Re7 h7 Kh8 Bd3 Be4 Kc3 Bxd3 Kxd3 Re4 Rh6 Rxf4 Rxg6 Rf3 Kd4 Rxa3 Rxc6 Rb3 Rc7 Rxb4 Kd5 Rg4 Rf7 Rc4) {-0.70/40 94} 32. Rd4 (Rd4 Nd5 Kd2 Nd7 Re1 Kg7 Nd3 Bf7 Be5 Kg8 Ba2 Nf8 h4 Re7 Bd6 Rxe1 Nxe1 Ne6 Rd3 Nef4 Bxf4 Nxf4 Bxf7 Kxf7 Rd7 Ke6 Rd6 Ke7 Nd3 Nxd3 Kxd3) {+1.27/16 209} Nd5 (Nd5 Kd2 Nd7 Re1 Kg7 Nd3 Bf7 f4 Rcd8 Ne5 Nxe5 Bxe5 Kg8 Kc1 Re7 Rd3 Red7 Red1 Be6 Ba2 Kf8 Kb2 Bf7 R1d2 Kg8 Bb1 Be6 Re2 Bf7 Rc2 Be6 Rc1 Bf7 Ba2 Be6 Rcd1 Re8 R1d2 Bf7 Bb1 Rde7 Bc2 h6 gxh6 Kh7) {-0.89/36 88} 33. Kd2 (Kd2 Nd7 Re1 Kg7 Nd3 Bf7 Be5 Kg8 Ba2 Nf8 h4 Rcd8 Bd6 Rxe1 Nxe1 Ne6 Rd3 Ng7 Be5 Nh5 Nc2 Re8 Bxd5 Bxd5 Rxd5 cxd5 f4 Kf7 Nd4 Nxf4 Bxf4 Re4) {+1.30/16 12} Kg7 (Kg7 Nd3 Nd7 Re1 Bf7 f4 Rcd8 Ne5 Nxe5 Bxe5 Kg8 Kc1 Rd7 Rd3 Red8 Red1 Be6 Ba2 Kf7 R3d2 Kf8 h4 Kg8 Kb2 Bf7 Rd4 Kf8 R1d2 Kg8 Kc1 Re8 R4d3 Red8 Rd1 Kf8 Bb1 Ke8 Re1 Re7 Rd2 Kd7 h5 Kc8 Rh1 Red7) {-0.99/40 80} 34. Bd3 (Bd3 Nd7 Re2 h6 h4 Bf7 Nd1 hxg5 hxg5 Rxe2 Bxe2 Re8 f4 Rh8 Bf3 Rh2 Ke1 Be6 Rd3 Kf7 Ne3 Rh3 Kf2 Nxe3) {+1.38/14 348} Nd7 (Nd7 Re2 h6 h4 hxg5 hxg5 Bf7 Nd1 Nc7 Rxe8 Nxe8 Bg3 Nf8 Be5 Kg8 Nf2 Bb3 Rh4 Ng7 Be2 Rd8 Kc3 Re8 f4 Nd7 Bf3 Nxe5 fxe5 Rxe5 Nd3 Re3 Bxc6 Bc4 Rxc4 bxc4 Kxc4 Ne6 Bxa4 Kf8 Bb3 Ke7 a4 Nxg5 a5) {-1.17/36 237} 35. Re2 (Re2 h6 h4 Bf7 Nd1 Nf8 Rxe8 Rxe8 f4 hxg5 hxg5 Nd7 Be2 Rh8 Bf3 Rh2 Ke1 Be6 Rd3 Rh3 Kf2 Kf7 Kg2 Rh8 Nc3 Nxc3) {+1.52/15 0} h6 (h6 f4 hxg5 fxg5 Rcd8 Nd1 Kf7 Nc3 N7b6 cxb6 Rxd6 Nxd5 Red8 Nc7 Rxd4 Nxe6 Rxd3 Kc2 R3d7 Nxd8 Rxd8 Rd2 Rb8 Rd7 Ke6 Rg7 Rxb6 Rxg6 Ke5 h4 Rb8 Rxc6 Rh8 Rh6 Rc8 Kd3 Rc4 h5 Rg4 g6 Rg3 Ke2 f4 Rh7 Re3 Kf2 Rxa3 Rb7 Rb3 Rxb5 Kf6 Ra5 Rxb4) {-1.13/36 124} 36. h4 (h4 hxg5 hxg5 Rcd8 Nd1 Nf8 f4 Kf7 Rh2 Ne7 Nc3 Nc8 Be5 Rxd4 Bxd4 Ne7 Bf6 Nd5 Be5 Nxc3 Kxc3 Bd5 Rh6 Bf3 Bb1 Bd5 Rh8 Ne6) {+1.55/14 41} hxg5 (hxg5 hxg5 Kg8 Nh3 Bf7 f4 Nf8 Ng1 Red8 Rh2 Rxd6 cxd6 Rd8 Rh4 Kg7 Be2 Ne6 Rxd5 cxd5 Bxb5 Rxd6 Ne2 Rd8 Bd3 Rd6 Rh1 Be8 Rb1 Kf7 b5 Rb6 Rb4 Rb7 Nc3 d4 Ne2 Ke7 b6 Bc6 Bc4 Kd6) {-1.23/34 52} 37. hxg5 (hxg5 Rcd8 Nd1 Kf7 Rh2 Nf8 f4 Ne7 Nc3 Nc8 Be5 Rxd4 Bxd4 Ne7 Be5 Rd8 Ke3 Nd5 Nxd5 Bxd5 Bd6 Re8 Kd2 Be4 Be2 Bd5 Rh8 Kg7) {+1.61/14 89} Kg8 (Kg8 Nh3 Bf7 f4 Nf8 Ng1 Red8 Rh2 Rxd6 cxd6 Rd8 Rh4 Kg7 Be2 Ne6 Rxd5 cxd5 Bxb5 Rxd6 Ne2 Rd8 Rh3 Be8 Bd3 d4 Bc4 Nc7 Rd3 Bb5 Nxd4 Bxc4 Nxf5 gxf5 Rxd8 Bf7 Ke3 Nd5 Kf3) {-0.95/34} 38. Nd1 (Nd1 Kf7 Rh4 Rh8 Reh2 Rxh4 Rxh4 Kg7 Be2 Bg8 Rd4 Be6 Ne3 Nxe3 Kxe3 Re8 Kf2 Kf7 f4 Rh8 Bf3 Rc8 Rd1 Bb3 Rh1 Kg7 Re1 Kg8 Re7) {+1.68/14 163} Nf8 (Nf8 Rh2 Rcd8 Nc3 Nxc3 Kxc3 Bb3 Rdh4 Kg7 Kd2 Bg8 Bf1 Bd5 f4 Kf7 Bd3 Rxd6 cxd6 Rd8 Kc3 Rxd6 Rh6 Rd7 Rd2 Kg7 Rh4 Kf7 Rh3 Kg8 Bf1 Kf7 Rh6 Rd6 Bg2 Ne6 Rh7 Kg8 Rh4 Kf8 Bf3 Ke7 Rh7 Kd8 Bxd5 cxd5 Rf2) {-1.61/37 182} 39. f4 (f4 Ne7 Nc3 Kf7 Rh2 Rcd8 Rh3 Nc8 Be5 Rxd4 Bxd4 Ne7 Be5 Rd8 Ke1 Bb3 Bd6 Nc8 Bc7 Rd7 Be5 Ne7 Bf1 Nd5 Bg2 Rd8 Nxd5) {+1.70/13 254} Ne7 (Ne7 Nc3 Kf7 Rh2 Bb3 Bf1 Ne6 Rd3 Rh8 Rdh3 Rxh3 Rxh3 Nd5 Nxd5 Bxd5 Rh7 Kg8 Re7 Rd8 Be2 Ng7 Bc7 Re8 Rd7 Be4 Be5 Ne6 Ke3 Nxg5 Rg7 Kf8 Rxg6 Nf7 Bg7 Ke7 Bh5 Kd7 Rf6 Bd5 Kf2 Ke7 Bxf7 Bxf7 Rxc6 Bc4) {-1.21/38 231} 40. Nc3 (Nc3 Kf7 Rh2 Rcd8 Rh3 Nc8 Be5 Rxd4 Bxd4 Ne7 Be5 Rd8 Ke1 Bb3 Bd6 Nd5 Nxd5 Bxd5 Kf2 Re8 Bxf8 Rxf8 Rh7 Ke6 Rg7 Rh8 Rxg6) {+1.54/14 0} Kf7 (Kf7 Rh2 Rcd8 Rh8 Bc8 Be2 Ne6 Rxe8 Rxe8 Rd3 Rh8 Bf3 Bd7 Ke1 Rh2 Ne2 Nd5 Kf1 Bc8 Kg1 Rh3 Kg2 Rh8 Be5 Re8 Bxd5 cxd5 Rh3 d4 Kf2 d3 Nc3 d2 Rh7 Kg8 Rh6 Kf7 c6 Rd8 Ke2) {-1.91/36 79} 41. Rh2 (Rh2 Rcd8 Rh3 Nc8 Be5 Rxd4 Bxd4 Ne7 Ne2 Rd8 Rh8 Rd7 Be5 Bc4 Nd4 Ne6 Rh7 Ke8 Rh8 Kf7 Rh7 Ke8 Bxc4 bxc4 Rh8 Kf7 Kc3 Nxd4 Bxd4 Nd5 Kxc4 Nxf4 Be5 Ne6 Rh7 Ke8 Rh6 Kf7 b5 Rd5 Rh7 Kg8) {+1.58/18 18} Rcd8 (Rcd8 Rh8 Bc8 Be2 Ne6 Rxe8 Rxe8 Rd3 Rh8 Bf3 Bb7 Re3 Rd8 Ke1 Rd7 Be2 Nc7 Rh3 Ncd5 Nxd5 Nxd5 Kf2 Ke6 Rh6 Kf7 Be5 Ba6 Bf3 Bb7 Rh8 Ne7 Ke3 Nd5 Bxd5 cxd5 Kd4 Ke6 Bd6) {-1.85/35 16} 42. Rh6 (Rh6 Nc8 Be5 Rxd4 Bxd4 Ne7 Be5 Bb3 Bd6 Nd5 Bxf8 Rxf8 Rh7 Kg8 Nxd5 Bxd5 Rd7 Bg2 Bb1 Bd5 Rd6 Kf7 Bd3 Bg2 Rd7 Kg8 Ke3 Re8 Kf2 Bh1 Be2 Re4 Rd1 Rxf4 Kg3) {+1.56/18 119} Nc8 (Nc8 Be5 Rxd4 Bxd4 Bb3 Be5 Ne7 Rh8 Rd8 Bc7 Re8 Bd6 Nd5 Rh6 Rd8 Nxd5 Bxd5 Ke3 Kg7 Kd4 Ne6 Kc3 Nf8 Be5 Kf7 Rh8 Re8 Rh2 Rd8 Rd2 Ne6 Bd6 Bf3 Rh2 Nf8 Bc7 Rd7 Be5 Bd5 Bd6 Rd8 Rd2 Ne6 Bf1 Be4 Rh2 Nf8 Rh8 Bd5 Bxf8 Be4 Rh6) {-0.74/36 24} 43. Be5 (Be5 Rxd4 Bxd4 Ne7 Be5 Bb3 Bd6 Nd5 Bxf8 Rxf8 Rh7 Kg8 Nxd5 Bxd5 Rd7 Bg2 Bb1 Bd5 Rd6 Kf7 Bd3 Bg2 Rd7 Kg8 Bb1 Bd5 Kc3 Re8 Rxd5 cxd5 Bd3 Kf7 Bxb5 Re3 Bd3 Rf3) {+1.52/17 19} Rxd4 (Rxd4 Bxd4 Bb3 Be5 Ne7 Rh8 Rd8 Bc7 Re8 Bd6 Nd5 Nxd5 Bxd5 Rh2 Be4 Be2 Rd8 Kc3 Bd5 Bd3 Re8 Rh6 Kg7 Be5 Kf7 Rh8 Rd8 Be2 Be4 Rh3 Re8 Rh2 Rd8 Rh8 Bd5 Rh3 Bg2 Rh6 Be4 Bd6 Bd5 Bd3 Re8 Bxf8 Rxf8 Rh7 Kg8) {-0.74/40 17} 44. Bxd4 (Bxd4 Ne7 Be5 Bb3 Bd6 Nd5 Bxf8 Rxf8 Rh7 Kg8 Nxd5 Bxd5 Rd7 Bg2 Bb1 Bd5 Rd6 Kf7 Bd3 Bg2 Rd7 Kg8 Bb1 Bd5 Kc3 Re8 Rxd5 cxd5 Bd3 Kf7 Bxb5 Re3 Bd3 Rf3) {+1.47/16 25} Ne7 (Ne7 Ne2 Bb3 Be5 Ne6 Rh7 Kg8 Rh4 Rd8 Rh1 Kf7 Rh7 Ke8 Bd6 Nc8 Bb8 Ne7 Be5 Nd5 Bd6 Rd7 Rh4 Kf7 Be5 Ne7 Rh7 Kg8 Rh8 Kf7 Bd6 Nd5 Ra8 Rd8 Ra7 Kg8 Be5 Rc8 Rd7 Kf8 Rh7 Rd8 Bd6 Kg8 Rh4 Bc4 Bxc4 bxc4 Rh6 Ndxf4 Nxf4 Nxf4 Kc3) {-0.74/39 17} 45. Be5 (Be5 Bb3 Ne2 Ne6 Rh7 Kg8 Rh6 Kf7 Nd4 Nxd4 Bxd4 Rd8 Be5 Rd7 Rh7 Ke6 Rh6 Kf7 Rh7 Ke8 Bd6 Bg8 Rh8 Kf7 Rh1 Nc8 Be5 Ne7 Rh6 Ke8 Bd6 Kf7 Be2) {+1.41/16 272} Bb3 (Bb3 Ne2 Ne6 Ng1 Bd5 Rh4 Kf8 Ne2 Kf7 Rh7 Kg8 Rh6 Kf7 Nc3 Bb3 Rh7 Kg8 Rh2 Kf7 Bf1 Rd8 Ke3 Nd5 Nxd5 Bxd5 Rh7 Ke8 Bd6 Rd7 Rh8 Kf7 Bd3 Kg7 Rh3 Rd8 Be5 Kf7 Rh7 Ke8 Bd6 Rd7 Rh6 Kf7 Be2 Be4 Rh7 Ke8 Rh3 Kf7 Bf3 Bxf3 Kxf3 Nf8 Rh6 Kg7 Bxf8 Kxf8 Rxg6 Rd3 Ke2 Rxa3) {-0.75/41 61} 46. Ne2 (Ne2 Ne6 Rh7 Kg8 Rh6 Kf7 Nd4 Nxd4 Bxd4 Rd8 Be5 Rd7 Rh7 Ke6 Rh6 Kf7 Rh7 Ke8 Bd6 Bg8 Rh8 Kf7 Rh1 Nc8 Be5 Ne7 Rh6 Ke8 Bd6 Kf7 Be2) {+1.40/15 24} Ne6 (Ne6 Ng1 Bd5 Rh4 Kf8 Ne2 Kf7 Rh7 Kg8 Rh6 Kf7 Nc3 Bb3 Rh7 Kg8 Rh2 Kf7 Be2 Rd8 Ke3 Nd5 Nxd5 Bxd5 Rh7 Ke8 Bd6 Rd7 Rh8 Kf7 Bd3 Kg7 Rh3 Nd8 Kd4 Ne6 Kc3 Nd8 Be5 Kf7 Rh8 Ne6 Bd6 Kg7 Rh6 Nd8 Be5 Kf7 Kd2 Ne6 Rh7 Ke8 Rh8 Kf7 Bd6 Kg7 Rh2 Kf7 Ke3 Kg7 Be5 Kf7 Rh7 Ke8 Rh4 Kf7 Bd6 Nf8 Bxf8 Kxf8 Rh6) {-0.74/41 4} 47. Rh7+ (Rh7 Kg8 Rh6 Kf7 Nd4 Nxd4 Bxd4 Rd8 Rh7 Ke6 Be5 Rd5 Kc3 Rd7 Rh6 Kf7 Rh8 Ke6 Bd6 Nd5 Kd2 Nxf4 Bxf4 Bc4 Bd6 Bxd3 Kxd3 Rf7 Bf4 Rd7 Ke3 Kd5) {+1.35/14 149} Kg8 (Kg8 Rh2 Kf7 Rh4 Bd5 Rh7 Kg8 Rh2 Kf7 Nc3 Bb3 Rh7 Kg8 Rh6 Kf7 Ke3 Nf8 Ne2 Ne6 Rh7 Kg8 Rh8 Kf7 Rh6 Nd5 Kd2 Ne7 Ng1 Bd5 Rh7 Kg8 Rh3 Kf7 Rh6 Rd8 Ne2 Bf3 Nc3 Nd5 Rh7 Kg8 Rh4 Kf7 Bd6 Kg7 Nxd5 Bxd5 Be5 Kg8 Rh6 Kf7 Bf6 Re8 Rh7 Kg8) {-0.74/41 31} 48. Rh6 (Rh6 Kf7 Rh2 Nd5 Rh7 Kf8 Ra7 Rc8 Ra6 Kf7 Ra7 Kg8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ba2 Bf3 Bc4 Bd6 Ke8 Bg2) {+1.33/15 72} Kf7 (Kf7 Nc3 Nf8 Be2 Rd8 Ke3 Nd5 Nxd5 Bxd5 Bd3 Re8 Kf2 Rd8 Bd6 Re8 Bf1 Be4 Rh3 Kg8 Be5 Rd8 Rh8 Kf7 Bd6 Re8 Rh6 Kg7 Be5 Kf7 Ke3 Rd8 Bd6 Bd5 Bd3 Re8 Kd2 Rd8 Kc3 Kg7 Rh2 Kf7 Rd2 Ne6 Bf1 Be4 Rh2 Nf8 Bxf8 Rxf8 Rh7 Kg8) {-0.74/44 51} 49. Ng1 (Ng1 Nd5 Rh7 Kf8 Ne2 Kg8 Rd7 Rd8 Ra7 Rc8 Ra6 Kf7 Bd6 Ke8 Ra7 Rd8 Rh7 Rd7 Rh8 Kf7 Rc8 Ne7 Ra8 Nd5 Ra6 Ne7 Nc1) {+1.35/15 55} Bd5 (Bd5 Rh7 Kg8 Rh4 Kf7 Ke3 Bb3 Ne2 Nd5 Kd2 Ne7 Rh6 Nd5 Bd6 Kg7 Rh1 Kg8 Rh4 Ne7 Nc3 Nf8 Rh3 Nd5 Nxd5 Bxd5 Rh6 Kf7 Rh4 Be4 Be2 Bd5 Be5 Be4 Ke3 Bd5 Rh3 Rd8 Rh6 Re8 Kf2 Rd8 Bd6 Re8 Bxf8 Rxf8 Rh7 Ke8 Bf3) {-0.74/45 39} 50. Rh7+ (Rh7 Kg8 Rh2 Kf7 Ne2 Bb3 Nd4 Nxd4 Bxd4 Rd8 Be5 Rd5 Kc3 Rd7 Rh8 Nd5 Kd2 Bc4 Bxc4 bxc4 Kc1 Ne3 Bd6 c3 b5 cxb5 c6) {+1.38/14 1} Kg8 (Kg8 Rh4 Kf7 Ke3 Bb3 Ne2 Nd5 Kd2 Ne7 Rh6 Nd5 Bd6 Kg7 Rh1 Kg8 Rh4 Ne7 Nc3 Nf8 Rh3 Nd5 Nxd5 Bxd5 Rh6 Kf7 Rh4 Be4 Be2 Bd5 Be5 Be4 Ke3 Bd5 Rh3 Rd8 Rh6 Re8 Kf2 Rd8 Bd6 Re8 Bd3 Bc4 Bxc4 bxc4 Be5 Nd7 Rh7 Ke6) {-0.74/45 28} 51. Rh2 (Rh2 Rd8 Ke3 Re8 Ne2 Bb3 Nc3 Nd5 Nxd5 Bxd5 Rh8 Kf7 Rh7 Kf8 Rd7 Rd8 Bd6 Kg8 Re7 Rf8 Bf1 Rd8 Be2 Rf8 Bf3 Bxf3 Rxe6 Bd5 Rxg6) {+1.49/14 38} Kf7 (Kf7 Ne2 Bb3 Rh7 Kg8 Rh8 Kf7 Rh2 Nd5 Rh7 Kf8 Rh4 Kf7 Bd6 Kg8 Rh2 Kf7 Rh7 Kg8 Rh4 Rd8 Nc3 Nxc3 Kxc3 Nf8 Rh6 Kg7 Rh3 Kf7 Kd2 Bd5 Rh8 Kg7 Be5 Kf7 Kc3 Re8 Rh6 Bb3 Be2 Bd5 Kd2 Rd8 Ke3 Be4 Rh3 Bd5 Bd6 Re8 Kf2 Be4 Rh6 Bd5 Bxf8 Rxf8 Rh7 Ke8) {-0.74/41 36} 52. Ne2 (Ne2 Bb3 Nd4 Nxd4 Bxd4 Rd8 Be5 Rd7 Bd6 Nc8 Bb8 Ne7 Rh7 Kg8 Rh6 Kf7 Rh8 Bc4 Bd6 Bxd3 Kxd3 Nd5 Rf8 Ke6 Rc8 Rh7 Rxc6 Rh3 Kd4 Rxa3 Be5 Ke7 Rxg6 Nxb4) {+1.34/14 65} Bb3 (Bb3) {-0.74/42} 53. Rh1 (Rh1 Nd5 Rh7 Kg8 Rd7 Rd8 Ra7 Rc8 Ra5 Kf8 Ra6 Kf7 Ra7 Kg8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ba2 Bf3 Bc4 Bd6 Ke8 Bg2) {+1.30/13 113} Nd5 (Nd5 Rh7 Kf8 Bd6 Kg8 Rh4 Ne7 Rh6 Kg7 Ng1 Bd5 Rh4 Kg8 Be5 Kf7 Rh7 Kg8 Rh2 Kf7 Ne2 Bb3 Rh7 Kg8 Rh8 Kf7 Rh2 Nd5 Rh1 Kf8 Rh7 Rd8 Rh4 Kf7 Bd6 Ba2 Rh7 Kg8 Ra7 Bb3 Be5 Rc8 Nd4 Nxd4 Bxd4 Nxf4) {-0.74/39 52} 54. Rh7+ (Rh7 Kg8 Rd7 Rd8 Ra7 Rc8 Ra5 Kf8 Ra6 Kf7 Ra7 Kg8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ba2 Bf3 Ke8 Bg2 Kd7 Nf3) {+1.26/15 0} Kf8 (Kf8 Bd6 Kg8 Rh4 Ba2 Rh2 Kf7 Rh6 Rd8 Nc3 Nxc3 Kxc3 Rd7 Kd2 Bb3 Rh7 Ke8 Rh4 Kf7 Kc3 Bd5 Kb2 Nd4 Rh7 Ke6 Rh6 Kf7 Bb1 Ne6 Rh7 Ke8 Rh8 Kf7 Rh6 Bb3 Rh7 Ke8 Rh8 Kf7 Kc3 Kg7 Be5 Kf7 Bd3 Bd5 Rh6 Ba2 Bf1 Bd5 Bd6 Be4 Be2 Kg7 Bd1 Bd5 Be5 Kf7 Rh7 Ke8 Rh4 Kf7 Be2 Nf8 Rh6 Rd8 Bd6 Re8 Bd3 Bf3 Be5 Rd8 Rh8 Bd5 Bd6 Bf3 Kd2 Bd5 Ke3 Re8 Be5 Bg2 Rh2 Bd5 Rh6 Bb3 Be2 Bd5 Kd2 Rd8 Bd6 Be4 Kc3 Kg7 Rh3 Kf7 Bd3 Bd5 Rh6 Bf3 Kd2 Re8 Bxf8 Rxf8 Rh7 Kg8) {-0.74/39 46} 55. Ra7 (Ra7 Rc8 Rd7 Rd8 Bd6 Kg8 Ra7 Re8 Ra6 Rc8 Be5 Kf7 Ra7 Kg8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ba2 Bf3 Bc4 Bd6 Ke8) {+1.28/13 65} Rc8 (Rc8 Rd7 Rd8 Rh7 Kg8 Rh2 Re8 Rh1 Kf7 Rh7 Kf8 Bd6 Kg8 Rh4 Ba2 Rh6 Kf7 Nc3 Nxc3 Kxc3 Bd5 Rh7 Kg8 Rb7 Be4 Bf1 Bf3 Rd7 Be4 Ra7 Bd5 Ra6 Rd8 Bd3 Rd7 Ra8 Kg7 Be5 Kf7 Rh8 Ke7 Rh7 Ke8 Rh4 Kf7 Bd6 Ke8 Bf1 Kf7 Rh7 Ke8 Rh8 Kf7 Bd3 Kg7 Be5 Kf7 Kd2 Rd8 Rh7 Kg8 Rh6 Kf7 Bd6 Kg7 Kc3 Nf8 Rh2 Kf7 Bxf8 Kxf8 Rh7) {-0.74/42 26} 56. Rh7 (Rh7 Re8 Bd6 Kg8 Ra7 Rd8 Be5 Rc8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ba2 Bf3 Ke8 Bg2 Kd7 Nf3) {+1.22/13 45} Rd8 (Rd8 Rh8 Kf7 Rh6 Re8 Rh7 Kf8 Bd6 Kg8 Rh4 Ba2 Rh6 Kf7 Nc3 Nxc3 Kxc3 Bd5 Rh7 Kg8 Rb7 Be4 Bf1 Bf3 Rd7 Nf8 Rc7 Ne6 Rb7 Bd5 Ra7 Nf8 Bd3 Ne6 Ra6 Kf7 Bc2 Be4 Ra7 Kg8 Rd7 Bd5 Be5 Nxg5 Rg7 Kf8 Rxg6 Ne4 Kb2 Re6) {-0.74/41 26} 57. Bd6+ (Bd6 Kg8 Ra7 Re8 Be5 Rc8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ke8 Bf3 Kd7 Nxb3 axb3 Kc1 Ne3 Be2 Ne6) {+1.24/13 8} Kg8 (Kg8 Rh4 Re8 Rh3 Rd8 Be5 Re8 Rh6 Kf7 Rh4 Ne7 Nc3 Nd5 Bd6 Kg7 Nxd5 Bxd5 Kc3 Nf8 Be5 Kg8 Rh8 Kf7 Rh6 Rd8 Bd6 Kg7 Bc7 Rd7 Be5 Kf7 Bd6 Ne6 Rh7 Ke8 Rh4 Kf7 Kd2 Kg8 Rh6 Kg7 Be5 Kf7 Rh7 Ke8 Rh8 Kf7 Kc3 Nf8 Bd6 Ne6 Kb2 Kg7 Rh3 Kf7 Rh7 Ng7 Kc3 Kg8 Rh6 Kf7 Kd4 Rd8 Rh7 Kg8 Rh3 Ne6 Ke3 Re8 Kf2 Nf8 Rh6 Kf7 Bxf8 Rxf8 Rh7 Kg8) {-0.74/43 23} 58. Ra7 (Ra7 Rc8 Rd7 Rd8 Ra7 Re8 Ra6 Rc8 Be5 Kf7 Ra7 Kg8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ba2 Bf3 Bc4 Bd6 Ke8 Bg2) {+1.23/13 24} Kh8 (Kh8 Ra6 Rc8 Be5 Kg8 Ra7 Rd8 Rb7 Ba2 Bb8 Bb3 Bd6 Kh8 Be5 Kg8 Ra7 Rc8 Rd7 Kf8 Rh7 Rd8 Rh8 Kf7 Rh6 Re8 Rh4 Ne7 Nc3 Nd5 Bd6 Kg7 Nxd5 Bxd5 Kc3 Nf8 Be5 Kg8 Rh6 Kf7 Rh3 Rd8 Rh2 Re8 Rd2 Rd8 Bc7 Rd7 Bd6 Ne6) {-0.74/41 51} 59. Ra6 (Ra6 Rc8 Ra5 Kg8 Ra6 Kf7 Be5 Ke7 Ra7 Kf8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Ke8 Be2 Kd7 Bf3 Bc4 Bg2) {+1.25/13 42} Rc8 (Rc8 Be5 Kg8 Ra5 Re8 Ra7 Rd8 Rb7 Ba2 Bb8 Bb3 Bd6 Ra8 Rd7 Rc8 Be5 Kf8 Rh7 Rd8 Rh8 Kf7 Rh6 Re8 Rh4 Ne7 Nc3 Nd5 Bd6 Kg7 Nxd5 Bxd5 Kc3 Nf8 Be5 Kg8 Rh6 Kf7 Rh3 Rd8 Rh2 Re8 Rd2 Rd8 Bc7 Rd7 Bd6 Ne6) {-0.74/45} 60. Ra5 (Ra5 Kg8 Ra6 Kf7 Be5 Ke7 Ra7 Kf8 Rd7 Rd8 Bd6 Ke8 Rh7 Rd7 Rh8 Kf7 Rc8 Ne7 Ra8 Nd5 Ra6 Ne7 Nc1) {+1.24/12 15} Re8 (Re8 Be5 Kg8 Ra6 Rc8 Bd6 Kh8 Ra7 Rd8 Rb7 Kg8 Be5 Rc8 Rd7 Kf8 Rh7 Rd8 Rh8 Kf7 Rh4 Rd7 Rh7 Ke8 Rh8 Kf7 Bd6 Rd8 Rh6 Rd7 Rh7 Ke8 Rh4 Kf7 Nc3 Nd4 Rh3 Ne6 Rh7 Ke8 Rh8 Kf7 Nxd5 Bxd5 Ke3 Kg7 Rh6 Kf7 Rh7 Ke8 Rh3 Kf7 Rh6 Kg7 Be5 Kf7) {-0.76/40 66} 61. Ra7 (Ra7 Kg8 Be5 Rc8 Rd7 Rd8 Ra7 Rc8 Rd7 Kf8 Rd6 Kf7 Ke1 Ne3 Nd4 Nxd4 Bxd4 Ng2 Kd2 Nxf4 Rf6) {+1.24/13 28} Rd8 (Rd8 Be5 Kg8 Ra6 Rc8 Bd6 Kh8 Bxb5 cxb5 Be5 Kg8 Rxe6 Nxf4 Re7 Nxe2 Kxe2 Kf8 Bd6 Re8 Re3 Kf7 c6 Bc4 Kd2 Rxe3 Kxe3 f4 Kd4 Ke8 Bxf4 Kd8 Kc5 Bd3 Kd6 Kc8 Kd5 Kd8 Kc5 Bc4 Bh2 Kc8 Kd4 Kd8 Ke5 Kc8 Kf6 Bd3 Bf4 Kd8 Bd6 Kc8 Ke6 Bc4 Ke5 Be2 Kf4 Bd3 Ke3 Bc4 Ke4 Kd8 c7 Kc8) {-0.52/39 20} 62. Ra6 (Ra6 Rc8 Be5 Kg8 Ra7 Kf8 Rd7 Rd8 Bd6 Kg8 Rxd8 Nxd8 Nd4 Kf7 Bf1 Ke8 Bg2 Kd7 Nf3 Nf7 Ne5 Nxe5 Bxe5) {+1.15/14 14} Rc8 (Rc8 Bxb5 cxb5 Be5 Kg8 Rxe6 Nxf4 Re7 Nxe2 Kxe2 Kf8 Bd6 Re8 Re3 Kf7 c6 Bc4 Kd2 Rxe3 Kxe3 f4 Kd4 Ke8 Bxf4 Kd8 Kc5 Bd3 Kd6 Kc8 Kd5 Kd8 Kc5 Bc4 Bh2 Kc8 Kd6 Bd3 Kd5 Bc4 Kc5 Bd3 Bd6 Bc4 Kb6 Be2 Be5 Bc4 Bd4 Bd3 c7 Bc4 Be5 Bd3 Kc5) {-0.52/42 21} 63. Ra5 (Ra5 Kg7 Ra7 Kg8 Be5 Kf8 Rd7 Rd8 Bd6 Kg8 Rxd8 Nxd8 Nd4 Kf7 Bf1 Ke8 Bg2 Kd7 Nf3 Nf7 Bb8 Kc8 Be5) {+1.13/13 5} Re8 (Re8 Be5 Kg8 Ra7 Rd8 Ra6 Rc8 Bxb5 cxb5 Rxe6 Nxf4 Re7 Nxe2 Kxe2 Kf8 Bd6 Re8 Re3 Kf7 c6 Bc4 Kd2 Rxe3 Kxe3 f4 Kd4 Ke8 Bxf4 Kd8 Kc5 Bd3 Kd6 Kc8 Kd5 Kd8 Kc5 Bc4 Bh2 Bd3 Kb6 Kc8 Bf4 Bc4 Be5 Bd3 Bd4 Bc4 Kc5 Kd8 Kd6 Bd3 Be5 Bc4 c7 Kc8 Kc6 Bd3 Bf4) {-0.52/43 25} 64. Be5+ (Be5 Kg8 Ra6 Rc8 Ra7 Kf8 Rd7 Rd8 Bd6 Kg8 Rxd8 Nxd8 Nd4 Kf7 Bf1 Ke8 Bg2 Kd7 Nf3 Nf7 Bb8 Kc8 Be5) {+1.16/13 10} Kg8 (Kg8 Ra7 Rd8 Ra6 Rc8 Bxb5 cxb5 Rxe6 Nxf4 Re7 Nxe2 Kxe2 Kf8 Bd6 Re8 Re3 Kf7 c6 Bc4 Kd2 Rxe3 Kxe3 f4 Kd4 Ke8 Bxf4 Kd8 Kc5 Bd3 Kd6 Kc8 Kd5 Kd8 Kc5 Bc4 Bh2 Bd3 Kb6 Kc8 Bf4 Bc4 Be5 Bd3 Bh2 Be2 Bg3 Bd3 Kc5 Bc4 Kd6 Kd8 Be5 Kc8 Ke7 Bd3 c7 Be2 Bg3) {-0.52/45 11} 65. Ra6 (Ra6 Rc8 Ra5 Kf7 Ra6 Ke7 Ra7 Kf8 Rd7 Rd8 Bd6 Ke8 Rh7 Rd7 Rh8 Kf7 Rc8 Ne7 Ra8 Nd5 Rh8 Ne7 Ng1 Bd5) {+1.14/13 21} Rc8 (Rc8 Bxb5 cxb5 Rxe6 Nxf4 Re7 Nxe2 Kxe2 Kf8 Bd6 Re8 Re3 Kf7 c6 Bc4 Kd2 f4 Bxf4 Rxe3 Kxe3 Ke7 Kd4 Kd8 Ke5 Kc8 Kf6 Bd3 Bd6 Be4 Kg7 Bd3 Kf7 Kd8 Kf6 Be4 Bf4 Bd3 c7 Kd7 Ke5 Bf5 Kd5 Be6 Kc5 Bc4 Be5 Bd3 Bg3 Bf1 Bd6 Kc8 Kd5 Be2 Bf4 Bd3 Kc6 Bc4) {-0.52/45 23} 66. Ra5 (Ra5 Kf7 Ra6 Ke7 Ra7 Kf8 Rd7 Rd8 Bd6 Kg8 Rxd8 Nxd8 Nd4 Kf7 Bf1 Ke8 Bg2 Kd7 Nf3 Nf7 Ne5 Nxe5 Bxe5 Bc4) {+1.13/15 0} Re8 (Re8 Ra7 Rd8 Rb7 Kf8 Rh7 Kg8 Rh8 Kf7 Rh6 Ne7 Rh7 Ke8 Bd6 Rd7 Nc3 Nf8 Rh8 Kf7 Ke3 Rd8 Rh6 Nd5 Nxd5 Bxd5 Kd4 Bb3 Kc3 Kg7 Bc7 Re8 Be5 Kf7 Rh8 Bd5 Rh3 Rd8 Rh6 Re8 Rh8 Ne6 Rh7 Kg8 Ra7 Nxg5 Rd7 Ne4 Kd4 Bf7 Rc7 Nf2 Rxc6 Nxd3) {-1.04/40 91} 67. Ra7 (Ra7 Rc8 Rd7 Kf8 Bd6 Kg8 Ra7 Rd8 Ra6 Rc8 Be5 Kf7 Ra7 Kg8 Rd7 Rd8 Rxd8 Nxd8 Nd4 Kf7 Be2 Ba2 Bf3 Bc4) {+1.14/12 10} Rd8 (Rd8 Rb7 Ba2 Bb8 Bb3 Bd6 Rc8 Ra7 Re8 Ra6 Rc8 Be5 Kf7 Ra7 Kg8 Rd7 Kf8 Rh7 Rd8 Rh8 Ke7 Rh2 Ke8 Rh6 Kf7 Rh4 Kf8 Bd6 Kg8 Rh6 Kf7 Be5 Rd7 Rh7 Ke8 Rh4 Kf8 Bd6 Kg7 Ng1 Bc4 Bxc4 bxc4 Nf3 Rd8 Kc2 Re8 Be5 Kf7 Rh7 Kf8 Rh8 Kf7 Rh4 Rd8 Rh7 Kg8 Rh6 Kf7) {-1.04/42 28} 68. Rb7 (Rb7 Kf8 Rh7 Re8 Bd6 Kg8 Rd7 Rc8 Ra7 Rd8 Ra6 Rc8 Be5 Ba2 Nc3 Nxc3 Kxc3 Bd5 Ra7 Re8 Rd7 Rd8 Re7) {+1.10/13 9} Kf8 (Kf8 Rh7 Kg8 Ra7 Rc8 Rd7 Kf8 Rh7 Rd8 Rh4 Kf7 Nc3 Re8 Bd6 Nd4 Rh7 Kg8 Rh3 Kf7 Ne2 Ne6 Rh7 Kg8 Rh4 Rd8 Nc3 Nxc3 Kxc3 Nf8 Rh6 Kg7 Be5 Kf7 Rh4 Re8 Rh1 Bd5 Rh3 Rd8 Rh8 Bb3 Be2 Bd5 Bd6 Kg7 Rh6 Nd7) {-0.84/37 5} 69. Rh7 (Rh7 Re8 Ra7 Rc8 Rd7 Rd8 Bd6 Kg8 Rxd8 Nxd8 Nd4 Kf7 Bf1 Ke8 Bg2 Kd7 Nf3 Nf7 Bb8 Kc8 Be5 Kd7 Bh1) {+1.08/12 0} Kg8 (Kg8 Ra7 Rc8 Rd7 Kf8 Rh7 Re8 Bd6 Kg8 Rh4 Rd8 Nc3 Nxc3 Kxc3 Nf8 Rh6 Kg7 Be5 Kf7 Be2 Bd5 Bd6 Kg7 Rh3 Re8 Be5 Kf7 Bf1 Rd8 Bd3 Re8 Rh8 Bb3 Rh2 Rd8 Bd6 Re8 Rh6 Kg7 Be5 Kf7 Rh8 Rd8 Be2 Bd5 Rh6 Re8 Bf1 Rd8 Bd6 Kg7 Rh2 Be4 Rh3 Kf7 Bd3 Bxd3 Kxd3 Re8) {-0.81/40 18} 70. Ra7 (Ra7 Rc8 Rd7 Kf8 Ra7 Kg8 Rd7 Kf8 Kc1 Ke8 Rh7 Rd8 Bd6 Rd7 Rh8 Kf7 Bb1 Bc4 Ng1 Ndxf4) {+1.13/11 7} Rc8 (Rc8 Rd7 Kf8 Rh7 Rd8 Rh8 Kf7 Rh2 Ke8 Bd6 Kf7 Rh4 Ba2 Rh3 Re8 Rh7 Kg8 Rh4 Bb3 Rh6 Kg7 Be5 Kf7 Rh7 Kf8 Bd6 Kg8 Rh4 Ne7 Be5 Kf7 Rh7 Kg8 Rh3 Kf7 Nc3 Nd5 Rh7 Kg8 Rh6 Nexf4 Nxd5 Nxd5 Rxg6 Kf7 Rg7 Kf8) {-0.81/39 18} 71. Rd7 (Rd7 Kf8 Bd6 Kg8 Ra7 Rd8 Be5 Rc8 Rd7 Kf8 Ke1 Rd8 Rxd8 Nxd8 Nd4 Ne3 Kd2 Nc4 Bxc4 Bxc4 Bf6 Ne6) {+1.06/12 4} Kf8 (Kf8 Rh7 Re8 Bd6 Kg8 Rh4 Ne7 Rh6 Kg7 Ng1 Bd5 Be5 Kf7 Rh7 Kg8 Rh2 Kf7 Ne2 Bb3 Nc3 Nd5 Rh7 Kg8 Rh4 Kf7 Bd6 Kg8 Nxd5 Bxd5 Rh6 Kg7 Kc3 Nf8 Be5 Kf7 Rh3 Bb3 Rh4 Ba2 Rh1 Bd5 Rh6 Bb3 Bd6 Kg7 Bxf8 Rxf8) {-0.81/37 13} 72. Bd6+ (Bd6 Kg8 Ra7 Rd8 Ra6 Rc8 Be5 Kf7 Ra7 Kg8 Rd7 Kf8 Kc1 Ke8 Rh7 Rd8 Bd6 Rd7 Rh8 Kf7 Kd2 Rd8 Rh7) {+1.03/13 0} Kg8 (Kg8 Be5 Kf8 Rh7 Re8 Bd6 Kg8 Rh4 Ne7 Rh3 Nd5 Be5 Kf7 Rh4 Ne7 Ng1 Bd5 Rh7 Kg8 Rh3 Kf7 Ne2 Bb3 Nc3 Nd5 Rh7 Kg8 Rh4 Kf7 Bd6 Kg8 Nxd5 Bxd5 Rh6 Kg7 Kc3 Nf8 Be5 Kf7 Rh3 Bb3 Bf1 Bd5 Rh8 Rd8 Bd3 Re8 Rh6 Bb3) {-0.81/42 26} 73. Ra7 (Ra7 Rd8 Ra6 Rc8 Be5 Kf7 Ra7 Kg8 Rd7 Kf8 Rd6 Ke7 Ke1 Ne3 Kd2 Nd5 Kc1 Kf7 Kd2 Ke7) {+0.99/14 0} Re8 (Re8 Rd7 Rd8 Rb7 Rc8 Be5 Rd8 Ra7 Rc8 Rd7 Kf8 Rh7 Re8 Bd6 Kg8 Rh4 Ne7 Be5 Nd5 Nc3 Kf7 Bd6 Nd4 Rh7 Kg8 Rh3 Kf7 Ne2 Ne6 Rh6 Rd8 Rh4 Kg8 Ng1 Bc4 Bxc4 bxc4 Ne2 Ndc7 Rh6 Kg7 Kc2 Nb5 Be5 Kf7 Rh7 Ke8 Nc3 Ned4 Bxd4 Nxd4 Kb2 Ne6 Rh8 Ke7 Rxd8 Kxd8 Nxa4 Nxf4 Nb6 Ne6) {-0.81/43 15} 74. Ra6 (Ra6 Rc8 Be5 Kf7 Ra7 Kg8 Rd7 Kf8 Kc1 Rd8 Rxd8 Nxd8 Nd4 Ke8 Nxb3 axb3 Be2 Nf7 Bb8 Kd7 Kb2 Kc8 Bf3) {+0.96/12 16} Rc8 (Rc8 Be5 Kf7 Ra7 Kf8 Rd7 Rd8 Rh7 Re8 Bd6 Kg8 Rh4 Ne7 Nc3 Nd5 Be5 Kf7 Ne2 Ne7 Ng1 Bd5 Rh2 Rd8 Ke3 Re8 Ne2 Bb3 Nc3 Nf8 Rh8 Ne6 Rh6 Nf8 Kd2 Nd5 Nxd5 Bxd5 Kc3 Bb3 Bd6 Bd5 Bxf8 Rxf8 Rh7 Kg8 Rd7 Bg2 Bb1 Re8) {-0.81/41 20} 75. Be5 (Be5 Kf7 Ra7 Kg8 Rd7 Kf8 Kc1 Rd8 Bd6 Ke8 Rh7 Rd7 Rh8 Kf7 Kd2 Rd8 Rxd8 Nxd8 Nd4 Ke8 Be5 Kd7 Nxf5 gxf5 Bxf5) {+0.93/13 0} Kf7 (Kf7 Ra7 Kg8 Rd7 Kf8 Rh7 Re8 Bd6 Kg8 Rh4 Ne7 Nc3 Nd5 Be5 Kf7 Ne2 Ne7 Rh2 Rd8 Rh7 Ke8 Rh6 Kf7 Nc3 Nf8 Ke3 Re8 Rh2 Rd8 Be2 Nd5 Nxd5 Bxd5 Rh8 Re8 Kd2 Rd8 Kc3 Re8 Bd3 Bb3 Bd6 Kg7 Rxf8) {-0.80/39 12} 76. Ra7+ (Ra7 Kg8 Rd7 Kf8 Kc1 Rd8 Bd6 Ke8 Rh7 Rd7 Rh8 Kf7 Kd2 Rd8 Rxd8 Nxd8 Nd4 Ke8 Bf1 Kd7 Bg2 Ne6 Nxe6 Kxe6 Bf3 Bc4 Be5) {+0.90/13 7} Kg8 (Kg8 Rd7 Kf8 Rh7 Re8 Bd6 Kg8 Rh4 Ne7 Rh1 Nd5 Be5 Kf7 Rh4 Ne7 Rh7 Kg8 Rh3 Kf7 Ng1 Bd5 Rh6 Rd8 Ne2 Bb3 Rh1 Re8 Nc3 Nf8 Rh8 Nd5 Nxd5 Bxd5 Kc3 Bb3 Bf1 Bd5 Rh3 Rd8 Rh6 Re8 Bd3 Bb3 Rh8 Bd5 Bd6 Kg7) {-0.80/41 11} 77. Rd7 (Rd7 Kf8 Kc1 Rd8 Bd6 Ke8 Rh7 Rd7 Rh8 Kf7 Rc8 Ne7 Ra8 Nd5 Kd2 Ne7 Ra6 Bd5 Nc3 Bg2 Ke3 Nc8) {+0.90/13 0} Kf8 (Kf8 Rh7 Re8 Bd6 Kg8 Rh4 Ne7 Rh6 Kg7 Ng1 Bd5 Be5 Kf7 Rh7 Kg8 Rh2 Kf7 Ne2 Bb3 Nc3 Nf8 Rh8 Rd8 Bc7 Re8 Bd6 Kg7 Rh2 Nd5 Nxd5 Bxd5 Be5 Kf7 Kc3 Bb3 Rh8 Bd5 Rh6 Bb3 Bd6 Kg7 Rh3) {-0.80/38 14} 78. Rd6 (Rd6 Ke7 Ke1 Ne3 Bf6 Kf7 Kd2 Nd5 Be5 Ke7 Ke1 Ne3 Nd4 Nxd4 Bxd4 Ng2 Kd2 Nxf4 Bf1 Bd5 Bf6 Kf7 Rd7 Kf8) {+0.87/13 7} Ke7 (Ke7 Nd4 Nxd4 Bxd4 Nxf4 Bf1 Kf7 Ke3 Nd5 Kf2 Bc4 Bg2 Re8 Bxd5 Bxd5 Rf6 Ke7 Rxg6 Kf8 Rf6 Kg8 Be3 Re4 Rxf5 Rc4 Ke2 Be4 Rf4 Bd5 Rxc4 Bxc4 Kf3 Kf7 Bc1 Ke6 Kf4 Kf7 Kf5 Bd3 Kg4 Kg6) {-0.31/29 13} 79. Ke1 (Ke1 Ne3 Kd2 Nd5 Ng1 Ndxf4 Nf3 Bd5 Bf6 Ke8 Ne5 Nxd3 Kxd3 Rc7 Nxg6 Rh7 Ne5 Rh3 Kd2 Rh2 Ke1 Rh1 Kf2 Rh2 Kg3 Rg2 Kh3 Nf4) {+0.81/14 0} Rd8 (Rd8 Rxc6 Nxb4 axb4 Rxd3 Ra6 Bc4 Bf6 Kf7 Ra7 Ke8 Nc3 Nd4 Ra8 Kf7 Bxd4 Rxd4 c6 Be6 Ra7 Kf8 c7 Rxb4 Ke2 Rxf4 Ra8 Kf7 Nxb5 Ke7 c8=Q Bxc8 Rxc8 Rg4 Ra8 Rxg5 Nd4 Rg4 Rxa4 Re4 Kd3 Kf6 Ra6 Ke5 Ne2 g5 Ra3 Kf6 Ra8 Re5 Rf8 Ke7) {-0.24/34 22} 80. Rxc6 (Rxc6 Nxb4 axb4 Rxd3 Ra6 Bc4 Nc1 Rd5 c6 Rxe5 fxe5 Kd8 Ne2 Kc7 Kf2 f4 Nc3 Nxg5 Kg2 Ne6 Kf3 g5 Kg4 Nd4 Ra7 Kxc6) {+0.89/14 1} Nxb4 (Nxb4 axb4 Rxd3 Ra6 Bc4 Bf6 Ke8 Nc3 Nd4 Ra8 Kf7 Bxd4 Rxd4 c6 Be6 Ra7 Kf8 c7 Rxb4 Ke2 Rxf4 Ra8 Kf7 Nxb5 Ke7 c8=N Bxc8 Rxc8 Rg4 Ra8 Rxg5 Nd4 Rg2 Kf3 Rg4 Ke3 Re4 Kd3 Kf6 Ra6 Ke5 Rxa4 Kf4 Ne6 Ke5 Ra6 Rg4) {-0.22/36 10} 81. axb4 (axb4 Rxd3 Ra6 Bc4 Nc1 Rd5 c6 Rxe5 fxe5 Kd8 Ra7 Nxg5 Nd3 Ne6 Nb2 Bb3 Nd3 Bc4 Kd2 g5 Nc5 Nxc5 bxc5 f4 Rd7 Kc8 Rd6) {+0.89/16 0} Rxd3 (Rxd3 Ra6 Bc4 Bf6 Ke8 Nc3 Nd4 Ra8 Kf7 Bxd4 Rxd4 c6 Be6 Ra7 Kf8 c7 Rxb4 Ke2 Rxf4 Ra8 Kf7 Nxb5 Ke7 c8=N Bxc8 Rxc8 Rg4 Ra8 Rxg5 Nd4 Rg3 Kf2 Rd3 Rxa4 f4 Ke2 Re3 Kd2 Kf6 Ra6 Ke5 Nc6 Ke6 Ra5 g5 Nd4 Kf6 Nf5 Re8) {-0.13/36 14} 82. Ra6 (Ra6 Bc4 Nc1 Rd5 Ne2 Rd3 Nc1 Rd5 c6 Rxe5 fxe5 Kd8 Ra7 Nxg5 Nd3 Ne6 Nb2 Bb3 Nd3 Bc4 Kd2 g5 Nc5 Nxc5 bxc5 f4 Rd7 Kc8 Rd6) {+0.76/16 0} Bc4 (Bc4 Nc1 Rd5 c6 Rxe5 fxe5 Kd8 Ne2 Kc7 Kf2 f4 Nc3 Nxg5 Ra7 Kxc6 Ra6 Kc7 Rxg6 Nf7 Rg7 Kc6 Rg6) {0.00/37 37} 83. Nc1 (Nc1 Rd5 Ne2 Rd3 Nc1 Rd5 c6 Rxe5 fxe5 Kd8 Ra7 Nxg5 Nd3 Ne6 Kd2 g5 Nb2 Bb3 Nd1 Kc8 Ne3 Nd4 Kc3 Nxc6 Rg7 f4 Nf5 f3 Nd6 Kd8 Rxg5) {+0.60/18 0} Rd5 (Rd5 c6 Rxe5 fxe5 Kd8 Ra7 Nxg5 Nd3 Ne6 Kf2 f4 Nb2 Bb3 Nd1 Kc8 Nc3 Nd4 Rg7 g5 Ne4 Nxc6 Nd6 Kd8 Nxb5 Nxe5 Rxg5 Nc6 Kf3 Nxb4 Kxf4 a3 Nd4 Bd5 Rg3 Kc7 Rxa3) {0.00/35 8} 84. Ne2 (Ne2 Rd3 Nc1 Rd5 c6 Rxe5 fxe5 Kd8 Ra7 Nxg5 Nd3 Ne6 Nc5 Nxc5 bxc5 g5 c7 Kc8 Ra6 Kxc7 e6 Kd8 c6 a3 Ra8 Ke7 c7 Bxe6 Rxa3 Kd7 Ra6 Bc4 Rc6 Kc8 Rc5 Be6 Rxb5 Kxc7) {+0.56/19 1} Rd3 (Rd3 Ng1 a3 Bf6 Ke8 c6 Nxf4 Ra8 Kf7 Ra7 Kf8 Ra8) {0.00/37 22} 85. Bf6+ (Bf6 Ke8 Nc3 Nxf4 c6 Rd8 Bxd8 Kxd8 Kd2 Kc7 Nb1 Ne6 Na3 Bf1 Ke3 Nxg5 Kf4 Ne6 Ke5 Nd8 Kd5 f4 Kc5 Ne6 Kd5 Nd8 Kc5 Ne6 Kd5 Nd8) {+0.55/20 3} Ke8 (Ke8 Nc3 Nxf4 c6 Rd8 Bxd8 Kxd8 Kd2 Kc7 Nb1 Bd5 Ra7 Kxc6 Ra6 Kb7 Rd6 Kc7 Rf6 Be6 Kc2 Bd5) {0.00/36 20} 86. Nc3 (Nc3 Nxf4 c6 Rd8 Bxd8 Kxd8 Kd2 Kc7 Nb1 Nd5 Ra7 Kxc6 Rg7 Nf4 Na3 Kd5 Rd7 Kc6 Rd8 Bf1 Rc8 Kd5 Rc5 Ke4 Nxb5 Bxb5 Rxb5 Nd5 Rb8 a3) {+0.53/20 0} Nxf4 (Nxf4 c6 Rd8 Bxd8 Kxd8 Kd2 Kc7 Nb1 Bd5 Ra7 Kxc6 Ra6 Kc7 Rf6 Be6 Kc2 Bd5) {0.00/37 9} 87. c6 (c6 Rd8 Bxd8 Kxd8 Kd2 Kc7 Nb1 Nd5 Ra7 Kxc6 Rg7 Nf4 Na3 Kd5 Rd7 Kc6 Rd8 Bf1 Rc8 Kd5 Rc5 Ke4 Nxb5 Bxb5 Rxb5 Nd5 Rb8 a3) {+0.50/21 0} Rd8 (Rd8 Bxd8 Kxd8 Ra7 Nd3 Kd2 Nxb4 c7 Kd7 Nxa4 bxa4 Rxa4 Na6 c8=Q Kxc8 Rxc4 Kd7 Rh4 Nc5 Ke3 Ne6 Rh7 Kd6 Rh6 Nxg5) {0.00/39 9} 88. Bxd8 (Bxd8 Kxd8 Kd2 Kc7 Nb1 Nd5 Ra7 Kxc6 Rg7 Nf4 Na3 Kd5 Rd7 Kc6 Rd8 Bf1 Rc8 Kd5 Rc5 Ke4 Nxb5 Bxb5 Rxb5 Nd5 Rb8 a3) {+0.48/18 0} Kxd8 (Kxd8 Ra7 Nd3 Kd2 Nxb4 c7 Kd7 Nxa4 bxa4 Rxa4 Na6 c8=Q Kxc8 Rxc4 Kd7 Rh4 Nc5 Ke3 Ne6 Rh7 Kd6 Rh6 Nxg5) {0.00/42 11} 89. Kd2 (Kd2 Kc7 Nb1 Nd5 Ra7 Kxc6 Rg7 Nxb4 Rxg6 Kc5 Na3 Bd5 Rf6 Nc6 Nc2 b4 Rxf5 b3 Na3 Nd4 Rf4 b2 g6 Nb5 Kc2 Nxa3 Kxb2 Nc4 Ka1 Ne3 g7 Nc2) {+0.46/17 0} Kc7 (Kc7 Nb1 Nd5 Na3 Nxb4 Ra7 Kxc6 Nxc4 bxc4 Kc3 Nd5 Kxc4 Nc7 Rxa4 Kd7 Kb4 Ne6 Ra7 Kd6 Ra6 Ke5 Kc4 Nxg5) {0.00/41 9} 90. Nb1 (Nb1 Nd5 Ra7 Kxc6 Rg7 Nxb4 Rxg6 Kc5 Na3 Bd5 Rf6 Nc6 Nc2 b4 Rxf5 b3 Ne3 Ne7 Re5 a3 Kc1 Kd6 Rxe7 Kxe7 Nxd5 Kf7 Nf4 Kg7) {+0.47/18 0} Nd5 (Nd5 Na3 Nxb4 Ra7 Kxc6 Nxc4 bxc4 Kc3 Nd5 Kxc4 Nc7 Rxa4 Kd7 Kb4 Ne6 Ra7 Kd6 Ra6 Ke5 Kc4 Nxg5) {0.00/41 8} 91. Ra7+ (Ra7 Kxc6 Rg7 Nxb4 Rxg6 Kc5 Na3 Bd5 Rf6 Nc6 Nc2 b4 Ne3 Be4 g6 a3 Nc2 a2 Na1 Ne7 g7 Bd5 Kc2 Kd4 Kb2 Ke5 Rf8 f4 Nc2 b3 Nb4 Bg8 Nd3) {+0.48/18 8} Kxc6 (Kxc6 Ra6 Nb6 Na3 Bf1 Ra7 Nc4 Nxc4 Bxc4 Ra6 Kd5 Rxg6 f4 Ra6 Ke4 g6 f3 g7 f2 Rf6 a3 Rxf2 a2 Re2 Bxe2 Kxe2) {0.00/40 8} 92. Rg7 (Rg7 Nxb4 Rxg6 Kc5 Na3 Bd5 Rf6 Nc6 Nc2 b4 Ne3 Be4 g6 a3 Nc2 a2 Na1 Ne7 Re6 Ng8 g7 Bd5 Re5 f4 Rf5 f3 Kc2 Kd4 Kb2 Ke4 Rf8 b3) {+0.46/18 0} Nxb4 (Nxb4 Rxg6 Kc5 Rf6 Nd5 Rxf5 b4 Kc2 Kd4 Rf3 Ne3 Kd2 a3 Nxa3 bxa3 Rxe3) {0.00/40 12} 93. Na3 (Na3 Bd5 Rxg6 Kc5 Rf6 Nc6 Nc2 b4 Ne3 Be4 g6 a3 Nc2 a2 Na1 Ne7 Re6 Ng8 Re8 Bd5 Rf8 Ne7 g7 b3 Kc3 Kd6 Nxb3 Bxb3 Kxb3 a1=Q g8=Q) {+0.41/17 3} Bd5 (Bd5 Rxg6 Kc5 Rf6 Nc6 Nxb5 Kxb5 Rxf5 Kc5 Rxd5) {0.00/42 10} 94. Rxg6+ (Rxg6 Kc5 Rf6 Nc6 Nc2 b4 Ne3 Be4 g6 a3 Nc2 a2 Na1 Ne7 g7 Bd5 Kd3 Bg8 Kc2 Kd5 Kb2 Ke5 Rf8 f4 Nc2 b3 Ne1 Ke4 Ng2) {+0.28/18 0} Kc5 (Kc5 Rf6 Nc6 Nc2 b4 g6 b3 Rxf5 bxc2 Rxd5 Kxd5) {0.00/39 9} 95. Rf6 (Rf6 Nc6 Nc2 b4 Ne3 Be4 g6 a3 Nc2 a2 Na1 Ne7 g7 Bd5 Kd3 Bc4 Kc2 Kd4 Kb2 Ke5 Rf8 f4 Nb3 Bd5 Nc1 f3 Nxa2 Nf5) {+0.27/16 0} Nc6 (Nc6 Nc2 b4 Rxf5 b3 g6 bxc2 Rxd5 Kxd5) {0.00/41 7} 96. Nc2 (Nc2 b4 Ne3 Be4 g6 a3 Nc2 a2 Na1 Ne7 g7 Bd5 Kd3 Bg8 Kc2 Kd5 Kb2 Ke5 Rf8 f4 Nc2 b3 Ne1 Ke4 Re8 Kf5) {+0.27/16 0} b4 (b4 g6 b3 Rxf5 bxc2 Rxd5 Kxd5) {0.00/41 16} 97. Ne3 (Ne3 Be4 g6 a3 Nc2 a2 Na1 Ne7 g7 Bd5 Kd3 Bg8 Kc2 Kd5 Kb2 Ke5 Rf8 b3 Nxb3 Bxb3 Re8 Kf6 Rxe7 Kxe7 Kxb3) {+0.22/17 0} Be4 (Be4 g6 Ne7 Re6 Nxg6 Rxg6 a3 Rg1 b3 Kc3 b2 Kb3 b1=R Rxb1 Bxb1) {0.00/37 8} 98. g6 (g6 a3 Nc2 a2 Na1 Ne7 Kc1 Bd5 Kb2 Kd4 Nc2 Ke5 Rb6 f4 Nxb4 Bg8 Nd3 Kf5 g7 f3 Ra6 Kg5 Nf2 Nf5) {+0.19/16 0} a3 (a3 Nc2 Ne7 Re6 Nxg6 Rxg6 b3 Nxa3 Kb4 Ra6 b2 Ra7 Kb3 Ke2 b1=Q Nxb1) {0.00/38 6} 99. Nc2 (Nc2 a2 Na1 Ne7 g7 Bd5 Kd3 Bg8 Kc2 Kd4 Kb2 Ke5 Rf8 b3 Ka3 f4 Kb2 Bd5 Nxb3 f3 Nd2 f2) {+0.18/15 0} Ne7 (Ne7 Re6 Nxg6 Rxg6 b3 Nxa3 Kb4 Ra6 b2 Ra7 Kb3 Ke2 b1=Q Nxb1) {0.00/40 5} 100. g7 (g7 a2 Na1 Bd5 Kd3 Bc4 Kc2 Kd4 Kb2 Ke5 Rf8 b3 Kc3 Be6 Nxb3 Bxb3 Kxb3) {+0.22/10 4} a2 (a2 Na1 Bd5 Kc2 Kd4 Ra6 Ke5 Nb3 f4 Rxa2 Bxb3 Kxb3 Kf6 Rg2 Ng8 Kb2 f3 Rg3 f2 Rf3 Kxg7) {0.00/39 8} 101. Na1 (Na1 Bd5 Kd3 Bg8 Kc2 Kd4 Kb2 Ke5 Rf8 b3 Rb8 Kf6 Nxb3 Kxg7 Kxa2 f4 Kb2 f3 Nd2 Bd5) {+0.07/12 0} Bd5 (Bd5 Kc2 Kd4 Ra6 f4 Nb3 Ke5 Rxa2 Nf5 Ra5 Nxg7 Kd3 Nf5 Nd2 Kd6 Rb5 f3 Rxb4 f2 Rf4 f1=Q Nxf1) {0.00/41 4} 102. Kc2 (Kc2 Kd4 Kb2 Ke5 Rb6 f4 Nc2 f3 Ne3 Be6 Rxb4 f2 Ra4 Kf6 Rf4 Kxg7 Rxf2 Kg6 Ng2 Kg5) {+0.04/11 1} b3+ (b3 Kb2 Kd4 Ra6 Ke5 Nxb3 Bxb3 Kxb3 a1=Q Rxa1) {0.00/42 5} 103. Kc3 (Kc3 Bc4 Kb2 Kd4 Rb6 Ke5 Nxb3 f4 Nd2 Bd5 Ra6 f3 Nxf3 Bxf3 Ra7 Ng8) {+0.31/8 1} Bc4 (Bc4 Rf8 Bd5 Kb2 Kd4 Re8 Ng8 Rb8 Ke5 Nxb3 Bxb3 Rxb3 a1=Q Kxa1) {0.00/43 6} 104. Kb2 (Kb2 Kd4 Rb6 Ke5 Nxb3 f4 Nd2 Bd5 Rb5 Ke6 Rb4 Kf7 Rxf4 Kxg7 Ne4 a1=Q Kxa1) {+0.31/10 0} Kd4 (Kd4 Rf8 Ke5 g8=Q Nxg8 Re8 Kf6 Re1 f4 Nxb3 f3 Nd2 f2 Ne4 Kg6 Nxf2 a1=Q Rxa1) {0.00/46 5} 105. Rb6 (Rb6 Ke5 Nxb3 Bxb3 Rxb3 Kf6 Rb7 Ng8 Kxa2 Kg6 Kb3 f4 Kc4 f3 Kd3 f2 Ke2 Nh6) {+0.27/11 0} Ke5 (Ke5 Nxb3 Bxb3 Rxb3 a1=Q Kxa1) {0.00/48 5} 106. Nxb3 (Nxb3 Bxb3 Rxb3 Kf6 Rb7 Ng8 Kxa2 Kg6 Kb3 f4 Kc4 f3 Kd3 Nh6 Ke3 Nf5 Kxf3) {+0.23/11 0} Bxb3 (Bxb3 Rxb3 a1=Q Kxa1) {0.00/53 7} 107. Rxb3 (Rxb3 Kf6 Rb7 Ng8 Kxa2 Kg6 Kb3 f4 Kc4 f3 Kd3 Nh6 Ke3 Nf5 Kxf3 Nxg7) {+0.17/10 0} a1=Q+ (a1=Q Kxa1) {0.00/105 4} 108. Kxa1 (Kxa1 Kf6 Rb7 Ng8 Kb2 Kg6 Kc3 f4 Kd4 f3 Ke3 Nh6 Kxf3 Kh7 Kf4) {+0.25/8 1} Kf6 (Kf6 Rb7 Ng8 Kb2 Ke6 Kb3 Kf6 Kb4 Nh6 Kc3 Ng8 Ra7 Kg6 Kd4 Kf6 Kd3 Kg6 Ke2 Nh6 Rb7 Kf6 Kf3 Kg6 Kf2 Kf6 Rd7 Kg6 Kg2 Ng8 Kf3 Nh6 Rb7 Ng8 Kf4 Kf6 Ra7 Nh6 Rd7 Ng8 Kf3 Kg6 Kg2 Nf6 Rb7 Ng8 Kg1 Nf6 Kf2 Ne4 Kf3 Nf6 Ra7 Ng8 Kf4 Nh6 Ke5 Nf7 Ke6 Nh6 Rb7 f4 Kd5 f3) {0.00/55 16} 109. Rb7 (Rb7 Ng8 Kb2 Kg6 Kc3 Nh6 Kd4 Kf6 Kd5 Ng8 Ra7 Nh6 Rd7 Ng8 Kd6 Nh6) {+0.26/11 0} Ng8 (Ng8 Kb2 Ke6 Kb3 Kf6 Kb4 Nh6 Kc3 Ng8 Ra7 Nh6 Kd3 Kg6 Kd4 Kf6 Rc7 Kg6 Ke5 Nf7 Kd5 Nh6 Rb7 Kf6 Rd7 Kg6 Ke5 Ng4 Kd4 Nh6 Ke3 Ng8 Rb7 Nh6 Kf4 Kf6 Rd7 Kg6 Rc7 Kf6 Kg3 Ng8 Ra7 Kg6 Kh4 Nf6 Kh3 Kh7 Kg3 Kg6 Kf3 Ng8 Rb7 Nf6 Ke3 Ng8 Rd7 Nh6 Ke2 Ng8 Kd1 Nf6 Kc2 f4 Ra7 Kh7) {0.00/58 8} 110. Kb2 (Kb2 Kg6 Kc3 Nh6 Kd4 Kf6 Kd5 Ng8 Rc7 Nh6 Rb7 Kg6 Ke6 Ng8 Rf7 Nh6) {+0.31/10 0} Nh6 (Nh6 Kc3 Ng8 Ra7 Kg6 Kd4 Kf6 Kd3 Kg6 Ke3 Nh6 Kd4 Kf6 Rc7 Kg6 Ke5 Nf7 Kd5 Nh6 Rb7 Kf6 Rd7 Ng8 Kc4 Nh6 Kd3 Ng8 Rc7 Kg6 Ke3 Nf6 Kd4 Ng8 Ke5 Nh6 Rd7 Nf7 Kf4 Nh6 Kf3 Ng8 Rc7 Nh6 Rb7 Ng8 Kg3 Nf6 Kf2 Ng8 Rc7 Nf6 Ra7 Ng8 Kg2 Nh6 Kg3 Ng8 Kh3 Nf6 Kg2 Kh7 Kg3 Nh5 Kh4 Nxg7 Kg5 Kg8) {0.00/56 3} 111. Kc3 (Kc3 Kg6 Kd4 Kf6 Rc7 Kg6 Ke5 Ng8 Ra7 Nh6 Ke6 Ng8 Ra1 Kxg7) {+0.34/9 0} Ng8 (Ng8 Ra7 Kg6 Kd4 Nh6 Kd3 Ng8 Rd7 Kh7 Kd4 Nh6 Kd5 Kg6 Rc7 Kf6 Kd4 Ng8 Kc4 Nh6 Kd5 Kg6 Rd7 Kf6 Kd4 Kg6 Ra7 Ng8 Ke5 Nh6 Kf4 Kf6 Rc7 Ng8 Rd7 Nh6 Ke3 Kg6 Kf3 Kf6 Rb7 Kg6 Ra7 Kf6 Kf2 Ng8 Kg2 Kg6 Kh3 Nh6 Rc7 Ng8 Rb7 Kf6 Rd7 Kg6 Rc7 Kh7 Rb7 Kg6 Kh2 Nh6 Rd7 Ng4 Kh3 Nf6 Kh4 f4 Ra7 f3) {0.00/50 3} 112. Kd4 (Kd4 Nh6 Ke3 Kg6 Kf4 Ng8 Ke5 Nh6 Rd7 Ng8 Ke6 Nh6 Rc7 Ng8) {+0.40/8 3} Kg6 (Kg6 Ke5 Nh6 Rd7 Nf7 Ke6 Nh6 Kd5 Kf6 Ra7 Kg6 Rc7 Kf6 Rd7 Kg6 Kd4 Kf6 Ke3 Kg6 Kf3 Ng8 Kf2 Nf6 Rb7 Ng8 Kg2 Nh6 Kg3 Ng8 Kh3 Nf6 Kg2 Kh7 Kg3 Ng8 Kf4 Kg6 Ra7 Nh6 Kf3 Ng8 Ke3 Nf6 Kd4 Ng8 Rb7 Nf6 Rc7 Ng8 Kc3 Nf6 Ra7 Ng8 Rd7 Nh6 Rb7 f4 Kd3 f3) {0.00/53 53} 113. Ke5 (Ke5 Nh6 Ra7 Ng8 Ke6 Nh6 Ra1 Kxg7 Rg1 Kh7 Kf6 Ng8 Kf7 Nh6 Ke6) {+0.42/9 1} Nh6 (Nh6 Rd7 Nf7 Ke6 Nh6 Kd5 Kf6 Ra7 Kg6 Rc7 Kf6 Rd7 Kg6 Kd4 Kf6 Kd3 Kg6 Ke2 Kh7 Kf3 Ng8 Kg3 Nf6 Rf7 Ng8 Rb7 Kg6 Kf4 Nf6 Ke5 Ng4 Kd4 Nf6 Kc4 Ng8 Kc3 Kf6 Ra7 Nh6 Kd3 Kg6 Rd7 Ng8 Kd4 Nh6 Rc7 Ng8 Ke5 Nh6 Kf4 Ng8 Ra7 Nf6 Kf3 Ng8 Kg2 Nh6 Kf2 Ng8 Rd7 Nf6 Rb7 Ng8 Kg3 Nf6 Kh4 f4 Ra7 f3) {0.00/55 2} 114. Rc7 (Rc7 Ng8 Ra7 Nh6 Ke6 f4 Ra1 Kxg7 Rg1 Kf8 Kf6 Ng8 Ke6 Nh6 Rg6) {+0.39/9 2} Ng4+ (Ng4 Kd4 Nh6 Rd7 Ng8 Ke5 Nh6 Kf4 Ng8 Rb7 Nf6 Ke5 Ng4 Kd4 Nf6 Re7 Ng8 Rd7 Nf6 Rb7 Ng8 Ke5 Nh6 Rd7 Ng4 Kd5 Nh6 Ra7 Kf6 Kd4 Kg6 Kd3 Ng8 Kc3 Nh6 Kd2 Ng8 Ke1 Nf6 Kf2 Ng4 Kg3 Nf6 Kh3 Ng8 Kg2 Nh6 Kg3 Kf6 Rd7 Kg6 Rc7 Kh7 Ra7 Ng8 Rd7 Nh6 Kh3 f4 Kg2 Nf5) {0.00/51 4} 115. Ke6 (Ke6 Nh6 Rf7 Ng8 Rd7 Nh6 Ra7 f4 Ke5 f3 Ke6 f2 Ra1 Kxg7 Rf1) {+0.38/8 0} Nh6 (Nh6 Kd5 Kf6 Rb7 Kg6 Kd4 Ng8 Ke5 Nh6 Rd7 Nf7 Kd5 Nh6 Kd4 Ng8 Rb7 Nf6 Rc7 Ng8 Kc3 Kf6 Kd2 Kg6 Rb7 Kh7 Ke3 Kg6 Ra7 Nf6 Kd3 Ng8 Kc2 Kf6 Kb3 Kg6 Kb2 Nh6 Kb1 f4 Kc2 f3 Kd1 Kh7) {0.00/38 31} 116. Rf7 (Rf7 Ng8 Rc7 Nh6 Ra7 f4 Ke5 f3 Ke4 f2 Ra1 Kxg7 Ke3 f1=B Ra7 Kf6) {+0.32/10 0} Ng8 (Ng8 Rxf5 Kxg7 Rf7 Kg6 Rf1 Kg7 Rg1 Kf8 Rg6 Ne7 Rf6 Ke8 Rf3 Ng8 Rf1 Nh6 Kf6 Ng8 Ke5 Nh6 Rb1 Kf7 Rb7 Kg6 Ke6 Ng8 Rf7 Nh6 Ra7 Ng8 Ra8 Kg7 Re8 Nh6 Re7 Kg6 Kd5 Kg5 Ra7 Kf4 Ra1 Kg5 Kd4 Ng4 Ra7 Kf5 Rf7 Kg5 Kc3 Nh6 Kb2 Nxf7 Kb1 Kh4) {0.00/40 2} 117. Rc7 (Rc7 Nh6 Rb7 f4 Rf7 Ng8 Rxf4 Kxg7 Rg4 Kf8 Rg6 Ne7 Rf6 Ke8 Rh6 Ng8) {+0.28/9 1} f4 (f4 Rf7 f3 Rxf3 Kxg7 Kf5 Nh6 Ke5 Kg6 Rg3 Kf7 Kd5 Ng8 Rf3 Ke7 Re3 Kf8 Ke5 Kg7 Rg3 Kf8 Ke6 Nh6 Kf6 Ng8 Kg5 Kf7 Rg2 Ne7 Rd2 Ke6 Re2 Kf7 Kf4 Kf6 Ke4 Ng8 Rg2 Kf7 Kd4 Ne7 Rg1 Kf6 Rf1 Ke6 Kc5 Ng6 Re1 Kf5 Kd4 Kf6 Ke4 Ne7 Rf1 Kg7 Rg1 Kf7 Ke5 Ng8 Rf1 Kg7 Kf5 Ne7 Ke4 Ng8 Ke5 Nh6 Rg1 Kf7 Kf4 Kf6 Ke4 Kf7 Ke3 Nf5 Kf4 Nd4 Ke4 Ne6 Ke5 Nd8 Kd5 Ke7 Rg7 Nf7 Rg3 Kf6 Rf3 Kg6 Ke6 Ng5 Ke7 Nxf3 Kd8 Kf6) {0.00/46 3} 118. Rf7 (Rf7 f3 Rxf3 Kxg7 Rg3 Kf8 Rg6 Ne7 Rf6 Ke8 Rh6 Ng8 Rh8 Kf8 Rh7 Ke8) {+0.19/8 1} f3 (f3 Rf8 Kxg7 Rf7 Kh6 Rxf3 Kg7 Rg3 Kf8 Rg1 Nh6 Kf6 Ng8 Kf5 Kf7 Re1 Nh6 Kf4 Ng8 Kg5 Kf8 Re6 Kf7 Rb6 Kg7 Rb7 Kf8 Rd7 Ne7 Kf4 Ng8 Kf5 Ne7 Kg5 Ng8 Rd8 Kf7 Rb8 Kg7 Ra8 Kf7 Kf4 Kg7 Ra7 Kf6 Ra6 Kf7 Ke5 Ne7 Ra3 Ng6 Kf5 Ne7 Kg5 Ng8 Rf3 Ke6 Rf1 Ne7 Rg1 Nc6 Re1 Kd6 Kf4 Kd5 Re8 Kd6 Ke4 Ne7 Rd8 Ke6 Rb8 Nd5 Kd4 Ne7 Rb6 Kf7 Ke3 Nd5 Kd2 Nxb6 Ke1 Ke6) {0.00/49 0} 119. Rxf3 (Rxf3 Kxg7 Rg3 Kf8 Rg6 Ne7 Rf6 Ke8 Rh6 Ng8 Rh8 Kf8 Rh7 Ke8 Ra7) {+0.13/8 0} Kxg7 (Kxg7 Ke5 Kg6 Rg3 Kf7 Rd3 Nh6 Rf3 Ke7 Rg3 Nf7 Ke4 Nd6 Kd5 Ne8 Ke5 Kd7 Rg6 Nc7 Rg5 Ne6 Rh5 Nc7 Rh7 Kc6 Kf5 Nd5 Ke6 Nc7 Ke5 Nb5 Rh6 Kc5 Rh1 Kc6 Rf1 Nc7 Rc1 Kd7 Rd1 Kc6 Rd6 Kc5 Rd8 Kc6 Rc8 Kb7 Rg8 Kc6 Rg4 Nb5 Rc4 Kd7 Kf6 Kd6 Rf4 Na3 Rf3 Nb5 Rd3 Kc6 Rd8 Nc7 Kg6 Ne6 Kf5 Nxd8 Kf6 Kd7) {0.00/51 3} 120. Rg3+ (Rg3 Kf8 Rg6 Ne7 Rf6 Ke8 Rh6 Ng8 Rh8 Kf8 Rh7 Ke8 Ra7 Kf8 Rf7) {+0.13/7 0} Kf8 (Kf8 Rf3 Kg7 Ke5 Kg6 Rg3 Kf7 Rd3 Nh6 Rf3 Ke7 Rg3 Nf7 Ke4 Nd6 Kd5 Ne8 Ke5 Kd7 Rg6 Nc7 Rg5 Ne6 Rh5 Nc7 Rh7 Kc6 Kf5 Nd5 Ke6 Nc7 Ke5 Nb5 Rh6 Kc5 Rh1 Kc6 Rf1 Nc7 Rc1 Kd7 Rd1 Kc6 Ke4 Nb5 Rg1 Nc7 Rg6 Kd7 Kd3 Ne8 Kd4 Nc7 Rb6 Ne8 Kd5 Nc7 Kc5 Ne6 Kc4 Nd8 Kd3 Ke7 Kd4 Ne6 Kd5 Nc7 Kc6 Ne8 Kc5 Kd7 Rb7 Ke6 Kd4 Nf6 Rb6 Kf5 Rb5 Ke6 Ra5 Ng4 Ra8 Kf5 Re8 Kg6 Kd5 Nf6 Ke6 Nxe8 Ke5 Kg5) {0.00/54 2} 121. Rg6 (Rg6 Ne7 Rf6 Ke8 Rh6 Ng8 Rh8 Kf8 Rh7 Ke8 Ra7 Kf8 Rf7 Ke8) {+0.18/7 1} Ne7 (Ne7 Rg3 Ng8 Rf3 Kg7 Ke5 Kg6 Rg3 Kf7 Rd3 Nh6 Rf3 Ke7 Rg3 Nf7 Ke4 Nd6 Kd5 Ne8 Ke5 Kd7 Rg6 Nc7 Rg5 Ne6 Rh5 Nc7 Rh7 Kc6 Kf5 Nd5 Ke6 Nc7 Ke5 Nb5 Rh6 Kc5 Rh1 Kc6 Rf1 Nc7 Rc1 Kd7 Kd4 Kd6 Ke4 Nb5 Kf5 Kd5 Rd1 Kc5 Ke6 Na3 Rd7 Nb5 Rg7 Kc6 Rg6 Nd4 Ke5 Kc5 Kf4 Nc6 Ke4 Nb4 Rg1 Nc6 Rd1 Na7 Rd3 Nb5 Rd8 Nd6 Ke5 Nc4 Ke6 Ne3 Rd7 Kc6 Kf6 Kxd7 Kg5 Ke6) {0.00/56 2} 122. Rf6+ (Rf6 Ke8 Rh6 Ng8 Rg6 Kf8 Kd6 Kf7 Rg2 Nh6 Ke5 Ng8) {+0.15/7 1} Ke8 (Ke8 Kd6 Nc8 Kc5 Ke7 Ra6 Kf7 Ra8 Ne7 Kd4 Ke6 Ra6 Kf5 Rb6 Ng6 Kd5 Nf4 Kd6 Ke4 Ra6 Nd3 Ra8 Kf5 Ra5 Kf6 Kd5 Nf4 Ke4 Ne6 Rf5 Ke7 Ke5 Nf8 Rf1 Nd7 Kd4 Nf6 Ra1 Kf8 Ke5 Ng8 Ra7 Ne7 Rd7 Ng8 Kf5 Ne7 Kg5 Ng8 Rd8 Kg7 Kf5 Ne7 Ke6 Ng8 Rc8 Nh6 Ke5 Ng4 Kf4 Nh6 Rc7 Kf6 Rc6 Kg7 Kg5 Ng8 Re6 Kf8 Rb6 Kf7 Rb7 Ke6 Rb1 Kf7 Rg1 Ke7 Rf1 Kd6 Re1 Kd7 Rd1 Ke7 Kf5 Kf7 Kf4 Kg7 Rf1 Nh6 Ke5 Ng4 Kf5 Ne3 Ke4 Nxf1 Ke5 Ng3) {0.00/50 2} 123. Rh6 (Rh6 Ng8 Rg6 Kf8 Kd6 Kf7 Ke5 Kxg6) {+0.14/7 1} Ng8 (Ng8 Rh8 Kf8 Rh7 Ke8 Ra7 Kf8 Ra8 Kg7 Rc8 Nh6 Rc7 Kg6 Ke5 Ng4 Kf4 Nh6 Ke4 Kf6 Kd4 Kf5 Rb7 Kf4 Kd3 Nf5 Rb5 Ng7 Kc4 Ke4 Rb1 Ne6 Ra1 Ke5 Re1 Kf5 Kd5 Nf4 Kd6 Kf6 Ra1 Kf5 Ra4 Nd3 Ra5 Kf6 Kd5 Nf4 Ke4 Ne6 Rf5 Ke7 Ke5 Nf8 Rf1 Nd7 Kd4 Ke6 Ra1 Ke7 Ra7 Ke6 Ke4 Nc5 Ke3 Kd5 Kf4 Ne6 Kg4 Ke5 Ra5 Kd4 Kf5 Nc5 Ra7 Kd5 Rh7 Ne4 Re7 Nd6 Kf4 Kd4 Rc7 Nc4 Rc6 Ne3 Re6 Nd5 Kf5 Ne3 Rxe3 Kxe3 Ke5 Kf3) {0.00/51 2} 124. Rh7 (Rh7 Kf8 Rf7 Ke8 Ra7 Kf8 Rf7 Ke8 Rf3 Nh6 Ra3 Kf8) {+0.11/7 1} Kf8 (Kf8 Ra7 Nh6 Ra3 Kg7 Rg3 Kf8 Rf3 Kg7 Ke5 Ng8 Ke4 Nf6 Kf5 Ng8 Rg3 Kf8 Ke4 Nf6 Kd3 Kf7 Kd4 Ng8 Rg1 Nf6 Re1 Ng8 Ra1 Nh6 Ra7 Kg6 Ra6 Kg5 Kc5 Nf5 Kd5 Nh6 Ra7 Kf5 Kc5 Ng4 Rf7 Ke4 Re7 Kf5 Kd6 Kg5 Rg7 Kf4 Rf7 Ke4 Rf8 Ne3 Re8 Kd4 Ke6 Nc4 Rd8 Ke3 Kd5 Nd2 Rd7 Nf3 Rf7 Nh2 Re7 Kf4 Re4 Kf5 Re6 Ng4 Ra6 Nf6 Kc4 Kg6 Kd3 Kg7 Ra8 Kh6 Rf8 Nd7 Ke3 Nxf8 Kf2 Ne6) {0.00/52 2} 125. Rf7+ (Rf7 Ke8 Rg7 Kf8 Rg6 Ne7 Rf6 Ke8 Rh6 Ng8 Rg6 Kf8 Rg4) {+0.11/8 0} Ke8 (Ke8) {0.00/1 0} 126. Rf2 (Rf2 Nh6 Ra2 Kf8 Ra7 Ng8 Rh7 Ke8 Ra7 Kf8 Rf7 Ke8 Rf4) {+0.15/7 1} Nh6 (Nh6 Kd5 Ke7 Rf1 Nf7 Rf3 Nh6 Ke5 Ng8 Re3 Kf7 Rg3 Kf8 Kf4 Ne7 Rg1 Ng8 Rd1 Kf7 Rf1 Kf6 Kg3 Ke7 Ra1 Ke6 Kf4 Kf6 Ra6 Kf7 Kg3 Ne7 Ra7 Ke6 Ra8 Ke5 Kf3 Ke6 Ra6 Ke5 Ra5 Nd5 Ra1 Kd4 Ra4 Ke5 Re4 Kf5 Re8 Nf6 Rf8 Ke5 Ra8 Ke6 Kf4 Nd5 Ke4 Nf6 Kf3 Kd6 Kf4 Nd5 Kf5 Ne7 Ke4 Nc6 Rg8 Ne7 Rg1 Ke6 Rf1 Nc6 Rd1 Nb4 Rc1 Kd6 Rg1 Nc6 Rg6 Kd7 Ke3 Ne5 Kf4 Nxg6 Ke4 Ne7) {0.00/53 6} 127. Ra2 (Ra2 Kf8 Ra7 Ng8 Kf5 Ne7 Ke6 Ng8 Rf7 Ke8 Rf4 Nh6 Rf6 Ng8) {+0.16/8 1} Kf8 (Kf8 Rg2 Ng8 Kd6 Nh6 Kc5 Ng8 Rb2 Kf7 Rb1 Ke6 Kd4 Ne7 Ke4 Nd5 Ra1 Nf6 Kd4 Nd7 Ra6 Ke7 Rg6 Nf6 Ke5 Nd7 Kf4 Kf7 Ra6 Ke7 Ra7 Kd6 Rb7 Ke6 Rb1 Kd6 Rd1 Ke6 Rg1 Nc5 Re1 Kd5 Rd1 Ke6 Rd8 Ke7 Rg8 Ke6 Ke3 Ke5 Rg5 Kd6 Kd4 Ne6 Kd3 Nxg5 Kc4 Ke5) {0.00/50 3} 128. Rg2 (Rg2 Ng8 Rg6 Ne7 Rg5 Ng8 Rg1 Nh6 Kf6 Ng8 Kg5 Kf7 Rf1) {+0.14/8 0} Ng8 (Ng8 Kd6 Nh6 Kc5 Ng8 Rb2 Kf7 Rb1 Ke6 Kd4 Ne7 Ke4 Nd5 Ra1 Nf6 Kd4 Nd7 Ra6 Ke7 Rg6 Nf6 Kd3 Kf7 Rg5 Nd7 Ke4 Ke6 Rg6 Ke7 Rc6 Nf6 Kf5 Nd7 Re6 Kf7 Rd6 Nf8 Rd1 Ng6 Rd7 Ne7 Ke5 Kf8 Rd1 Kg7 Rg1 Kf8 Rf1 Kg7 Rf3 Ng8 Ke4 Nf6 Kf5 Ng8 Rg3 Kf8 Kg5 Ke7 Re3 Kd6 Re1 Ne7 Kf6 Nd5 Kg6 Ne7 Kf7 Nf5 Re4 Kd5 Kf6 Kxe4 Kg5 Nd4) {0.00/51 1} 129. Rg6 (Rg6 Ne7 Rf6 Ke8 Rh6 Ng8 Rh8 Kf8 Rh5 Kg7 Rg5 Kf8) {+0.16/7 0} Ne7 (Ne7 Rg3 Ng8 Rf3 Kg7 Ke5 Kg6 Rg3 Kf7 Kf4 Nh6 Rg2 Kf6 Kf3 Nf5 Ke4 Nh6 Rg3 Kf7 Rf3 Ke6 Kf4 Nf7 Re3 Kf6 Re1 Nd6 Rf1 Ke6 Rg1 Kf6 Rg5 Nc4 Rg8 Nd6 Rb8 Ke6 Rb6 Kd5 Ra6 Nc4 Rg6 Kd4 Kf5 Kd5 Kf6 Nd6 Rg5 Kc6 Ke6 Nb5 Rg3 Nc7 Kf5 Kc5 Rd3 Kc6 Kf6 Nb5 Ke7 Na7 Rd2 Nb5 Rd7 Nc7 Rd6 Kc5 Rd1 Nd5 Ke6 Nc7 Kf5 Nb5 Rg1 Nd4 Ke5 Nf3 Kf4 Nxg1 Ke3 Kb4) {0.00/52 2} 130. Rf6+ (Rf6 Ke8 Rh6 Ng8 Rh5 Kf8 Rh7 Ke8 Ra7 Kf8 Rf7 Ke8 Rf4) {+0.11/7 1} Ke8 (Ke8 Kd6 Nc8 Kc5 Ke7 Ra6 Kf7 Ra8 Ne7 Kd4 Nf5 Ke5 Nh6 Kd5 Kf6 Ra6 Kg7 Ra7 Kf6 Rc7 Ng8 Rc6 Kg7 Rb6 Nf6 Kc4 Kf7 Rb7 Ke6 Ra7 Ne4 Kd4 Nf6 Ra6 Kf5 Rb6 Ne4 Rb5 Kf4 Rb7 Nf6 Rb6 Ne4 Re6 Ng3 Rf6 Nf5 Kd5 Kg5 Ra6 Nh6 Rb6 Ng4 Kd4 Kf4 Rb1 Ne3 Rb8 Nf5 Kd5 Ne3 Kc5 Ke5 Re8 Kf4 Kd4 Nf5 Kd3 Kg5 Ke2 Kf6 Kf3 Kg7 Kf4 Kf7 Kxf5 Kxe8 Ke6 Kf8) {0.00/53 3} 131. Rh6 (Rh6 Ng8 Rg6 Kf8 Kd7 Kf7 Re6 Kg7 Rd6 Kf7) {+0.13/7 0} Ng8 (Ng8 Rh8 Kf8 Rh7 Ke8 Ra7 Kf8 Rf7 Ke8 Rf1 Nh6 Rd1 Ng8 Rb1 Kf8 Kf5 Ne7 Kf4 Kf7 Ke4 Ke6 Rb6 Kf7 Rb7 Kf6 Rc7 Ng6 Rc6 Kf7 Ra6 Ne7 Kd4 Kg7 Rb6 Nf5 Ke5 Nh6 Rb7 Kg6 Kf4 Kf6 Ke4 Nf7 Kf3 Nd6 Rc7 Ke5 Ra7 Nc4 Ra1 Kd5 Rd1 Ke5 Re1 Kd5 Kf4 Kd6 Ke4 Nd2 Kf5 Nc4 Rd1 Kc5 Ke4 Nd6 Kf4 Kc6 Kg5 Nc4 Kf6 Kc5 Kf5 Ne3 Ke5 Nxd1 Kf6 Nc3) {0.00/52 2} 132. Rg6 (Rg6 Kf8 Kd7 Kf7 Rg2 Nf6 Kd6 Ne8 Ke5 Ng7) {+0.11/7 1} Kf8 (Kf8 Kd6 Kf7 Rg4 Kf8 Rg1 Nf6 Ke5 Ng8 Rf1 Kg7 Kd4 Kg6 Ke3 Nf6 Kf4 Ng8 Rg1 Kf7 Rg2 Nf6 Rb2 Nd5 Ke5 Ne7 Rb7 Kf8 Rb8 Kf7 Ra8 Kg7 Kd4 Nf5 Kc4 Ne3 Kb5 Nf5 Ka4 Kf6 Ra5 Ke6 Kb4 Nd6 Kb3 Ne4 Kc2 Nd6 Kd2 Ne8 Ra6 Kd5 Kd3 Nd6 Rb6 Ke5 Rxd6 Kxd6 Ke2 Kc5) {0.00/47 2} 133. Rg1 (Rg1 Nh6 Rg2 Ng8 Rd2 Kg7 Rd7 Kg6 Rf7 Nh6) {+0.13/7 1} Nh6 (Nh6 Re1 Kg7 Kd6 Nf5 Kd7 Kf6 Re6 Kg7 Rb6 Nh6 Rb7 Kg6 Kc6 Nf7 Kd5 Kf6 Rb6 Kg7 Ke6 Nh6 Rb7 Kg6 Kd5 Ng8 Rb8 Nf6 Kd6 Ng4 Rg8 Kf5 Rf8 Ke4 Re8 Kf5 Re2 Kf4 Kc5 Ne3 Kd4 Nf5 Kd3 Kg4 Rg2 Kf4 Rf2 Ke5 Re2 Kd5 Rg2 Nd6 Rg5 Ke6 Ra5 Nf5 Rc5 Ne7 Kd4 Nf5 Kc3 Ng3 Rc8 Kf7 Kd4 Nf5 Kd5 Ne7 Ke4 Nxc8 Kf4 Nd6) {0.00/48 2} 134. Rg2 (Rg2 Ng8 Rg3 Nh6 Kf6 Ng8 Kf5 Kf7 Rf3) {+0.11/7 1} Ng8 (Ng8 Kd6 Nf6 Rf2 Kg7 Rf1 Ng8 Ke5 Ne7 Rf3 Ng8 Rg3 Kf8 Kf4 Ne7 Rg1 Ng8 Rg6 Kf7 Rg2 Nf6 Rb2 Nd5 Ke5 Ne7 Rb7 Kf8 Rb8 Kf7 Ra8 Kg7 Kd4 Nf5 Kc4 Ne3 Kb5 Nf5 Ka4 Kf6 Ra5 Ke6 Kb4 Nd6 Kb3 Ne4 Kc4 Nd6 Kd4 Nf5 Ke4 Ng3 Kf3 Nf5 Ra8 Ke5 Ke2 Ke4 Kd2 Nd4 Re8 Kd5 Re1 Nf3 Kd3 Nxe1 Kc3 Nf3) {0.00/53 2} 135. Rg3 (Rg3 Nh6 Rg1 Ng8 Rf1 Kg7 Rf7 Kg6 Rf8 Kg7 Rf7 Kg6) {+0.11/7 0} Nh6 (Nh6 Rg1 Ng8 Rc1 Nh6 Rf1 Kg7 Rg1 Kf8 Rc1 Kg7 Rc7 Kg6 Ra7 Ng4 Rb7 Nh6 Kd6 Nf5 Kd5 Nh6 Rb6 Kg5 Ke4 Ng4 Rb5 Kf6 Rf5 Kg6 Kf4 Nh6 Rf8 Kg7 Ra8 Kg6 Ra6 Kg7 Kg5 Nf7 Kh5 Ne5 Rb6 Nc4 Rc6 Ne5 Ra6 Nf7 Rg6 Kf8 Rg1 Nd6 Re1 Kf7 Kg5 Ne8 Rf1 Ke7 Ra1 Kd6 Ra6 Ke7 Kg6 Nc7 Kf5 Nxa6 Ke4 Nb4) {0.00/53 2} 136. Rg6 (Rg6 Ng8 Kd6 Kf7 Rg7 Kxg7) {+0.10/6 1} Ng8 (Ng8 Kd6 Kf7 Rg4 Kf8 Rg1 Nf6 Ke5 Ng8 Ke4 Ne7 Rf1 Kg7 Ke3 Ng6 Kd4 Ne7 Ke4 Ng8 Ke5 Ne7 Rf3 Ng8 Rg3 Kf7 Ke4 Ne7 Kf3 Kf6 Rg2 Ke5 Re2 Kf6 Re1 Nd5 Ke4 Ne7 Kd3 Ng6 Kd2 Nf4 Rb1 Nd5 Kd3 Nf4 Ke4 Ne6 Rf1 Ke7 Ke5 Nc7 Rf6 Ne8 Re6 Kd7 Rg6 Nc7 Rg7 Kc6 Ke4 Nb5 Rf7 Nd6 Kd3 Nxf7 Ke3 Kc7) {0.00/50 2} 137. Rg2 (Rg2 Nh6 Rg3 Ng8 Rh3 Kg7 Rg3 Kf8) {+0.12/6 0} Nh6 (Nh6 Kf6 Ng8 Kg6 Ne7 Kg5 Ng8 Rf2 Ke7 Re2 Kd6 Re1 Ne7 Kf6 Nd5 Kf5 Ne7 Ke4 Nc6 Rd1 Ke6 Rg1 Ne5 Rg5 Nd7 Rg6 Nf6 Kd4 Kf5 Rg1 Nd7 Rf1 Ke6 Re1 Kd6 Ke3 Nc5 Kf3 Kd5 Rd1 Ke6 Ra1 Kd5 Kg4 Ne4 Kf4 Nc5 Rd1 Ke6 Kf3 Ke5 Kg4 Ne4 Rf1 Kd4 Kh5 Ng3 Kg5 Nxf1 Kh6 Kc5) {0.00/51 2} 138. Rg3 (Rg3 Ng8 Rg5 Nh6 Rg1 Ng8 Rg2 Nh6 Rg6 Ng8) {+0.10/6 1} Ng8 (Ng8 Rf3 Kg7 Rb3 Kf8 Kf5 Ne7 Kf4 Ng8 Rf3 Kg7 Re3 Kf7 Re5 Kf6 Rf5 Ke6 Rg5 Ne7 Re5 Kd6 Ke4 Nc6 Rd5 Ke6 Rd1 Ne5 Kf4 Nc6 Rg1 Ne7 Re1 Kd6 Ke4 Nc6 Rd1 Ke6 Rg1 Ne5 Rg8 Nd7 Re8 Kd6 Ra8 Nf6 Kf5 Nd7 Rg8 Kd5 Rg6 Nc5 Rg1 Nd3 Ra1 Nc5 Ra7 Ne4 Rd7 Nd6 Kf4 Kc6 Ke5 Kxd7 Kf4 Ke6 Kg4) {0.00/50 1} 139. Rg5 (Rg5 Nh6 Rg1 Ng8 Rg2 Nh6 Kf6 Ng8 Ke6) {+0.11/6 1} Nh6 (Nh6 Kd5 Kf7 Re5 Kg7 Kc4 Kf7 Kd3 Kf6 Ke4 Ng4 Rf5 Ke6 Rg5 Nf6 Kd3 Kd6 Kd4 Nd7 Rg6 Ke7 Kd5 Nf6 Ke5 Nd7 Kf4 Nf6 Kg5 Ne8 Rc6 Nd6 Rc1 Nf7 Kf4 Kd6 Rg1 Ke6 Rg6 Kd5 Rb6 Nd6 Rb8 Ne4 Rb1 Nc3 Kf5 Nxb1 Kf4 Nc3) {0.00/44 2} 140. Kf6 (Kf6 Ng8 Kg6 Ne7 Kf6 Ng8 Ke6 Nh6 Rg1 Ng8 Rg2 Nh6 Rg6 Ng8) {+0.10/6 0} Ng8+ (Ng8 Ke6 Nh6 Kd5 Nf7 Rg2 Nh6 Rb2 Kf7 Rb7 Kg6 Rb6 Kg5 Ke5 Ng4 Kd4 Kf4 Rb1 Nf6 Rb7 Kf5 Rb5 Ke6 Rb8 Nd7 Ra8 Kd6 Ra6 Ke7 Rg6 Nf6 Ke5 Nd7 Kf4 Nf6 Kg5 Ne8 Rc6 Nd6 Rc1 Nf7 Kf4 Kd6 Ke4 Ke7 Rc7 Ke6 Rc6 Ke7 Kf4 Nd8 Rc8 Kd7 Ke4 Kxc8 Kd5 Kb7) {0.00/45 2} 141. Ke6 (Ke6 Nh6 Rg1 Ng8 Rc1 Nh6 Rc7 Ng8 Rf7 Ke8 Rf4 Nh6) {+0.09/7 0} Nh6 (Nh6 Kd5 Nf7 Rg2 Nh6 Rb2 Kf7 Rb7 Kg6 Rb6 Kg5 Ke5 Ng4 Kd4 Kf4 Rb1 Nf6 Rb7 Kf5 Rb5 Ke6 Rb8 Nd7 Ra8 Kd6 Ra6 Ke7 Rg6 Nf6 Ke5 Nd7 Kf4 Nf6 Kg5 Ne8 Rc6 Nd6 Rc1 Nf7 Kf4 Kd6 Ke4 Ke7 Rc7 Ke6 Rc6 Ke7 Kf4 Nd8 Rc8 Kd7 Ke4 Kxc8 Kd5 Kb7) {0.00/51 0} 142. Rg1 (Rg1 Ng8 Rg2 Nh6 Rd2 Ng8 Rd7 Nh6 Rh7 Ng8) {+0.10/5 0} Ng8 (Ng8 Rc1 Nh6 Rf1 Kg7 Rg1 Kf8 Rc1 Kg7 Rc7 Kg6 Ra7 Ng4 Rb7 Nh6 Kd6 Nf5 Kd5 Nh6 Rb6 Kg5 Ke5 Ng4 Kd4 Kf4 Rb1 Nf6 Rb7 Kf5 Rb5 Ke6 Rb8 Nd7 Ra8 Kd6 Ra6 Ke7 Rg6 Nf6 Ke5 Nd7 Kf4 Nf6 Kf5 Nd5 Rc6 Ne3 Ke4 Ng4 Kf4 Nf6 Rxf6 Kxf6 Ke3 Kg5) {0.00/54 1} 143. Rg3 (Rg3 Nh6 Rg1 Ng8 Rh1 Kg7 Rg1 Kf8) {+0.12/6 1} Nh6 (Nh6 Rg1 Ng8 Rc1 Nh6 Rf1 Kg7 Rg1 Kf8 Rc1 Kg7 Rc7 Kg6 Ra7 Ng4 Rb7 Nh6 Kd6 Nf5 Kd5 Nh6 Rb6 Kg5 Ke5 Ng4 Kd4 Kf4 Rb1 Nf6 Rb7 Kf5 Rb5 Ke6 Re5 Kd6 Re1 Nd5 Ke4 Ne7 Ra1 Kd7 Ra6 Ke8 Ke5 Kf8 Rb6 Ng8 Kf5 Ke8 Rb8 Kf7 Rxg8 Kxg8 Kg6 Kf8 Kh5 Ke7) {0.00/54 3} 144. Rg1 (Rg1 Ng8 Rg2 Nh6 Rg6 Ng8) {+0.09/6 1} Ng8 (Ng8 Rc1 Nh6 Rf1 Kg7 Rg1 Kf8 Rc1 Kg7 Rc7 Kg6 Ra7 Ng4 Rb7 Nh6 Kd6 Nf5 Kd5 Nh6 Rb6 Kg5 Ke5 Ng4 Ke6 Kf4 Rb4 Kf3 Rb1 Kf4 Ra1 Nh6 Ra4 Kg5 Ke5 Nf7 Ke4 Nd6 Kd5 Nf7 Ra7 Kg6 Ke4 Nh6 Kf4 Kf6 Ra6 Kg7 Rxh6 Kxh6 Ke5 Kg6 Ke6 Kg7) {0.00/53 0} 145. Rg2 (Rg2 Nh6 Rg3 Ng8) {+0.10/6 0} Nh6 (Nh6 Rb2 Ng8 Ke5 Kf7 Kd6 Kg7 Rb7 Kf6 Rb6 Ne7 Kd7 Kf7 Rb1 Ng8 Kd6 Kg6 Rg1 Kf7 Rg4 Kf8 Rf4 Kg7 Rf3 Kg6 Ke5 Kg7 Kf5 Nh6 Kg5 Ng8 Rh3 Kf8 Kf5 Kf7 Rh7 Kf8 Ra7 Ne7 Ke4 Nc6 Ra1 Ke7 Kd5 Kd7 Re1 Ne7 Rxe7 Kxe7 Ke5 Kd7 Kf6 Kd6) {0.00/50 2} 146. Rg5 (Rg5 Ng8 Rg1 Nh6 Kf6 Ng8 Ke5 Kf7 Rf1) {+0.08/6 1} Ng8 (Ng8 Rc5 Kg7 Rc7 Kg6 Ra7 Nh6 Ra3 Ng4 Rg3 Kg5 Rg2 Kf4 Ra2 Ke4 Ra4 Kf3 Kf5 Ne3 Kf6 Ng4 Ke6 Nh6 Rb4 Ng4 Kd5 Ne3 Ke5 Ng4 Kd4 Nh6 Rb1 Kf4 Ra1 Nf5 Kd5 Kf3 Ke5 Ne3 Ra3 Ke2 Kf4 Nf1 Rf3 Ke1 Rxf1 Kxf1 Kg3 Kg1 Kg4 Kf2) {0.00/49 2} 147. Rg1 (Rg1 Nh6 Rg3 Ng8) {+0.11/6 0} Nh6 (Nh6 Ke5 Nf7 Kf6 Nh6 Kg6 Ng8 Rg5 Ne7 Kh7 Kf7 Ra5 Kf8 Ra3 Ke8 Ra8 Kf7 Ra2 Nd5 Ra6 Nf6 Kh6 Ng8 Kh5 Nf6 Kh4 Nd5 Kg5 Ne7 Rb6 Ke8 Rb7 Ng8 Ra7 Kf8 Kh5 Nf6 Kh6 Ng8 Kg5 Ke8 Kg6 Ne7 Rxe7 Kxe7 Kg7 Ke6) {0.00/43 1} 148. Kf6 (Kf6 Ng8 Kg6 Ne7 Kf6 Ng8 Ke6) {+0.08/6 1} Ng8+ (Ng8 Kg6 Ne7 Kg5 Ng8 Ra1 Kg7 Re1 Kf8 Kf4 Nf6 Rf1 Ng8 Rf3 Kg7 Re3 Kf7 Rd3 Nf6 Ke5 Ke7 Kf5 Ne8 Re3 Kd7 Kg6 Nc7 Re4 Kc6 Kf5 Nb5 Ke5 Kd7 Rf4 Nc7 Rg4 Ne6 Re4 Nd8 Rd4 Ke7 Rxd8 Kxd8 Kd6 Ke8 Kc5 Kf7 Kb4 Ke7) {0.00/47 2} 149. Kg6 (Kg6 Ne7 Kf6 Ng8 Kg6 Ne7 Kh7 Kf7 Rf1 Ke6 Re1 Kf6 Re2) {+0.08/7 0} Ne7+ (Ne7 Kg5 Ng8 Ra1 Kg7 Re1 Kf8 Kf4 Nf6 Rf1 Ng8 Rf3 Kg7 Re3 Kf7 Rd3 Nf6 Ke5 Ke7 Kf5 Ne8 Re3 Kd7 Rf3 Ng7 Kg6 Ne6 Rf6 Nc7 Rf1 Nd5 Kg5 Nc7 Re1 Ne8 Kg4 Nd6 Rd1 Ke6 Rxd6 Kxd6 Kh3 Ke5) {0.00/42 0} 150. Kf6 (Kf6 Ng8 Kg6 Ne7 Kh7 Kf7 Rf1 Ke6 Re1 Kf6 Re2) {+0.09/7 0} Ng8+ (Ng8 Kg6 Ne7 Kg5 Nd5 Rd1 Ne7 Rd7 Kf7 Rb7 Kf8 Kf6 Ng8 Kf5 Ne7 Ke5 Ng8 Kd6 Nh6 Rb6 Nf7 Ke6 Nh6 Rb1 Kg7 Rf1 Ng8 Re1 Kf8 Rh1 Kg7 Rh5 Kg6 Rf5 Nh6 Rf6 Kg5 Rxh6 Kxh6 Kf6 Kh7 Kf7 Kh6 Ke8 Kg6) {0.00/39 15} 151. Kg6 (Kg6 Ne7 Kf6 Ng8) {+0.06/7 1} Ne7+ (Ne7 Kg5 Nd5 Rd1 Ne7 Rd7 Kf7 Rb7 Kf8 Kf6 Ng8 Kf5 Ne7 Ke5 Ng8 Rd7 Nh6 Ra7 Ke8 Kf6 Ng8 Kf5 Kf8 Kg5 Ne7 Ra6 Kg7 Rf6 Ng8 Rf3 Nh6 Rg3 Ng8 Kh4 Kf7 Rxg8 Kxg8 Kh3 Kh7) {0.00/33 0} 152. Kf6 (Kf6 Ng8) {+0.05/5 0} Ng8+ (Ng8) {0.00/52 1 3-fold repetition} 1/2-1/2 ================================================ FILE: files/misc/pgn.txt ================================================ Standard: Portable Game Notation Specification and Implementation Guide Revised: 1994.03.12 Authors: Interested readers of the Internet newsgroup rec.games.chess Coordinator: Steven J. Edwards (send comments to sje@world.std.com) 0: Preface From the Tower of Babel story: "If now, while they are one people, all speaking the same language, they have started to do this, nothing will later stop them from doing whatever they propose to do." Genesis XI, v.6, _New American Bible_ 1: Introduction PGN is "Portable Game Notation", a standard designed for the representation of chess game data using ASCII text files. PGN is structured for easy reading and writing by human users and for easy parsing and generation by computer programs. The intent of the definition and propagation of PGN is to facilitate the sharing of public domain chess game data among chessplayers (both organic and otherwise), publishers, and computer chess researchers throughout the world. PGN is not intended to be a general purpose standard that is suitable for every possible use; no such standard could fill all conceivable requirements. Instead, PGN is proposed as a universal portable representation for data interchange. The idea is to allow the construction of a family of chess applications that can quickly and easily process chess game data using PGN for import and export among themselves. 2: Chess data representation Computer usage among chessplayers has become quite common in recent years and a variety of different programs, both commercial and public domain, are used to generate, access, and propagate chess game data. Some of these programs are rather impressive; most are now well behaved in that they correctly follow the Laws of Chess and handle users' data with reasonable care. Unfortunately, many programs have had serious problems with several aspects of the external representation of chess game data. Sometimes these problems become more visible when a user attempts to move significant quantities of data from one program to another; if there has been no real effort to ensure portability of data, then the chances for a successful transfer are small at best. 2.1: Data interchange incompatibility The reasons for format incompatibility are easy to understand. In fact, most of them are correlated with the same problems that have already been seen with commercial software offerings for other domains such as word processing, spreadsheets, fonts, and graphics. Sometimes a manufacturer deliberately designs a data format using encryption or some other secret, proprietary technique to "lock in" a customer. Sometimes a designer may produce a format that can be deciphered without too much difficulty, but at the same time publicly discourage third party software by claiming trade secret protection. Another software producer may develop a non-proprietary system, but it may work well only within the scope of a single program or application because it is not easily expandable. Finally, some other software may work very well for many purposes, but it uses symbols and language not easily understood by people or computers available to those outside the country of its development. 2.2: Specification goals A specification for a portable game notation must observe the lessons of history and be able to handle probable needs of the future. The design criteria for PGN were selected to meet these needs. These criteria include: 1) The details of the system must be publicly available and free of unnecessary complexity. Ideally, if the documentation is not available for some reason, typical chess software developers and users should be able to understand most of the data without the need for third party assistance. 2) The details of the system must be non-proprietary so that users and software developers are unrestricted by concerns about infringing on intellectual property rights. The idea is to let chess programmers compete in a free market where customers may choose software based on their real needs and not based on artificial requirements created by a secret data format. 3) The system must work for a variety of programs. The format should be such that it can be used by chess database programs, chess publishing programs, chess server programs, and chessplaying programs without being unnecessarily specific to any particular application class. 4) The system must be easily expandable and scalable. The expansion ability must include handling data items that may not exist currently but could be expected to emerge in the future. (Examples: new opening classifications and new country names.) The system should be scalable in that it must not have any arbitrary restrictions concerning the quantity of stored data. Also, planned modes of expansion should either preserve earlier databases or at least allow for their automatic conversion. 5) The system must be international. Chess software users are found in many countries and the system should be free of difficulties caused by conventions local to a given region. 6) Finally, the system should handle the same kinds and amounts of data that are already handled by existing chess software and by print media. 2.3: A sample PGN game Although its description may seem rather lengthy, PGN is actually fairly simple. A sample PGN game follows; it has most of the important features described in later sections of this document. [Event "F/S Return Match"] [Site "Belgrade, Serbia JUG"] [Date "1992.11.04"] [Round "29"] [White "Fischer, Robert J."] [Black "Spassky, Boris V."] [Result "1/2-1/2"] 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7 11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5 Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5 hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6 Nf2 42. g4 Bd3 43. Re6 1/2-1/2 3: Formats: import and export There are two formats in the PGN specification. These are the "import" format and the "export" format. These are the two different ways of formatting the same PGN data according to its source. The details of the two formats are described throughout the following sections of this document. Other than formats, there is the additional topic of PGN presentation. While both PGN import and export formats are designed to be readable by humans, there is no recommendation that either of these be an ultimate mode of chess data presentation. Rather, software developers are urged to consider all of the various techniques at their disposal to enhance the display of chess data at the presentation level (i.e., highest level) of their programs. This means that the use of different fonts, character sizes, color, and other tools of computer aided interaction and publishing should be explored to provide a high quality presentation appropriate to the function of the particular program. 3.1: Import format allows for manually prepared data The import format is rather flexible and is used to describe data that may have been prepared by hand, much like a source file for a high level programming language. A program that can read PGN data should be able to handle the somewhat lax import format. 3.2: Export format used for program generated output The export format is rather strict and is used to describe data that is usually prepared under program control, something like a pretty printed source program reformatted by a compiler. 3.2.1: Byte equivalence For a given PGN data file, export format representations generated by different PGN programs on the same computing system should be exactly equivalent, byte for byte. 3.2.2: Archival storage and the newline character Export format should also be used for archival storage. Here, "archival" storage is defined as storage that may be accessed by a variety of computing systems. The only extra requirement for archival storage is that the newline character have a specific representation that is independent of its value for a particular computing system's text file usage. The archival representation of a newline is the ASCII control character LF (line feed, decimal value 10, hexadecimal value 0x0a). Sadly, there are some accidents of history that survive to this day that have baroque representations for a newline: multicharacter sequences, end-of-line record markers, start-of-line byte counts, fixed length records, and so forth. It is well beyond the scope of the PGN project to reconcile all of these to the unified world of ANSI C and the those enjoying the bliss of a single '\n' convention. Some systems may just not be able to handle an archival PGN text file with native text editors. In these cases, an indulgence of sorts is granted to use the local newline convention in non-archival PGN files for those text editors. 3.2.3: Speed of processing Several parts of the export format deal with exact descriptions of line and field justification that are absent from the import format details. The main reason for these restrictions on the export format are to allow the construction of simple data translation programs that can easily scan PGN data without having to have a full chess engine or other complex parsing routines. The idea is to encourage chess software authors to always allow for at least a limited PGN reading capability. Even when a full chess engine parsing capability is available, it is likely to be at least two orders of magnitude slower than a simple text scanner. 3.2.4: Reduced export format A PGN game represented using export format is said to be in "reduced export format" if all of the following hold: 1) it has no commentary, 2) it has only the standard seven tag roster identification information ("STR", see below), 3) it has no recursive annotation variations ("RAV", see below), and 4) it has no numeric annotation glyphs ("NAG", see below). Reduced export format is used for bulk storage of unannotated games. It represents a minimum level of standard conformance for a PGN exporting application. 4: Lexicographical issues PGN data is composed of characters; non-overlapping contiguous sequences of characters form lexical tokens. 4.1: Character codes PGN data is represented using a subset of the eight bit ISO 8859/1 (Latin 1) character set. ("ISO" is an acronym for the International Standards Organization.) This set is also known as ECMA-94 and is similar to other ISO Latin character sets. ISO 8859/1 includes the standard seven bit ASCII character set for the 32 control character code values from zero to 31. The 95 printing character code values from 32 to 126 are also equivalent to seven bit ASCII usage. (Code value 127, the ASCII DEL control character, is a graphic character in ISO 8859/1; it is not used for PGN data representation.) The 32 ISO 8859/1 code values from 128 to 159 are non-printing control characters. They are not used for PGN data representation. The 32 code values from 160 to 191 are mostly non-alphabetic printing characters and their use for PGN data is discouraged as their graphic representation varies considerably among other ISO Latin sets. Finally, the 64 code values from 192 to 255 are mostly alphabetic printing characters with various diacritical marks; their use is encouraged for those languages that require such characters. The graphic representations of this last set of 64 characters is fairly constant for the ISO Latin family. Printing character codes outside of the seven bit ASCII range may only appear in string data and in commentary. They are not permitted for use in symbol construction. Because some PGN users' environments may not support presentation of non-ASCII characters, PGN game authors should refrain from using such characters in critical commentary or string values in game data that may be referenced in such environments. PGN software authors should have their programs handle such environments by displaying a question mark ("?") for non-ASCII character codes. This is an important point because there are many computing systems that can display eight bit character data, but the display graphics may differ among machines and operating systems from different manufacturers. Only four of the ASCII control characters are permitted in PGN import format; these are the horizontal and vertical tabs along with the linefeed and carriage return codes. The external representation of the newline character may differ among platforms; this is an acceptable variation as long as the details of the implementation are hidden from software implementors and users. When a choice is practical, the Unix "newline is linefeed" convention is preferred. 4.2: Tab characters Tab characters, both horizontal and vertical, are not permitted in the export format. This is because the treatment of tab characters is highly dependent upon the particular software in use on the host computing system. Also, tab characters may not appear inside of string data. 4.3: Line lengths PGN data are organized as simple text lines without any special bytes or markers for secondary record structure imposed by specific operating systems. Import format PGN text lines are limited to having a maximum of 255 characters per line including the newline character. Lines with 80 or more printing characters are strongly discouraged because of the difficulties experienced by common text editors with long lines. In some cases, very long tag values will require 80 or more columns, but these are relatively rare. An example of this is the "FEN" tag pair; it may have a long tag value, but this particular tag pair is only used to represent a game that doesn't start from the usual initial position. 5: Commentary Comment text may appear in PGN data. There are two kinds of comments. The first kind is the "rest of line" comment; this comment type starts with a semicolon character and continues to the end of the line. The second kind starts with a left brace character and continues to the next right brace character. Comments cannot appear inside any token. Brace comments do not nest; a left brace character appearing in a brace comment loses its special meaning and is ignored. A semicolon appearing inside of a brace comment loses its special meaning and is ignored. Braces appearing inside of a semicolon comments lose their special meaning and are ignored. *** Export format representation of comments needs definition work. 6: Escape mechanism There is a special escape mechanism for PGN data. This mechanism is triggered by a percent sign character ("%") appearing in the first column of a line; the data on the rest of the line is ignored by publicly available PGN scanning software. This escape convention is intended for the private use of software developers and researchers to embed non-PGN commands and data in PGN streams. A percent sign appearing in any other place other than the first position in a line does not trigger the escape mechanism. 7: Tokens PGN character data is organized as tokens. A token is a contiguous sequence of characters that represents a basic semantic unit. Tokens may be separated from adjacent tokens by white space characters. (White space characters include space, newline, and tab characters.) Some tokens are self delimiting and do not require white space characters. A string token is a sequence of zero or more printing characters delimited by a pair of quote characters (ASCII decimal value 34, hexadecimal value 0x22). An empty string is represented by two adjacent quotes. (Note: an apostrophe is not a quote.) A quote inside a string is represented by the backslash immediately followed by a quote. A backslash inside a string is represented by two adjacent backslashes. Strings are commonly used as tag pair values (see below). Non-printing characters like newline and tab are not permitted inside of strings. A string token is terminated by its closing quote. Currently, a string is limited to a maximum of 255 characters of data. An integer token is a sequence of one or more decimal digit characters. It is a special case of the more general "symbol" token class described below. Integer tokens are used to help represent move number indications (see below). An integer token is terminated just prior to the first non-symbol character following the integer digit sequence. A period character (".") is a token by itself. It is used for move number indications (see below). It is self terminating. An asterisk character ("*") is a token by itself. It is used as one of the possible game termination markers (see below); it indicates an incomplete game or a game with an unknown or otherwise unavailable result. It is self terminating. The left and right bracket characters ("[" and "]") are tokens. They are used to delimit tag pairs (see below). Both are self terminating. The left and right parenthesis characters ("(" and ")") are tokens. They are used to delimit Recursive Annotation Variations (see below). Both are self terminating. The left and right angle bracket characters ("<" and ">") are tokens. They are reserved for future expansion. Both are self terminating. A Numeric Annotation Glyph ("NAG", see below) is a token; it is composed of a dollar sign character ("$") immediately followed by one or more digit characters. It is terminated just prior to the first non-digit character following the digit sequence. A symbol token starts with a letter or digit character and is immediately followed by a sequence of zero or more symbol continuation characters. These continuation characters are letter characters ("A-Za-z"), digit characters ("0-9"), the underscore ("_"), the plus sign ("+"), the octothorpe sign ("#"), the equal sign ("="), the colon (":"), and the hyphen ("-"). Symbols are used for a variety of purposes. All characters in a symbol are significant. A symbol token is terminated just prior to the first non-symbol character following the symbol character sequence. Currently, a symbol is limited to a maximum of 255 characters in length. 8: Parsing games A PGN database file is a sequential collection of zero or more PGN games. An empty file is a valid, although somewhat uninformative, PGN database. A PGN game is composed of two sections. The first is the tag pair section and the second is the movetext section. The tag pair section provides information that identifies the game by defining the values associated with a set of standard parameters. The movetext section gives the usually enumerated and possibly annotated moves of the game along with the concluding game termination marker. The chess moves themselves are represented using SAN (Standard Algebraic Notation), also described later in this document. 8.1: Tag pair section The tag pair section is composed of a series of zero or more tag pairs. A tag pair is composed of four consecutive tokens: a left bracket token, a symbol token, a string token, and a right bracket token. The symbol token is the tag name and the string token is the tag value associated with the tag name. (There is a standard set of tag names and semantics described below.) The same tag name should not appear more than once in a tag pair section. A further restriction on tag names is that they are composed exclusively of letters, digits, and the underscore character. This is done to facilitate mapping of tag names into key and attribute names for use with general purpose database programs. For PGN import format, there may be zero or more white space characters between any adjacent pair of tokens in a tag pair. For PGN export format, there are no white space characters between the left bracket and the tag name, there are no white space characters between the tag value and the right bracket, and there is a single space character between the tag name and the tag value. Tag names, like all symbols, are case sensitive. All tag names used for archival storage begin with an upper case letter. PGN import format may have multiple tag pairs on the same line and may even have a tag pair spanning more than a single line. Export format requires each tag pair to appear left justified on a line by itself; a single empty line follows the last tag pair. Some tag values may be composed of a sequence of items. For example, a consultation game may have more than one player for a given side. When this occurs, the single character ":" (colon) appears between adjacent items. Because of this use as an internal separator in strings, the colon should not otherwise appear in a string. The tag pair format is designed for expansion; initially only strings are allowed as tag pair values. Tag value formats associated with the STR (Seven Tag Roster, see below) will not change; they will always be string values. However, there are long term plans to allow general list structures as tag values for non-STR tag pairs. Use of these expanded tag values will likely be restricted to special research programs. In all events, the top level structure of a tag pair remains the same: left bracket, tag name, tag value, and right bracket. 8.1.1: Seven Tag Roster There is a set of tags defined for mandatory use for archival storage of PGN data. This is the STR (Seven Tag Roster). The interpretation of these tags is fixed as is the order in which they appear. Although the definition and use of additional tag names and semantics is permitted and encouraged when needed, the STR is the common ground that all programs should follow for public data interchange. For import format, the order of tag pairs is not important. For export format, the STR tag pairs appear before any other tag pairs. (The STR tag pairs must also appear in order; this order is described below). Also for export format, any additional tag pairs appear in ASCII order by tag name. The seven tag names of the STR are (in order): 1) Event (the name of the tournament or match event) 2) Site (the location of the event) 3) Date (the starting date of the game) 4) Round (the playing round ordinal of the game) 5) White (the player of the white pieces) 6) Black (the player of the black pieces) 7) Result (the result of the game) A set of supplemental tag names is given later in this document. For PGN export format, a single blank line appears after the last of the tag pairs to conclude the tag pair section. This helps simple scanning programs to quickly determine the end of the tag pair section and the beginning of the movetext section. 8.1.1.1: The Event tag The Event tag value should be reasonably descriptive. Abbreviations are to be avoided unless absolutely necessary. A consistent event naming should be used to help facilitate database scanning. If the name of the event is unknown, a single question mark should appear as the tag value. Examples: [Event "FIDE World Championship"] [Event "Moscow City Championship"] [Event "ACM North American Computer Championship"] [Event "Casual Game"] 8.1.1.2: The Site tag The Site tag value should include city and region names along with a standard name for the country. The use of the IOC (International Olympic Committee) three letter names is suggested for those countries where such codes are available. If the site of the event is unknown, a single question mark should appear as the tag value. A comma may be used to separate a city from a region. No comma is needed to separate a city or region from the IOC country code. A later section of this document gives a list of three letter nation codes along with a few additions for "locations" not covered by the IOC. Examples: [Site "New York City, NY USA"] [Site "St. Petersburg RUS"] [Site "Riga LAT"] 8.1.1.3: The Date tag The Date tag value gives the starting date for the game. (Note: this is not necessarily the same as the starting date for the event.) The date is given with respect to the local time of the site given in the Event tag. The Date tag value field always uses a standard ten character format: "YYYY.MM.DD". The first four characters are digits that give the year, the next character is a period, the next two characters are digits that give the month, the next character is a period, and the final two characters are digits that give the day of the month. If the any of the digit fields are not known, then question marks are used in place of the digits. Examples: [Date "1992.08.31"] [Date "1993.??.??"] [Date "2001.01.01"] 8.1.1.4: The Round tag The Round tag value gives the playing round for the game. In a match competition, this value is the number of the game played. If the use of a round number is inappropriate, then the field should be a single hyphen character. If the round is unknown, a single question mark should appear as the tag value. Some organizers employ unusual round designations and have multipart playing rounds and sometimes even have conditional rounds. In these cases, a multipart round identifier can be made from a sequence of integer round numbers separated by periods. The leftmost integer represents the most significant round and succeeding integers represent round numbers in descending hierarchical order. Examples: [Round "1"] [Round "3.1"] [Round "4.1.2"] 8.1.1.5: The White tag The White tag value is the name of the player or players of the white pieces. The names are given as they would appear in a telephone directory. The family or last name appears first. If a first name or first initial is available, it is separated from the family name by a comma and a space. Finally, one or more middle initials may appear. (Wherever a comma appears, the very next character should be a space. Wherever an initial appears, the very next character should be a period.) If the name is unknown, a single question mark should appear as the tag value. The intent is to allow meaningful ASCII sorting of the tag value that is independent of regional name formation customs. If more than one person is playing the white pieces, the names are listed in alphabetical order and are separated by the colon character between adjacent entries. A player who is also a computer program should have appropriate version information listed after the name of the program. The format used in the FIDE Rating Lists is appropriate for use for player name tags. Examples: [White "Tal, Mikhail N."] [White "van der Wiel, Johan"] [White "Acme Pawngrabber v.3.2"] [White "Fine, R."] 8.1.1.6: The Black tag The Black tag value is the name of the player or players of the black pieces. The names are given here as they are for the White tag value. Examples: [Black "Lasker, Emmanuel"] [Black "Smyslov, Vasily V."] [Black "Smith, John Q.:Woodpusher 2000"] [Black "Morphy"] 8.1.1.7: The Result tag The Result field value is the result of the game. It is always exactly the same as the game termination marker that concludes the associated movetext. It is always one of four possible values: "1-0" (White wins), "0-1" (Black wins), "1/2-1/2" (drawn game), and "*" (game still in progress, game abandoned, or result otherwise unknown). Note that the digit zero is used in both of the first two cases; not the letter "O". All possible examples: [Result "0-1"] [Result "1-0"] [Result "1/2-1/2"] [Result "*"] 8.2: Movetext section The movetext section is composed of chess moves, move number indications, optional annotations, and a single concluding game termination marker. Because illegal moves are not real chess moves, they are not permitted in PGN movetext. They may appear in commentary, however. One would hope that illegal moves are relatively rare in games worthy of recording. 8.2.1: Movetext line justification In PGN import format, tokens in the movetext do not require any specific line justification. In PGN export format, tokens in the movetext are placed left justified on successive text lines each of which has less than 80 printing characters. As many tokens as possible are placed on a line with the remainder appearing on successive lines. A single space character appears between any two adjacent symbol tokens on the same line in the movetext. As with the tag pair section, a single empty line follows the last line of data to conclude the movetext section. Neither the first or the last character on an export format PGN line is a space. (This may change in the case of commentary; this area is currently under development.) 8.2.2: Movetext move number indications A move number indication is composed of one or more adjacent digits (an integer token) followed by zero or more periods. The integer portion of the indication gives the move number of the immediately following white move (if present) and also the immediately following black move (if present). 8.2.2.1: Import format move number indications PGN import format does not require move number indications. It does not prohibit superfluous move number indications anywhere in the movetext as long as the move numbers are correct. PGN import format move number indications may have zero or more period characters following the digit sequence that gives the move number; one or more white space characters may appear between the digit sequence and the period(s). 8.2.2.2: Export format move number indications There are two export format move number indication formats, one for use appearing immediately before a white move element and one for use appearing immediately before a black move element. A white move number indication is formed from the integer giving the fullmove number with a single period character appended. A black move number indication is formed from the integer giving the fullmove number with three period characters appended. All white move elements have a preceding move number indication. A black move element has a preceding move number indication only in two cases: first, if there is intervening annotation or commentary between the black move and the previous white move; and second, if there is no previous white move in the special case where a game starts from a position where Black is the active player. There are no other cases where move number indications appear in PGN export format. 8.2.3: Movetext SAN (Standard Algebraic Notation) SAN (Standard Algebraic Notation) is a representation standard for chess moves using the ASCII Latin alphabet. Examples of SAN recorded games are found throughout most modern chess publications. SAN as presented in this document uses English language single character abbreviations for chess pieces, although this is easily changed in the source. English is chosen over other languages because it appears to be the most widely recognized. An alternative to SAN is FAN (Figurine Algebraic Notation). FAN uses miniature piece icons instead of single letter piece abbreviations. The two notations are otherwise identical. 8.2.3.1: Square identification SAN identifies each of the sixty four squares on the chessboard with a unique two character name. The first character of a square identifier is the file of the square; a file is a column of eight squares designated by a single lower case letter from "a" (leftmost or queenside) up to and including "h" (rightmost or kingside). The second character of a square identifier is the rank of the square; a rank is a row of eight squares designated by a single digit from "1" (bottom side [White's first rank]) up to and including "8" (top side [Black's first rank]). The initial squares of some pieces are: white queen rook at a1, white king at e1, black queen knight pawn at b7, and black king rook at h8. 8.2.3.2: Piece identification SAN identifies each piece by a single upper case letter. The standard English values: pawn = "P", knight = "N", bishop = "B", rook = "R", queen = "Q", and king = "K". The letter code for a pawn is not used for SAN moves in PGN export format movetext. However, some PGN import software disambiguation code may allow for the appearance of pawn letter codes. Also, pawn and other piece letter codes are needed for use in some tag pair and annotation constructs. It is admittedly a bit chauvinistic to select English piece letters over those from other languages. There is a slight justification in that English is a de facto universal second language among most chessplayers and program users. It is probably the best that can be done for now. A later section of this document gives alternative piece letters, but these should be used only for local presentation software and not for archival storage or for dynamic interchange among programs. 8.2.3.3: Basic SAN move construction A basic SAN move is given by listing the moving piece letter (omitted for pawns) followed by the destination square. Capture moves are denoted by the lower case letter "x" immediately prior to the destination square; pawn captures include the file letter of the originating square of the capturing pawn immediately prior to the "x" character. SAN kingside castling is indicated by the sequence "O-O"; queenside castling is indicated by the sequence "O-O-O". Note that the upper case letter "O" is used, not the digit zero. The use of a zero character is not only incompatible with traditional text practices, but it can also confuse parsing algorithms which also have to understand about move numbers and game termination markers. Also note that the use of the letter "O" is consistent with the practice of having all chess move symbols start with a letter; also, it follows the convention that all non-pwn move symbols start with an upper case letter. En passant captures do not have any special notation; they are formed as if the captured pawn were on the capturing pawn's destination square. Pawn promotions are denoted by the equal sign "=" immediately following the destination square with a promoted piece letter (indicating one of knight, bishop, rook, or queen) immediately following the equal sign. As above, the piece letter is in upper case. 8.2.3.4: Disambiguation In the case of ambiguities (multiple pieces of the same type moving to the same square), the first appropriate disambiguating step of the three following steps is taken: First, if the moving pieces can be distinguished by their originating files, the originating file letter of the moving piece is inserted immediately after the moving piece letter. Second (when the first step fails), if the moving pieces can be distinguished by their originating ranks, the originating rank digit of the moving piece is inserted immediately after the moving piece letter. Third (when both the first and the second steps fail), the two character square coordinate of the originating square of the moving piece is inserted immediately after the moving piece letter. Note that the above disambiguation is needed only to distinguish among moves of the same piece type to the same square; it is not used to distinguish among attacks of the same piece type to the same square. An example of this would be a position with two white knights, one on square c3 and one on square g1 and a vacant square e2 with White to move. Both knights attack square e2, and if both could legally move there, then a file disambiguation is needed; the (nonchecking) knight moves would be "Nce2" and "Nge2". However, if the white king were at square e1 and a black bishop were at square b4 with a vacant square d2 (thus an absolute pin of the white knight at square c3), then only one white knight (the one at square g1) could move to square e2: "Ne2". 8.2.3.5: Check and checkmate indication characters If the move is a checking move, the plus sign "+" is appended as a suffix to the basic SAN move notation; if the move is a checkmating move, the octothorpe sign "#" is appended instead. Neither the appearance nor the absence of either a check or checkmating indicator is used for disambiguation purposes. This means that if two (or more) pieces of the same type can move to the same square the differences in checking status of the moves does not allieviate the need for the standard rank and file disabiguation described above. (Note that a difference in checking status for the above may occur only in the case of a discovered check.) Neither the checking or checkmating indicators are considered annotation as they do not communicate subjective information. Therefore, they are qualitatively different from move suffix annotations like "!" and "?". Subjective move annotations are handled using Numeric Annotation Glyphs as described in a later section of this document. There are no special markings used for double checks or discovered checks. There are no special markings used for drawing moves. 8.2.3.6: SAN move length SAN moves can be as short as two characters (e.g., "d4"), or as long as seven characters (e.g., "Qa6xb7#", "fxg1=Q+"). The average SAN move length seen in realistic games is probably just fractionally longer than three characters. If the SAN rules seem complicated, be assured that the earlier notation systems of LEN (Long English Notation) and EDN (English Descriptive Notation) are much more complex, and that LAN (Long Algebraic Notation, the predecessor of SAN) is unnecessarily bulky. 8.2.3.7: Import and export SAN PGN export format always uses the above canonical SAN to represent moves in the movetext section of a PGN game. Import format is somewhat more relaxed and it makes allowances for moves that do not conform exactly to the canonical format. However, these allowances may differ among different PGN reader programs. Only data appearing in export format is in all cases guaranteed to be importable into all PGN readers. There are a number of suggested guidelines for use with implementing PGN reader software for permitting non-canonical SAN move representation. The idea is to have a PGN reader apply various transformations to attempt to discover the move that is represented by non-canonical input. Some suggested transformations include: letter case remapping, capture indicator insertion, check indicator insertion, and checkmate indicator insertion. 8.2.3.8: SAN move suffix annotations Import format PGN allows for the use of traditional suffix annotations for moves. There are exactly six such annotations available: "!", "?", "!!", "!?", "?!", and "??". At most one such suffix annotation may appear per move, and if present, it is always the last part of the move symbol. When exported, a move suffix annotation is translated into the corresponding Numeric Annotation Glyph as described in a later section of this document. For example, if the single move symbol "Qxa8?" appears in an import format PGN movetext, it would be replaced with the two adjacent symbols "Qxa8 $2". 8.2.4: Movetext NAG (Numeric Annotation Glyph) An NAG (Numeric Annotation Glyph) is a movetext element that is used to indicate a simple annotation in a language independent manner. An NAG is formed from a dollar sign ("$") with a non-negative decimal integer suffix. The non-negative integer must be from zero to 255 in value. 8.2.5: Movetext RAV (Recursive Annotation Variation) An RAV (Recursive Annotation Variation) is a sequence of movetext containing one or more moves enclosed in parentheses. An RAV is used to represent an alternative variation. The alternate move sequence given by an RAV is one that may be legally played by first unplaying the move that appears immediately prior to the RAV. Because the RAV is a recursive construct, it may be nested. *** The specification for import/export representation of RAV elements needs further development. 8.2.6: Game Termination Markers Each movetext section has exactly one game termination marker; the marker always occurs as the last element in the movetext. The game termination marker is a symbol that is one of the following four values: "1-0" (White wins), "0-1" (Black wins), "1/2-1/2" (drawn game), and "*" (game in progress, result unknown, or game abandoned). Note that the digit zero is used in the above; not the upper case letter "O". The game termination marker appearing in the movetext of a game must match the value of the game's Result tag pair. (While the marker appears as a string in the Result tag, it appears as a symbol without quotes in the movetext.) 9: Supplemental tag names The following tag names and their associated semantics are recommended for use for information not contained in the Seven Tag Roster. 9.1: Player related information Note that if there is more than one player field in an instance of a player (White or Black) tag, then there will be corresponding multiple fields in any of the following tags. For example, if the White tag has the three field value "Jones:Smith:Zacharias" (a consultation game), then the WhiteTitle tag could have a value of "IM:-:GM" if Jones was an International Master, Smith was untitled, and Zacharias was a Grandmaster. 9.1.1: Tags: WhiteTitle, BlackTitle These use string values such as "FM", "IM", and "GM"; these tags are used only for the standard abbreviations for FIDE titles. A value of "-" is used for an untitled player. 9.1.2: Tags: WhiteElo, BlackElo These tags use integer values; these are used for FIDE Elo ratings. A value of "-" is used for an unrated player. 9.1.3: Tags: WhiteUSCF, BlackUSCF These tags use integer values; these are used for USCF (United States Chess Federation) ratings. Similar tag names can be constructed for other rating agencies. 9.1.4: Tags: WhiteNA, BlackNA These tags use string values; these are the e-mail or network addresses of the players. A value of "-" is used for a player without an electronic address. 9.1.5: Tags: WhiteType, BlackType These tags use string values; these describe the player types. The value "human" should be used for a person while the value "program" should be used for algorithmic (computer) players. 9.2: Event related information The following tags are used for providing additional information about the event. 9.2.1: Tag: EventDate This uses a date value, similar to the Date tag field, that gives the starting date of the Event. 9.2.2: Tag: EventSponsor This uses a string value giving the name of the sponsor of the event. 9.2.3: Tag: Section This uses a string; this is used for the playing section of a tournament (e.g., "Open" or "Reserve"). 9.2.4: Tag: Stage This uses a string; this is used for the stage of a multistage event (e.g., "Preliminary" or "Semifinal"). 9.2.5: Tag: Board This uses an integer; this identifies the board number in a team event and also in a simultaneous exhibition. 9.3: Opening information (locale specific) The following tag pairs are used for traditional opening names. The associated tag values will vary according to the local language in use. 9.3.1: Tag: Opening This uses a string; this is used for the traditional opening name. This will vary by locale. This tag pair is associated with the use of the EPD opcode "v0" described in a later section of this document. 9.3.2: Tag: Variation This uses a string; this is used to further refine the Opening tag. This will vary by locale. This tag pair is associated with the use of the EPD opcode "v1" described in a later section of this document. 9.3.3: Tag: SubVariation This uses a string; this is used to further refine the Variation tag. This will vary by locale. This tag pair is associated with the use of the EPD opcode "v2" described in a later section of this document. 9.4: Opening information (third party vendors) The following tag pairs are used for representing opening identification according to various third party vendors and organizations. References to these organizations does not imply any endorsement of them or any endorsement by them. 9.4.1: Tag: ECO This uses a string of either the form "XDD" or the form "XDD/DD" where the "X" is a letter from "A" to "E" and the "D" positions are digits; this is used for an opening designation from the five volume _Encyclopedia of Chess Openings_. This tag pair is associated with the use of the EPD opcode "eco" described in a later section of this document. 9.4.2: Tag: NIC This uses a string; this is used for an opening designation from the _New in Chess_ database. This tag pair is associated with the use of the EPD opcode "nic" described in a later section of this document. 9.5: Time and date related information The following tags assist with further refinement of the time and data information associated with a game. 9.5.1: Tag: Time This uses a time-of-day value in the form "HH:MM:SS"; similar to the Date tag except that it denotes the local clock time (hours, minutes, and seconds) of the start of the game. Note that colons, not periods, are used for field separators for the Time tag value. The value is taken from the local time corresponding to the location given in the Site tag pair. 9.5.2: Tag: UTCTime This tag is similar to the Time tag except that the time is given according to the Universal Coordinated Time standard. 9.5.3: Tag:; UTCDate This tag is similar to the Date tag except that the date is given according to the Universal Coordinated Time standard. 9.6: Time control The follwing tag is used to help describe the time control used with the game. 9.6.1: Tag: TimeControl This uses a list of one or more time control fields. Each field contains a descriptor for each time control period; if more than one descriptor is present then they are separated by the colon character (":"). The descriptors appear in the order in which they are used in the game. The last field appearing is considered to be implicitly repeated for further control periods as needed. There are six kinds of TimeControl fields. The first kind is a single question mark ("?") which means that the time control mode is unknown. When used, it is usually the only descriptor present. The second kind is a single hyphen ("-") which means that there was no time control mode in use. When used, it is usually the only descriptor present. The third Time control field kind is formed as two positive integers separated by a solidus ("/") character. The first integer is the number of moves in the period and the second is the number of seconds in the period. Thus, a time control period of 40 moves in 2 1/2 hours would be represented as "40/9000". The fourth TimeControl field kind is used for a "sudden death" control period. It should only be used for the last descriptor in a TimeControl tag value. It is sometimes the only descriptor present. The format consists of a single integer that gives the number of seconds in the period. Thus, a blitz game would be represented with a TimeControl tag value of "300". The fifth TimeControl field kind is used for an "incremental" control period. It should only be used for the last descriptor in a TimeControl tag value and is usually the only descriptor in the value. The format consists of two positive integers separated by a plus sign ("+") character. The first integer gives the minimum number of seconds allocated for the period and the second integer gives the number of extra seconds added after each move is made. So, an incremental time control of 90 minutes plus one extra minute per move would be given by "4500+60" in the TimeControl tag value. The sixth TimeControl field kind is used for a "sandclock" or "hourglass" control period. It should only be used for the last descriptor in a TimeControl tag value and is usually the only descriptor in the value. The format consists of an asterisk ("*") immediately followed by a positive integer. The integer gives the total number of seconds in the sandclock period. The time control is implemented as if a sandclock were set at the start of the period with an equal amount of sand in each of the two chambers and the players invert the sandclock after each move with a time forfeit indicated by an empty upper chamber. Electronic implementation of a physical sandclock may be used. An example sandclock specification for a common three minute egg timer sandclock would have a tag value of "*180". Additional TimeControl field kinds will be defined as necessary. 9.7: Alternative starting positions There are two tags defined for assistance with describing games that did not start from the usual initial array. 9.7.1: Tag: SetUp This tag takes an integer that denotes the "set-up" status of the game. A value of "0" indicates that the game has started from the usual initial array. A value of "1" indicates that the game started from a set-up position; this position is given in the "FEN" tag pair. This tag must appear for a game starting with a set-up position. If it appears with a tag value of "1", a FEN tag pair must also appear. 9.7.2: Tag: FEN This tag uses a string that gives the Forsyth-Edwards Notation for the starting position used in the game. FEN is described in a later section of this document. If a SetUp tag appears with a tag value of "1", the FEN tag pair is also required. 9.8: Game conclusion There is a single tag that discusses the conclusion of the game. 9.8.1: Tag: Termination This takes a string that describes the reason for the conclusion of the game. While the Result tag gives the result of the game, it does not provide any extra information and so the Termination tag is defined for this purpose. Strings that may appear as Termination tag values: * "abandoned": abandoned game. * "adjudication": result due to third party adjudication process. * "death": losing player called to greater things, one hopes. * "emergency": game concluded due to unforeseen circumstances. * "normal": game terminated in a normal fashion. * "rules infraction": administrative forfeit due to losing player's failure to observe either the Laws of Chess or the event regulations. * "time forfeit": loss due to losing player's failure to meet time control requirements. * "unterminated": game not terminated. 9.9: Miscellaneous These are tags that can be briefly described and that doon't fit well inother sections. 9.9.1: Tag: Annotator This tag uses a name or names in the format of the player name tags; this identifies the annotator or annotators of the game. 9.9.2: Tag: Mode This uses a string that gives the playing mode of the game. Examples: "OTB" (over the board), "PM" (paper mail), "EM" (electronic mail), "ICS" (Internet Chess Server), and "TC" (general telecommunication). 9.9.3: Tag: PlyCount This tag takes a single integer that gives the number of ply (moves) in the game. 10: Numeric Annotation Glyphs NAG zero is used for a null annotation; it is provided for the convenience of software designers as a placeholder value and should probably not be used in external PGN data. NAGs with values from 1 to 9 annotate the move just played. NAGs with values from 10 to 135 modify the current position. NAGs with values from 136 to 139 describe time pressure. Other NAG values are reserved for future definition. Note: the number assignments listed below should be considered preliminary in nature; they are likely to be changed as a result of reviewer feedback. NAG Interpretation --- -------------- 0 null annotation 1 good move (traditional "!") 2 poor move (traditional "?") 3 very good move (traditional "!!") 4 very poor move (traditional "??") 5 speculative move (traditional "!?") 6 questionable move (traditional "?!") 7 forced move (all others lose quickly) 8 singular move (no reasonable alternatives) 9 worst move 10 drawish position 11 equal chances, quiet position 12 equal chances, active position 13 unclear position 14 White has a slight advantage 15 Black has a slight advantage 16 White has a moderate advantage 17 Black has a moderate advantage 18 White has a decisive advantage 19 Black has a decisive advantage 20 White has a crushing advantage (Black should resign) 21 Black has a crushing advantage (White should resign) 22 White is in zugzwang 23 Black is in zugzwang 24 White has a slight space advantage 25 Black has a slight space advantage 26 White has a moderate space advantage 27 Black has a moderate space advantage 28 White has a decisive space advantage 29 Black has a decisive space advantage 30 White has a slight time (development) advantage 31 Black has a slight time (development) advantage 32 White has a moderate time (development) advantage 33 Black has a moderate time (development) advantage 34 White has a decisive time (development) advantage 35 Black has a decisive time (development) advantage 36 White has the initiative 37 Black has the initiative 38 White has a lasting initiative 39 Black has a lasting initiative 40 White has the attack 41 Black has the attack 42 White has insufficient compensation for material deficit 43 Black has insufficient compensation for material deficit 44 White has sufficient compensation for material deficit 45 Black has sufficient compensation for material deficit 46 White has more than adequate compensation for material deficit 47 Black has more than adequate compensation for material deficit 48 White has a slight center control advantage 49 Black has a slight center control advantage 50 White has a moderate center control advantage 51 Black has a moderate center control advantage 52 White has a decisive center control advantage 53 Black has a decisive center control advantage 54 White has a slight kingside control advantage 55 Black has a slight kingside control advantage 56 White has a moderate kingside control advantage 57 Black has a moderate kingside control advantage 58 White has a decisive kingside control advantage 59 Black has a decisive kingside control advantage 60 White has a slight queenside control advantage 61 Black has a slight queenside control advantage 62 White has a moderate queenside control advantage 63 Black has a moderate queenside control advantage 64 White has a decisive queenside control advantage 65 Black has a decisive queenside control advantage 66 White has a vulnerable first rank 67 Black has a vulnerable first rank 68 White has a well protected first rank 69 Black has a well protected first rank 70 White has a poorly protected king 71 Black has a poorly protected king 72 White has a well protected king 73 Black has a well protected king 74 White has a poorly placed king 75 Black has a poorly placed king 76 White has a well placed king 77 Black has a well placed king 78 White has a very weak pawn structure 79 Black has a very weak pawn structure 80 White has a moderately weak pawn structure 81 Black has a moderately weak pawn structure 82 White has a moderately strong pawn structure 83 Black has a moderately strong pawn structure 84 White has a very strong pawn structure 85 Black has a very strong pawn structure 86 White has poor knight placement 87 Black has poor knight placement 88 White has good knight placement 89 Black has good knight placement 90 White has poor bishop placement 91 Black has poor bishop placement 92 White has good bishop placement 93 Black has good bishop placement 84 White has poor rook placement 85 Black has poor rook placement 86 White has good rook placement 87 Black has good rook placement 98 White has poor queen placement 99 Black has poor queen placement 100 White has good queen placement 101 Black has good queen placement 102 White has poor piece coordination 103 Black has poor piece coordination 104 White has good piece coordination 105 Black has good piece coordination 106 White has played the opening very poorly 107 Black has played the opening very poorly 108 White has played the opening poorly 109 Black has played the opening poorly 110 White has played the opening well 111 Black has played the opening well 112 White has played the opening very well 113 Black has played the opening very well 114 White has played the middlegame very poorly 115 Black has played the middlegame very poorly 116 White has played the middlegame poorly 117 Black has played the middlegame poorly 118 White has played the middlegame well 119 Black has played the middlegame well 120 White has played the middlegame very well 121 Black has played the middlegame very well 122 White has played the ending very poorly 123 Black has played the ending very poorly 124 White has played the ending poorly 125 Black has played the ending poorly 126 White has played the ending well 127 Black has played the ending well 128 White has played the ending very well 129 Black has played the ending very well 130 White has slight counterplay 131 Black has slight counterplay 132 White has moderate counterplay 133 Black has moderate counterplay 134 White has decisive counterplay 135 Black has decisive counterplay 136 White has moderate time control pressure 137 Black has moderate time control pressure 138 White has severe time control pressure 139 Black has severe time control pressure 11: File names and directories File names chosen for PGN data should be both informative and portable. The directory names and arrangements should also be chosen for the same reasons and also for ease of navigation. Some of suggested file and directory names may be difficult or impossible to represent on certain computing systems. Use of appropriate conversion customs is encouraged. 11.1: File name suffix for PGN data The use of the file suffix ".pgn" is encouraged for ASCII text files containing PGN data. 11.2: File name formation for PGN data for a specific player PGN games for a specific player should have a file name consisting of the player's last name followed by the ".pgn" suffix. 11.3: File name formation for PGN data for a specific event PGN games for a specific event should have a file name consisting of the event's name followed by the ".pgn" suffix. 11.4: File name formation for PGN data for chronologically ordered games PGN data files used for chronologically ordered (oldest first) archives use date information as file name root strings. A file containing all the PGN games for a given year would have an eight character name in the format "YYYY.pgn". A file containing PGN data for a given month would have a ten character name in the format "YYYYMM.pgn". Finally, a file for PGN games for a single day would have a twelve character name in the format "YYYYMMDD.pgn". Large files are split into smaller files as needed. As game files are commonly arranged by chronological order, games with missing or incomplete Date tag pair data are to be avoided. Any question mark characters in a Date tag value will be treated as zero digits for collation within a file and also for file naming. Large quantities of PGN data arranged by chronological order should be organized into hierarchical directories. A directory containing all PGN data for a given year would have a four character name in the format "YYYY"; directories containing PGN files for a given month would have a six character name in the format "YYYYMM". 11.5: Suggested directory tree organization A suggested directory arrangement for ftp sites and CD-ROM distributions: * PGN: master directory of the PGN subtree (pub/chess/Game-Databases/PGN) * PGN/Events: directory of PGN files, each for a specific event * PGN/Events/News: news and status of the event collection * PGN/Events/ReadMe: brief description of the local directory contents * PGN/MGR: directory of the Master Games Repository subtree * PGN/MGR/News: news and status of the entire PGN/MGR subtree * PGN/MGR/ReadMe: brief description of the local directory contents * PGN/MGR/YYYY: directory of games or subtrees for the year YYYY * PGN/MGR/YYYY/ReadMe: description of local directory for year YYYY * PGN/MGR/YYYY/News: news and status for year YYYY data * PGN/News: news and status of the entire PGN subtree * PGN/Players: directory of PGN files, each for a specific player * PGN/Players/News: news and status of the player collection * PGN/Players/ReadMe: brief description of the local directory contents * PGN/ReadMe: brief description of the local directory contents * PGN/Standard: the PGN standard (this document) * PGN/Tools: software utilities that access PGN data 12: PGN collating sequence There is a standard sorting order for PGN games within a file. This collation is based on eight keys; these are the seven tag values of the STR and also the movetext itself. The first (most important, primary key) is the Date tag. Earlier dated games appear prior to games played at a later date. This field is sorted by ascending numeric value first with the year, then the month, and finally the day of the month. Query characters used for unknown date digit values will be treated as zero digit characters for ordering comparison. The second key is the Event tag. This is sorted in ascending ASCII order. The third key is the Site tag. This is sorted in ascending ASCII order. The fourth key is the Round tag. This is sorted in ascending numeric order based on the value of the integer used to denote the playing round. A query or hyphen used for the round is ordered before any integer value. A query character is ordered before a hyphen character. The fifth key is the White tag. This is sorted in ascending ASCII order. The sixth key is the Black tag. This is sorted in ascending ASCII order. The seventh key is the Result tag. This is sorted in ascending ASCII order. The eighth key is the movetext itself. This is sorted in ascending ASCII order with the entire text including spaces and newline characters. 13: PGN software This section describes some PGN software that is either currently available or expected to be available in the near future. The entries are presented in rough chronological order of their being made known to the PGN standard coordinator. Authors of PGN capable software are encouraged to contact the coordinator (e-mail address listed near the start of this document) so that the information may be included here in this section. In addition to the PGN standard, there are two more chess standards of interest to the chess software community. These are the FEN standard (Forsyth-Edwards Notation) for position notation and the EPD standard (Extended Position Description) for comprehensive position description for automated interprogram processing. These are described in a later section of this document. Some PGN software is freeware and can be gotten from ftp sites and other sources. Other PGN software is payware and appears as part of commercial chessplaying programs and chess database managers. Those who are interested in the propagation of the PGN standard are encouraged to support manufacturers of chess software that use the standard. If a particular vendor does not offer PGN compatibility, it is likely that a few letters to them along with a copy of this specification may help them decide to include PGN support in their next release. The staff at the University of Oklahoma at Norman (USA) have graciously provided an ftp site (chess.uoknor.edu) for the storage of chess related data and programs. Because file names change over time, those accessing the site are encouraged to first retrieve the file "pub/chess/ls-lR.gz" for a current listing. A scan of this listing will also help locate versions of PGN programs for machine types and operating systems other than those listed below. Further information about this archive can be gotten from its administrator, Chris Petroff (chris@uoknor.edu). For European users, the kind staff at the University of Hamburg (Germany) have provided the ftp site ftp.math.uni-hamburg.de; this carries a daily mirror of the pub/chess directory at the chess.uoknor.edu site. 13.1: The SAN Kit The "SAN Kit" is an ANSI C source chess programming toolkit available for free from the ftp site chess.uoknor.edu in the directory pub/chess/Unix as the file "SAN.tar.gz" (a gzip tar archive). This kit contains code for PGN import and export and can be used to "regularize" PGN data into reduced export format by use of its "tfgg" command. The SAN Kit also supports FEN I/O. Code from this kit is freely redistributable for anyone as long as future distribution is unhindered for everyone. The SAN Kit is undergoing continuous development, although dates of future deliveries are quite difficult to predict and releases sometimes appear months apart. Suggestions and comments should be directed to its author, Steven J. Edwards (sje@world.std.com). 13.2: pgnRead The program "pgnRead" runs under MS Windows 3.1 and provides an interactive graphical user interface for scanning PGN data files. This program includes a colorful figurine chessboard display and scrolling controls for game and game text selection. It is available from the chess.uoknor.edu ftp site in the pub/chess/DOS directory; several versions are available with names of the form "pgnrd**.exe"; the latest at this writing is "PGNRD130.EXE". Suggestions and comments should be directed to its author, Keith Fuller (keithfx@aol.com). 13.3: mail2pgn/GIICS The program "mail2pgn" produces a PGN version of chess game data generated by the ICS (Internet Chess Server). It can be found at the chess.uoknor.edu ftp site in the pub/chess/DOS directory as the file "mail2pgn.zip" A C language version is in the directory pub/chess/Unix as the file "mail2pgn.c". Suggestions and comments should be directed to its author, John Aronson (aronson@helios.ece.arizona.edu). This code has been reportedly incorporated into the GIICS (Graphical Interface for the ICS); suggestions and comments should be directed to its author, Tony Acero (ace3@midway.uchicago.edu). There is a report that mail2pgn has been superseded by the newer program "MV2PGN" described below. 13.4: XBoard "XBoard" is a comprehensive chess utility running under the X Window System that provides a graphical user interface in a portable manner. A new version now handles PGN data. It is available from the chess.uoknor.edu ftp site in the pub/chess/X directory as the file "xboard-3.0.pl9.tar.gz". Suggestions and comments should be directed to its author, Tim Mann (mann@src.dec.com). 13.5: cupgn The program "cupgn" converts game data stored in the ChessBase format into PGN. It is available from the chess.uoknor.edu ftp site in the pub/chess/Game-Databases/CBUFF directory as the file "cupgn.tar.gz". Another version is in the directory pub/chess/DOS as the file "cupgn120.exe". Suggestions and comments should be directed to its author, Anjo Anjewierden (anjo@swi.psy.uva.nl). 13.6: Zarkov The current version (3.0) of the commercial chessplaying program "Zarkov" can read and write games using PGN. This program can also use the EPD standard for communication with other EPD capable programs. Historically, Zarkov is the very first program to use EPD. Suggestions and comments should be directed to its author, John Stanback (jhs@icbdfcs1.fc.hp.com). A vendor for North America is: International Chess Enterprises P.O. Box 19457 Seattle, WA 98109 USA (800) 262-4277 A vendor for Europe is: Gambit-Soft Feckenhauser Strasse 27 D-78628 Rottweil GERMANY 49-741-21573 13.7: Chess Assistant The upcoming version of the multifunction commercial database program "Chess Assistant" will be able to use the PGN standard as an import and export option. There is a report of a freeware program, "PGN2CA", that will convert PGN databases into Chess Assistant format. For more information, the contact is Victor Zakharov, one of the members of the Chess Assistant development team (VICTOR@ldis.cs.msu.su). A vendor for North America is: International Chess Enterprises P.O. Box 19457 Seattle, WA 98109 USA (800) 262-4277 13.8: BOOKUP The MS-DOS edition of the multifunction commercial program BOOKUP, version 8.1, is able to use the EPD standard for communication with other EPD capable programs. It may also be PGN capable as well. The BOOKUP 8.1.1 Addenda notes dated 1993.12.17 provide comprehensive information on how to use EPD in conjunction with "analyst" programs such as Zarkov and HIARCS. Specifically, the search and evaluation abilities of an analyst program are combined with the information organization abilities of the BOOKUP database program to provide position scoring. This is done by first having BOOKUP export a database in EPD format, then having an analyst program annotate each EPD record with a numeric score, and then having BOOKUP import the changed EPD file. BOOKUP can then apply minimaxing to the imported database; this results in scores from terminal positions being propagated back to earlier positions and even back to moves from the starting array. For some reason, BOOKUP calls this process "backsolving", but it's really just standard minimaxing. In any case, it's a good example of how different programs from different authors performing different types of tasks can be integrated by use of a common, non-proprietary standard. This allows for a new set of powerful features that are beyond the capabilities of any one of the individual component programs. BOOKUP allows for some customizing of EPD actions. One such customization is to require the positional evaluations to follow the EPD standard; this means that the score is always given from the viewpoint of the active player. This is explained more fully in the section on the "ce" (centipawn evaluation) opcode in the EPD description in a later section of this document. To ensure that BOOKUP handles the centipawn evaluations in the "right" way, the EPD setting "Positive for White" must be set to "N". This makes BOOKUP work correctly with Zarkov and with all other programs that use the "right" centipawn evaluation convention. There is an apparent problem with HIARCS that requires this option to be set to "Y"; but this really means that, if true, HIARCS needs to be adjusted to use the "right" centipawn evaluation convention. A vendor in North America is: BOOKUP 2763 Kensington Place West Columbus, OH 43202 USA (800) 949-5445 (614) 263-7219 13.9: HIARCS The current version (2.1) of the commercial chessplaying program "HIARCS" is able to use the EPD standard for communication with other EPD capable programs. It may also be PGN capable as well. More details will appear here as they become available. A vendor in North America is: HIARCS c/o BOOKUP 2763 Kensington Place West Columbus, OH 43202 USA (800) 949-5445 (614) 263-7219 13.10: Deja Vu The chess database "Deja Vu" from ChessWorks is a PGN compatible collection of over 300,000 games. It is available only on CD-ROM and is scheduled for release in 1994.05 with periodic revisions thereafter. The introductory price is US$329. For further information, the authors are John Crayton and Eric Schiller and they can be contacted via e-mail (chesswks@netcom.com). 13.11: MV2PGN The program "MV2PGN" can be used to convert game data generated by both current and older versions of the GIICS (Graphical Interface - Internet Chess Server). The program is included in the self extracting archive available from chess.uoknor.edu in the directory pub/chess/DOS as the file "ics2pgn.exe". Source code is also included. This program is reported to supersede the older "mail2pgn" and was needed due to a change in ICS recording format in late 1993. For further information about MV2PGN, the contact person is Gary Bastin (gbastin@x102a.ess.harris.com). 13.12: The Hansen utilities (cb2pgn, nic2pgn, pgn2cb, pgn2nic) The Hansen utilities are used to convert among various chess data representation formats. The PGN related programs include: "cb2pgn.exe" (convert ChessBase to PGN), "nic2pgn.exe" (convert NIC to PGN), "pgn2cb.exe" (convert PGN to ChessBase), and "pgn2nic.exe" (convert PGN to NIC). The ChessBase related utilities (cb2pgn/pgn2cb) are found at chess.uoknor.edu in the pub/chess/Game-Databases/ChessBase directory. The NIC related utilities (nic2pgn/pgn2nic) are found at chess.uoknor.edu in the pub/chess/Game-Databases/NIC directory. For further information about the Hansen utilities, the contact person is the author, Carsten Hansen (ch0506@hdc.hha.dk). 13.13: Slappy the Database "Slappy the Database" is a commercial chess database and translation program scheduled for release no sooner than late 1994. It is a low cost utility with a simple character interface intended for those who want a supported product but who do not need (or cannot afford) a comprehensive, feature-laden program with a graphical user interface. Slappy's two most important features are its batch processing ability and its full implementation of each and every standard described in this document. Versions of Slappy the Database will be provided for various platforms inclu ding: Intel 386/486 Unix, Apple Macintosh, and MS-DOS. Slappy may also be useful to those who have a full feature program who also need to run time consuming chess database tasks on a spare computer. Suggestions and comments should be directed to its author, Steven J. Edwards (sje@world.std.com). More details will appear here as they become available. 13.14: CBASCII "CBASCII" is a general utility for converting chess data between ChessBase format and ASCII representations. It has PGN capability, and it is available from the chess.uoknor.edu ftp site in the pub/chess/DOS directory as the file "cba1_2.zip". The contact person is the program's author, Andy Duplain (duplain@btcs.bt.co.uk). 13.15: ZZZZZZ "ZZZZZZ" is a chessplaying program, complete with source, that also includes some database functions. A recent version is reported to have both PGN and EPD capabilities. It is available from the chess.uoknor.edu ftp site in the pub/chess/Unix directory as the file "zzzzzz-3.2b1.tar.gz". The contact person is its author, Gijsbert Wiesenecker (wiesenecker@sara.nl). 13.16: icsconv The program "icsconv" can be used to convert Internet Chess Server games, both old and new format, to PGN. It is available from the chess.uoknor.edu site in the pub/chess/Game-Databases/PGN/Tools directory as the file "icsconv.exe". The contact person is the author, Kevin Nomura (chow@netcom.com). 13.17: CHESSOP (CHESSOPN/CHESSOPG) CHESSOP is an openings database and viewing tool with support for reading PGN games. It runs under MS-DOS and displays positions rather than games. For each position, both good and bad moves are listed with appropriate annotation. Transpositions are handled as well. The distributed database contains over 100,000 positions covering all the common openings. Users can feed in their own PGN data as well. CHESSOP takes 3 Mbyte of hard disk, costs US$39 and can be obtained from: CHESSX Software 12 Bluebell Close Glenmore Park AUSTRALIA 2745. The ideas behind CHESSOP can be seen in CHESSOPN (alias CHESSOPG), a free version on the ICS server which has a reduced openings database (25,000 positions) and no PGN or transposition support but is otherwise the same as CHESSOP. (These are the files "chessopg.zip" in the directory pub/chess/DOS at the chess.uoknor.edu ftp site.) 13.18: CAT2PGN The program "CAT2PGN" is a utility that translates data from the format used by Chess Assistant into PGN. It is available from the chess.uoknor.edu ftp site. The contact person for CAT2PGN is its author, David Myers (myers@frodo.biochem.duke.edu). 13.19: pgn2opg The utility "pgn2opg" can be used to convert PGN files into a text format used by the "CHESSOPG" program mentioned above. Although it does not perform any semantic analysis on PGN input, it has been demonstrated to handle known correct PGN input properly. The file can be found in the pub/chess/PGN/Tools directory at the chess.uoknor.edu ftp site. For more information, the author is David Barnes (djb@ukc.ac.uk). 14: PGN data archives The primary PGN data archive repository is located at the ftp site chess.uoknor.edu as the directory "pub/chess/Game-Databases/PGN". It is organized according to the description given in section C.5 of this document. The European site ftp.math.uni-hamburg.de is also reported to carry a regularly updated copy of the repository. 15: International Olympic Committee country codes International Olympic Committee country codes are employed for Site nation information because of their traditional use with the reporting of international sporting events. Due to changes in geography and linguistic custom, some of the following may be incorrect or outdated. Corrections and extensions should be sent via e-mail to the PGN coordinator whose address listed near the start of this document. AFG: Afghanistan AIR: Aboard aircraft ALB: Albania ALG: Algeria AND: Andorra ANG: Angola ANT: Antigua ARG: Argentina ARM: Armenia ATA: Antarctica AUS: Australia AZB: Azerbaijan BAN: Bangladesh BAR: Bahrain BHM: Bahamas BEL: Belgium BER: Bermuda BIH: Bosnia and Herzegovina BLA: Belarus BLG: Bulgaria BLZ: Belize BOL: Bolivia BRB: Barbados BRS: Brazil BRU: Brunei BSW: Botswana CAN: Canada CHI: Chile COL: Columbia CRA: Costa Rica CRO: Croatia CSR: Czechoslovakia CUB: Cuba CYP: Cyprus DEN: Denmark DOM: Dominican Republic ECU: Ecuador EGY: Egypt ENG: England ESP: Spain EST: Estonia FAI: Faroe Islands FIJ: Fiji FIN: Finland FRA: France GAM: Gambia GCI: Guernsey-Jersey GEO: Georgia GER: Germany GHA: Ghana GRC: Greece GUA: Guatemala GUY: Guyana HAI: Haiti HKG: Hong Kong HON: Honduras HUN: Hungary IND: India IRL: Ireland IRN: Iran IRQ: Iraq ISD: Iceland ISR: Israel ITA: Italy IVO: Ivory Coast JAM: Jamaica JAP: Japan JRD: Jordan JUG: Yugoslavia KAZ: Kazakhstan KEN: Kenya KIR: Kyrgyzstan KUW: Kuwait LAT: Latvia LEB: Lebanon LIB: Libya LIC: Liechtenstein LTU: Lithuania LUX: Luxembourg MAL: Malaysia MAU: Mauritania MEX: Mexico MLI: Mali MLT: Malta MNC: Monaco MOL: Moldova MON: Mongolia MOZ: Mozambique MRC: Morocco MRT: Mauritius MYN: Myanmar NCG: Nicaragua NET: The Internet NIG: Nigeria NLA: Netherlands Antilles NLD: Netherlands NOR: Norway NZD: New Zealand OST: Austria PAK: Pakistan PAL: Palestine PAN: Panama PAR: Paraguay PER: Peru PHI: Philippines PNG: Papua New Guinea POL: Poland POR: Portugal PRC: People's Republic of China PRO: Puerto Rico QTR: Qatar RIN: Indonesia ROM: Romania RUS: Russia SAF: South Africa SAL: El Salvador SCO: Scotland SEA: At Sea SEN: Senegal SEY: Seychelles SIP: Singapore SLV: Slovenia SMA: San Marino SPC: Aboard spacecraft SRI: Sri Lanka SUD: Sudan SUR: Surinam SVE: Sweden SWZ: Switzerland SYR: Syria TAI: Thailand TMT: Turkmenistan TRK: Turkey TTO: Trinidad and Tobago TUN: Tunisia UAE: United Arab Emirates UGA: Uganda UKR: Ukraine UNK: Unknown URU: Uruguay USA: United States of America UZB: Uzbekistan VEN: Venezuela VGB: British Virgin Islands VIE: Vietnam VUS: U.S. Virgin Islands WLS: Wales YEM: Yemen YUG: Yugoslavia ZAM: Zambia ZIM: Zimbabwe ZRE: Zaire 16: Additional chess data standards While PGN is used for game storage, there are other data representation standards for other chess related purposes. Two important standards are FEN and EPD, both described in this section. 16.1: FEN FEN is "Forsyth-Edwards Notation"; it is a standard for describing chess positions using the ASCII character set. A single FEN record uses one text line of variable length composed of six data fields. The first four fields of the FEN specification are the same as the first four fields of the EPD specification. A text file composed exclusively of FEN data records should have a file name with the suffix ".fen". 16.1.1: History FEN is based on a 19th century standard for position recording designed by the Scotsman David Forsyth, a newspaper journalist. The original Forsyth standard has been slightly extended for use with chess software by Steven Edwards with assistance from commentators on the Internet. This new standard, FEN, was first implemented in Edwards' SAN Kit. 16.1.2: Uses for a position notation Having a standard position notation is particularly important for chess programmers as it allows them to share position databases. For example, there exist standard position notation databases with many of the classical benchmark tests for chessplaying programs, and by using a common position notation format many hours of tedious data entry can be saved. Additionally, a position notation can be useful for page layout programs and for confirming position status for e-mail competition. Many interesting chess problem sets represented using FEN can be found at the chess.uoknor.edu ftp site in the directory pub/chess/SAN_testsuites. 16.1.3: Data fields FEN specifies the piece placement, the active color, the castling availability, the en passant target square, the halfmove clock, and the fullmove number. These can all fit on a single text line in an easily read format. The length of a FEN position description varies somewhat according to the position. In some cases, the description could be eighty or more characters in length and so may not fit conveniently on some displays. However, these positions aren't too common. A FEN description has six fields. Each field is composed only of non-blank printing ASCII characters. Adjacent fields are separated by a single ASCII space character. 16.1.3.1: Piece placement data The first field represents the placement of the pieces on the board. The board contents are specified starting with the eighth rank and ending with the first rank. For each rank, the squares are specified from file a to file h. White pieces are identified by uppercase SAN piece letters ("PNBRQK") and black pieces are identified by lowercase SAN piece letters ("pnbrqk"). Empty squares are represented by the digits one through eight; the digit used represents the count of contiguous empty squares along a rank. A solidus character "/" is used to separate data of adjacent ranks. 16.1.3.2: Active color The second field represents the active color. A lower case "w" is used if White is to move; a lower case "b" is used if Black is the active player. 16.1.3.3: Castling availability The third field represents castling availability. This indicates potential future castling that may of may not be possible at the moment due to blocking pieces or enemy attacks. If there is no castling availability for either side, the single character symbol "-" is used. Otherwise, a combination of from one to four characters are present. If White has kingside castling availability, the uppercase letter "K" appears. If White has queenside castling availability, the uppercase letter "Q" appears. If Black has kingside castling availability, the lowercase letter "k" appears. If Black has queenside castling availability, then the lowercase letter "q" appears. Those letters which appear will be ordered first uppercase before lowercase and second kingside before queenside. There is no white space between the letters. 16.1.3.4: En passant target square The fourth field is the en passant target square. If there is no en passant target square then the single character symbol "-" appears. If there is an en passant target square then is represented by a lowercase file character immediately followed by a rank digit. Obviously, the rank digit will be "3" following a white pawn double advance (Black is the active color) or else be the digit "6" after a black pawn double advance (White being the active color). An en passant target square is given if and only if the last move was a pawn advance of two squares. Therefore, an en passant target square field may have a square name even if there is no pawn of the opposing side that may immediately execute the en passant capture. 16.1.3.5: Halfmove clock The fifth field is a nonnegative integer representing the halfmove clock. This number is the count of halfmoves (or ply) since the last pawn advance or capturing move. This value is used for the fifty move draw rule. 16.1.3.6: Fullmove number The sixth and last field is a positive integer that gives the fullmove number. This will have the value "1" for the first move of a game for both White and Black. It is incremented by one immediately after each move by Black. 16.1.4: Examples Here's the FEN for the starting position: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 And after the move 1. e4: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1 And then after 1. ... c5: rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2 And then after 2. Nf3: rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 For two kings on their home squares and a white pawn on e2 (White to move) with thirty eight full moves played with five halfmoves since the last pawn move or capture: 4k3/8/8/8/8/8/4P3/4K3 w - - 5 39 16.2: EPD EPD is "Extended Position Description"; it is a standard for describing chess positions along with an extended set of structured attribute values using the ASCII character set. It is intended for data and command interchange among chessplaying programs. It is also intended for the representation of portable opening library repositories. A single EPD uses one text line of variable length composed of four data field followed by zero or more operations. The four fields of the EPD specification are the same as the first four fields of the FEN specification. A text file composed exclusively of EPD data records should have a file name with the suffix ".epd". 16.2.1: History EPD is based in part on the earlier FEN standard; it has added extensions for use with opening library preparation and also for general data and command interchange among advanced chess programs. EPD was developed by John Stanback and Steven Edwards; its first implementation is in Stanback's master strength chessplaying program Zarkov. 16.2.2: Uses for an extended position notation Like FEN, EPD can also be used for general position description. However, unlike FEN, EPD is designed to be expandable by the addition of new operations that provide new functionality as needs arise. Many interesting chess problem sets represented using EPD can be found at the chess.uoknor.edu ftp site in the directory pub/chess/SAN_testsuites. 16.2.3: Data fields EPD specifies the piece placement, the active color, the castling availability, and the en passant target square of a position. These can all fit on a single text line in an easily read format. The length of an EPD position description varies somewhat according to the position and any associated operations. In some cases, the description could be eighty or more characters in length and so may not fit conveniently on some displays. However, most EPD descriptions pass among programs only and these are not usually seen by program users. (Note: due to the likelihood of future expansion of EPD, implementors are encouraged to have their programs handle EPD text lines of up to 1024 characters long.) Each EPD data field is composed only of non-blank printing ASCII characters. Adjacent data fields are separated by a single ASCII space character. 16.2.3.1: Piece placement data The first field represents the placement of the pieces on the board. The board contents are specified starting with the eighth rank and ending with the first rank. For each rank, the squares are specified from file a to file h. White pieces are identified by uppercase SAN piece letters ("PNBRQK") and black pieces are identified by lowercase SAN piece letters ("pnbrqk"). Empty squares are represented by the digits one through eight; the digit used represents the count of contiguous empty squares along a rank. A solidus character "/" is used to separate data of adjacent ranks. 16.2.3.2: Active color The second field represents the active color. A lower case "w" is used if White is to move; a lower case "b" is used if Black is the active player. 16.2.3.3: Castling availability The third field represents castling availability. This indicates potential future castling that may or may not be possible at the moment due to blocking pieces or enemy attacks. If there is no castling availability for either side, the single character symbol "-" is used. Otherwise, a combination of from one to four characters are present. If White has kingside castling availability, the uppercase letter "K" appears. If White has queenside castling availability, the uppercase letter "Q" appears. If Black has kingside castling availability, the lowercase letter "k" appears. If Black has queenside castling availability, then the lowercase letter "q" appears. Those letters which appear will be ordered first uppercase before lowercase and second kingside before queenside. There is no white space between the letters. 16.2.3.4: En passant target square The fourth field is the en passant target square. If there is no en passant target square then the single character symbol "-" appears. If there is an en passant target square then is represented by a lowercase file character immediately followed by a rank digit. Obviously, the rank digit will be "3" following a white pawn double advance (Black is the active color) or else be the digit "6" after a black pawn double advance (White being the active color). An en passant target square is given if and only if the last move was a pawn advance of two squares. Therefore, an en passant target square field may have a square name even if there is no pawn of the opposing side that may immediately execute the en passant capture. 16.2.4: Operations An EPD operation is composed of an opcode followed by zero or more operands and is concluded by a semicolon. Multiple operations are separated by a single space character. If there is at least one operation present in an EPD line, it is separated from the last (fourth) data field by a single space character. 16.2.4.1: General format An opcode is an identifier that starts with a letter character and may be followed by up to fourteen more characters. Each additional character may be a letter or a digit or the underscore character. An operand is either a set of contiguous non-white space printing characters or a string. A string is a set of contiguous printing characters delimited by a quote character at each end. A string value must have less than 256 bytes of data. If at least one operand is present in an operation, there is a single space between the opcode and the first operand. If more than one operand is present in an operation, there is a single blank character between every two adjacent operands. If there are no operands, a semicolon character is appended to the opcode to mark the end of the operation. If any operands appear, the last operand has an appended semicolon that marks the end of the operation. Any given opcode appears at most once per EPD record. Multiple operations in a single EPD record should appear in ASCII order of their opcode names (mnemonics). However, a program reading EPD records may allow for operations not in ASCII order by opcode mnemonics; the semantics are the same in either case. Some opcodes that allow for more than one operand may have special ordering requirements for the operands. For example, the "pv" (predicted variation) opcode requires its operands (moves) to appear in the order in which they would be played. All other opcodes that allow for more than one operand should have operands appearing in ASCII order. An example of the latter set is the "bm" (best move[s]) opcode; its operands are moves that are all immediately playable from the current position. Some opcodes require one or more operands that are chess moves. These moves should be represented using SAN. If a different representation is used, there is no guarantee that the EPD will be read correctly during subsequent processing. Some opcodes require one or more operands that are integers. Some opcodes may require that an integer operand must be within a given range; the details are described in the opcode list given below. A negative integer is formed with a hyphen (minus sign) preceding the integer digit sequence. An optional plus sign may be used for indicating a non-negative value, but such use is not required and is indeed discouraged. Some opcodes require one or more operands that are floating point numbers. Some opcodes may require that a floating point operand must be within a given range; the details are described in the opcode list given below. A floating point operand is constructed from an optional sign character ("+" or "-"), a digit sequence (with at least one digit), a radix point (always "."), and a final digit sequence (with at least one digit). 16.2.4.2: Opcode mnemonics An opcode mnemonic used for archival storage and for interprogram communication starts with a lower case letter and is composed of only lower case letters, digits, and the underscore character (i.e., no upper case letters). These mnemonics will also all be at least two characters in length. Opcode mnemonics used only by a single program or an experimental suite of programs should start with an upper case letter. This is so they may be easily distinguished should they be inadvertently be encountered by other programs. When a such a "private" opcode be demonstrated to be widely useful, it should be brought into the official list (appearing below) in a lower case form. If a given program does not recognize a particular opcode, that operation is simply ignored; it is not signaled as an error. 16.2.5: Opcode list The opcodes are listed here in ASCII order of their mnemonics. Suggestions for new opcodes should be sent to the PGN standard coordinator listed near the start of this document. 16.2.5.1: Opcode "acn": analysis count: nodes The opcode "acn" takes a single non-negative integer operand. It is used to represent the number of nodes examined in an analysis. Note that the value may be quite large for some extended searches and so use of (at least) a long (four byte) representation is suggested. 16.2.5.2: Opcode "acs": analysis count: seconds The opcode "acs" takes a single non-negative integer operand. It is used to represent the number of seconds used for an analysis. Note that the value may be quite large for some extended searches and so use of (at least) a long (four byte) representation is suggested. 16.2.5.3: Opcode "am": avoid move(s) The opcode "am" indicates a set of zero or more moves, all immediately playable from the current position, that are to be avoided in the opinion of the EPD writer. Each operand is a SAN move; they appear in ASCII order. 16.2.5.4: Opcode "bm": best move(s) The opcode "bm" indicates a set of zero or more moves, all immediately playable from the current position, that are judged to the best available by the EPD writer. Each operand is a SAN move; they appear in ASCII order. 16.2.5.5: Opcode "c0": comment (primary, also "c1" though "c9") The opcode "c0" (lower case letter "c", digit character zero) indicates a top level comment that applies to the given position. It is the first of ten ranked comments, each of which has a mnemonic formed from the lower case letter "c" followed by a single decimal digit. Each of these opcodes takes either a single string operand or no operand at all. This ten member comment family of opcodes is intended for use as descriptive commentary for a complete game or game fragment. The usual processing of these opcodes are as follows: 1) At the beginning of a game (or game fragment), a move sequence scanning program initializes each element of its set of ten comment string registers to be null. 2) As the EPD record for each position in the game is processed, the comment operations are interpreted from left to right. (Actually, all operations in n EPD record are interpreted from left to right.) Because operations appear in ASCII order according to their opcode mnemonics, opcode "c0" (if present) will be handled prior to all other opcodes, then opcode "c1" (if present), and so forth until opcode "c9" (if present). 3) The processing of opcode "cN" (0 <= N <= 9) involves two steps. First, all comment string registers with an index equal to or greater than N are set to null. (This is the set "cN" though "c9".) Second, and only if a string operand is present, the value of the corresponding comment string register is set equal to the string operand. 16.2.5.6: Opcode "ce": centipawn evaluation The opcode "ce" indicates the evaluation of the indicated position in centipawn units. It takes a single operand, an optionally signed integer that gives an evaluation of the position from the viewpoint of the active player; i.e., the player with the move. Positive values indicate a position favorable to the moving player while negative values indicate a position favorable to the passive player; i.e., the player without the move. A centipawn evaluation value close to zero indicates a neutral positional evaluation. Values are restricted to integers that are equal to or greater than -32767 and are less than or equal to 32766. A value greater than 32000 indicates the availability of a forced mate to the active player. The number of plies until mate is given by subtracting the evaluation from the value 32767. Thus, a winning mate in N fullmoves is a mate in ((2 * N) - 1) halfmoves (or ply) and has a corresponding centipawn evaluation of (32767 - ((2 * N) - 1)). For example, a mate on the move (mate in one) has a centipawn evaluation of 32766 while a mate in five has a centipawn evaluation of 32758. A value less than -32000 indicates the availability of a forced mate to the passive player. The number of plies until mate is given by subtracting the evaluation from the value -32767 and then negating the result. Thus, a losing mate in N fullmoves is a mate in (2 * N) halfmoves (or ply) and has a corresponding centipawn evaluation of (-32767 + (2 * N)). For example, a mate after the move (losing mate in one) has a centipawn evaluation of -32765 while a losing mate in five has a centipawn evaluation of -32757. A value of -32767 indicates an illegal position. A stalemate position has a centipawn evaluation of zero as does a position drawn due to insufficient mating material. Any other position known to be a certain forced draw also has a centipawn evaluation of zero. 16.2.5.7: Opcode "dm": direct mate fullmove count The "dm" opcode is used to indicate the number of fullmoves until checkmate is to be delivered by the active color for the indicated position. It always takes a single operand which is a positive integer giving the fullmove count. For example, a position known to be a "mate in three" would have an operation of "dm 3;" to indicate this. This opcode is intended for use with problem sets composed of positions requiring direct mate answers as solutions. 16.2.5.8: Opcode "draw_accept": accept a draw offer The opcode "draw_accept" is used to indicate that a draw offer made after the move that lead to the indicated position is accepted by the active player. This opcode takes no operands. 16.2.5.9: Opcode "draw_claim": claim a draw The opcode "draw_claim" is used to indicate claim by the active player that a draw exists. The draw is claimed because of a third time repetition or because of the fifty move rule or because of insufficient mating material. A supplied move (see the opcode "sm") is also required to appear as part of the same EPD record. The draw_claim opcode takes no operands. 16.2.5.10: Opcode "draw_offer": offer a draw The opcode "draw_offer" is used to indicate that a draw is offered by the active player. A supplied move (see the opcode "sm") is also required to appear as part of the same EPD record; this move is considered played from the indicated position. The draw_offer opcode takes no operands. 16.2.5.11: Opcode "draw_reject": reject a draw offer The opcode "draw_reject" is used to indicate that a draw offer made after the move that lead to the indicated position is rejected by the active player. This opcode takes no operands. 16.2.5.12: Opcode "eco": _Encyclopedia of Chess Openings_ opening code The opcode "eco" is used to associate an opening designation from the _Encyclopedia of Chess Openings_ taxonomy with the indicated position. The opcode takes either a single string operand (the ECO opening name) or no operand at all. If an operand is present, its value is associated with an "ECO" string register of the scanning program. If there is no operand, the ECO string register of the scanning program is set to null. The usage is similar to that of the "ECO" tag pair of the PGN standard. 16.2.5.13: Opcode "fmvn": fullmove number The opcode "fmvn" represents the fullmove n umber associated with the position. It always takes a single operand that is the positive integer value of the move number. This opcode is used to explicitly represent the fullmove number in EPD that is present by default in FEN as the sixth field. Fullmove number information is usually omitted from EPD because it does not affect move generation (commonly needed for EPD-using tasks) but it does affect game notation (commonly needed for FEN-using tasks). Because of the desire for space optimization for large EPD files, fullmove numbers were dropped from EPD's parent FEN. The halfmove clock information was similarly dropped. 16.2.5.14: Opcode "hmvc": halfmove clock The opcode "hmvc" represents the halfmove clock associated with the position. The halfmove clock of a position is equal to the number of plies since the last pawn move or capture. This information is used to implement the fifty move draw rule. It always takes a single operand that is the non-negative integer value of the halfmove clock. This opcode is used to explicitly represent the halfmove clock in EPD that is present by default in FEN as the fifth field. Halfmove clock information is usually omitted from EPD because it does not affect move generation (commonly needed for EPD-using tasks) but it does affect game termination issues (commonly needed for FEN-using tasks). Because of the desire for space optimization for large EPD files, halfmove clock values were dropped from EPD's parent FEN. The fullmove number information was similarly dropped. 16.2.5.15: Opcode "id": position identification The opcode "id" is used to provide a simple identifying label for the indicated position. It takes a single string operand. This opcode is intended for use with test suites used for measuring chessplaying program strength. An example "id" operand for the seven hundred fifty seventh position of the one thousand one problems in Reinfeld's _1001 Winning Chess Sacrifices and Combinations_ would be "WCSAC.0757" while the fifteenth position in the twenty four problem Bratko-Kopec test suite would have an "id" operand of "BK.15". 16.2.5.16: Opcode "nic": _New In Chess_ opening code The opcode "nic" is used to associate an opening designation from the _New In Chess_ taxonomy with the indicated position. The opcode takes either a single string operand (the NIC opening name) or no operand at all. If an operand is present, its value is associated with an "NIC" string register of the scanning program. If there is no operand, the NIC string register of the scanning program is set to null. The usage is similar to that of the "NIC" tag pair of the PGN standard. 16.2.5.17: Opcode "noop": no operation The "noop" opcode is used to indicate no operation. It takes zero or more operands, each of which may be of any type. The operation involves no processing. It is intended for use by developers for program testing purposes. 16.2.5.18: Opcode "pm": predicted move The "pm" opcode is used to provide a single predicted move for the indicated position. It has exactly one operand, a move playable from the position. This move is judged by the EPD writer to represent the best move available to the active player. If a non-empty "pv" (predicted variation) line of play is also present in the same EPD record, the first move of the predicted variation is the same as the predicted move. The "pm" opcode is intended for use as a general "display hint" mechanism. 16.2.5.19: Opcode "pv": predicted variation The "pv" opcode is used to provide a predicted variation for the indicated position. It has zero or more operands which represent a sequence of moves playable from the position. This sequence is judged by the EPD writer to represent the best play available. If a "pm" (predicted move) operation is also present in the same EPD record, the predicted move is the same as the first move of the predicted variation. 16.2.5.20: Opcode "rc": repetition count The "rc" opcode is used to indicate the number of occurrences of the indicated position. It takes a single, positive integer operand. Any position, including the initial starting position, is considered to have an "rc" value of at least one. A value of three indicates a candidate for a draw claim by the position repetition rule. 16.2.5.21: Opcode "resign": game resignation The opcode "resign" is used to indicate that the active player has resigned the game. This opcode takes no operands. 16.2.5.22: Opcode "sm": supplied move The "sm" opcode is used to provide a single supplied move for the indicated position. It has exactly one operand, a move playable from the position. This move is the move to be played from the position. The "sm" opcode is intended for use to communicate the most recent played move in an active game. It is used to communicate moves between programs in automatic play via a network. This includes correspondence play using e-mail and also programs acting as network front ends to human players. 16.2.5.23: Opcode "tcgs": telecommunication: game selector The "tcgs" opcode is one of the telecommunication family of opcodes used for games conducted via e-mail and similar means. This opcode takes a single operand that is a positive integer. It is used to select among various games in progress between the same sender and receiver. 16.2.5.24: Opcode "tcri": telecommunication: receiver identification The "tcri" opcode is one of the telecommunication family of opcodes used for games conducted via e-mail and similar means. This opcode takes two order dependent string operands. The first operand is the e-mail address of the receiver of the EPD record. The second operand is the name of the player (program or human) at the address who is the actual receiver of the EPD record. 16.2.5.25: Opcode "tcsi": telecommunication: sender identification The "tcsi" opcode is one of the telecommunication family of opcodes used for games conducted via e-mail and similar means. This opcode takes two order dependent string operands. The first operand is the e-mail address of the sender of the EPD record. The second operand is the name of the player (program or human) at the address who is the actual sender of the EPD record. 16.2.5.26: Opcode "v0": variation name (primary, also "v1" though "v9") The opcode "v0" (lower case letter "v", digit character zero) indicates a top level variation name that applies to the given position. It is the first of ten ranked variation names, each of which has a mnemonic formed from the lower case letter "v" followed by a single decimal digit. Each of these opcodes takes either a single string operand or no operand at all. This ten member variation name family of opcodes is intended for use as traditional variation names for a complete game or game fragment. The usual processing of these opcodes are as follows: 1) At the beginning of a game (or game fragment), a move sequence scanning program initializes each element of its set of ten variation name string registers to be null. 2) As the EPD record for each position in the game is processed, the variation name operations are interpreted from left to right. (Actually, all operations in n EPD record are interpreted from left to right.) Because operations appear in ASCII order according to their opcode mnemonics, opcode "v0" (if present) will be handled prior to all other opcodes, then opcode "v1" (if present), and so forth until opcode "v9" (if present). 3) The processing of opcode "vN" (0 <= N <= 9) involves two steps. First, all variation name string registers with an index equal to or greater than N are set to null. (This is the set "vN" though "v9".) Second, and only if a string operand is present, the value of the corresponding variation name string register is set equal to the string operand. 17: Alternative chesspiece identifier letters English language piece names are used to define the letter set for identifying chesspieces in PGN movetext. However, authors of programs which are used only for local presentation or scanning of chess move data may find it convenient to use piece letter codes common in their locales. This is not a problem as long as PGN data that resides in archival storage or that is exchanged among programs still uses the SAN (English) piece letter codes: "PNBRQK". For the above authors only, a list of alternative piece letter codes are provided: Language Piece letters (pawn knight bishop rook queen king) ---------- -------------------------------------------------- Czech P J S V D K Danish B S L T D K Dutch O P L T D K English P N B R Q K Estonian P R O V L K Finnish P R L T D K French P C F T D R German B S L T D K Hungarian G H F B V K Icelandic P R B H D K Italian P C A T D R Norwegian B S L T D K Polish P S G W H K Portuguese P C B T D R Romanian P C N T D R Spanish P C A T D R Swedish B S L T D K 18: Formal syntax ::= ::= ::= ::= [ ] ::= ::= ::= ::= ::= ::= ( ) ::= 1-0 0-1 1/2-1/2 * ::= 19: Canonical chess position hash coding *** This section is under development. 20: Binary representation (PGC) *** This section is under development. The binary coded version of PGN is PGC (PGN Game Coding). PGC is a binary representation standard of PGN data designed for the dual goals of storage efficiency and program I/O. A file containing PGC data should have a name with a suffix of ".pgc". Unlike PGN text files that may have locale dependent representations for newlines, PGC files have data that does not vary due to local processing environment. This means that PGC files may be transferred among systems using general binary file methods. PGC files should be used only when the use of PGN is impractical due to time and space resource constraints. As the general level of processing capabilities increases, the need for PGC over PGN will decrease. Therefore, implementors are encouraged not to use PGC as the default representation because it is much more difficult (than PGN) to understand without proper software. PGC data is composed of a sequence of PGC records. Each record is composed of a sequence of one or more bytes. The first byte is the PGN record marker and it specifies the interpretation of the remaining portion of the record. This remaining portion is composed of zero or more PGN record items. Item types include move sequences, move sets, and character strings. 20.1: Bytes, words, and doublewords At the lowest level, PGC binary data is organized as bytes, words (two contiguous bytes), and doublewords (four contiguous bytes). All eight bits of a byte are used. Longwords (eight contiguous bytes) are not used. Integer values are stored using two's complement representation. Integers may be signed or unsigned depending on context. Multibyte integers are stored in low-endian format with the least significant byte appearing first. A one byte integer item is called "int-1". A two byte integer item is called "int-2". A four byte integer item is called "int-4". Characters are stored as bytes using the ISO 8859/1 Latin-1 (ECMA-94) code set. There is no provision for other characters sets or representations. 20.2: Move ordinals A chess move is represented using a move ordinal. This is a single unsigned byte quantity with values from zero to 255. A move ordinal is interpreted as an index into the list of legal moves from the current position. This list is constructed by generating the legal moves from the current position, assigning SAN ASCII strings to each move, and then sorting these strings in ascending order. Note that a seven bit ordinal, as used by some inferior representation systems, is insufficient as there are some positions that have more than 128 moves available. Examples: From the initial position, there are twenty moves. Move ordinal 0 corresponds to the SAN move string "Na3"; move ordinal 1 corresponds to "Nc3", move ordinal 4 corresponds to "a3", and move ordinal 19 corresponds to "h4". Moves can be organized into sequences and sets. A move sequence is an ordered list of moves that are played, one after another from first to last. A move set is a list of moves that are all playable from the current position. Move sequence data is represented using a length header followed by move ordinal data. The length header is an unsigned integer that may be a byte or a word. The integer gives the number, possibly zero, of following move ordinal bytes. Most move sequences can be represented using just a byte header; these are called "mvseq-1" items. Move sequence data using a word header are called "mvseq-2" items. Move set data is represented using a length header followed by move ordinal data. The length header is an unsigned integer that is a byte. The integer gives the number, possibly zero, of following move ordinal bytes. All move sets are be represented using just a byte header; these are called "mvset-1" items. (Note the implied restriction that a move set can only have a maximum of 255 of the possible 256 ordinals present at one time.) 20.3: String data PGC string data is represented using a length header followed by bytes of character data. The length header is an unsigned integer that may be a byte, a word, or a doubleword. The integer gives the number, possibly zero, of following character bytes. Most strings can be represented using just a byte header; these are called "string-1" items. String data using a word header are called "string-2" items and string data using a doubleword header are called "string-4" items. No special ASCII NUL termination byte is required for PGC storage of a string as the length is explicitly given in the item header. 20.4: Marker codes PGC marker codes are given in hexadecimal format. PGC marker code zero (marker 0x00) is the "noop" marker and carries no meaning. Each additional marker code defined appears in its own subsection below. 20.4.1: Marker 0x01: reduced export format single game Marker 0x01 is used to indicate a single complete game in reduced export format. This refers to a game that has only the Seven Tag Roster data, played moves, and no annotations or comments. This record type is used as an alternative to the general game data begin/end record pairs described below. The general marker pair (0x05/0x06) is used to help represent game data that can't be adequately represented in reduced export format. There are eight items that follow marker 0x01 to form the "reduced export format single game" record. In order, these are: 1) string-1 (Event tag value) 2) string-1 (Site tag value) 3) string-1 (Date tag value) 4) string-1 (Round tag value) 5) string-1 (White tag value) 6) string-1 (Black tag value) 7) string-1 (Result tag value) 8) mvseq-2 (played moves) 20.4.2: Marker 0x02: tag pair Marker 0x02 is used to indicate a single tag pair. There are two items that follow marker 0x02 to form the "tag pair" record; in order these are: 1) string-1 (tag pair name) 2) string-1 (tag pair value) 20.4.3: Marker 0x03: short move sequence Marker 0x03 is used to indicate a short move sequence. There is one item that follows marker 0x03 to form the "short move sequence" record; this is: 1) mvseq-1 (played moves) 20.4.4: Marker 0x04: long move sequence Marker 0x04 is used to indicate a long move sequence. There is one item that follows marker 0x04 to form the "long move sequence" record; this is: 1) mvseq-2 (played moves) 20.4.5: Marker 0x05: general game data begin Marker 0x05 is used to indicate the beginning of data for a game. It has no associated items; it is a complete record by itself. Instead, it marks the beginning of PGC records used to describe a game. All records up to the corresponding "general game data end" record are considered to be part of the same game. (PGC record type 0x01, "reduced export format single game", is not permitted to appear within a general game begin/end record pair. The general game construct is to be used as an alternative to record type 0x01 in those cases where the latter is too restrictive to contain the data for a game.) 20.4.6: Marker 0x06: general game data end Marker 0x06 is used to indicate the end of data for a game. It has no associated items; it is a complete record by itself. Instead, it marks the end of PGC records used to describe a game. All records after the corresponding (and earlier appearing) "general game data begin" record are considered to be part of the same game. 20.4.7: Marker 0x07: simple-nag Marker 0x07 is used to indicate the presence of a simple NAG (Numeric Annotation Glyph). This is an annotation marker that has only a short type identification and no operands. There is one item that follows marker 0x07 to form the "simple-nag" record; this is: 1) int-1 (unsigned NAG value, from 0 to 255) 20.4.8: Marker 0x08: rav-begin Marker 0x08 is used to indicate the beginning of an RAV (Recursive Annotation Variation). It has no associated items; it is a complete record by itself. Instead, it marks the beginning of PGC records used to describe a recursive annotation. It is considered an opening bracket for a later rav-end record; the recursive annotation is completely described between the bracket pair. The rav-begin/data/rav-end structures can be nested. 20.4.9: Marker 0x09: rav-end Marker 0x09 is used to indicate the end of an RAV (Recursive Annotation Variation). It has no associated items; it is a complete record by itself. Instead, it marks the end of PGC records used to describe a recursive annotation. It is considered a closing bracket for an earlier rav-begin record; the recursive annotation is completely described between the bracket pair. The rav-begin/data/rav-end structures can be nested. 20.4.10: Marker 0x0a: escape-string Marker 0x0a is used to indicate the presence of an escape string. This is a string represented by the use of the percent sign ("%") escape mechanism in PGN. The data that is escaped is the sequence of characters immediately follwoing the percent sign up to but not including the terminating newline. As is the case with the PGN percent sign escape, the use of a PGC escape-string record is limited to use for non-archival data. There is one item that follows marker 0x0a to form the "escape-string" record; this is the string data being escaped: 1) string-2 (escaped string data) 21: E-mail correspondence usage *** This section is under development. Standard: EOF ================================================ FILE: files/misc/scraps.js ================================================ "use strict"; function NewPGNFileLoader(filename, callback) { let loader = Object.create(null); loader.type = "pgn"; loader.callback = callback; loader.msg = "Loading PGN..."; loader.buf = null; loader.preparser = null; loader.shutdown = function() { this.callback = null; this.msg = ""; this.buf = null; if (this.preparser) { this.preparser.shutdown(); this.preparser = null; } }; loader.load = function(filename) { fs.readFile(filename, (err, data) => { if (err) { console.log(err); this.shutdown(); } else if (this.callback) { // We might already have aborted this.buf = data; this.continue(); } }); }; loader.continue = function() { if (!this.callback) { return; } if (!this.preparser) { this.preparser = NewPGNPreParser(this.buf, (games) => { if (this.callback) { let cb = this.callback; cb(games); } this.shutdown(); }); } this.msg = this.preparser.msg; setTimeout(() => {this.continue();}, 20); // Just to update these messages. }; setTimeout(() => {loader.load(filename);}, 0); return loader; } // ------------------------------------------------------------------------------------------------------------------------------ function NewPGNPreParser(buf, callback) { // Cannot fail unless aborted. let loader = Object.create(null); loader.type = "pgn"; loader.callback = callback; loader.msg = "Preparsing..."; loader.games = [new_pgn_record()]; loader.lines = null; loader.buf = buf; loader.splitter = null; loader.n = 0; loader.shutdown = function() { this.callback = null; this.msg = ""; this.games = null; this.lines = null; this.buf = null; if (this.splitter) { this.splitter.shutdown(); this.splitter = null; } }; loader.continue = function() { if (!this.callback) { return; } if (!this.splitter) { this.splitter = NewLineSplitter(this.buf, (lines) => { this.lines = lines; }); } if (!this.lines) { this.msg = this.splitter.msg; setTimeout(() => {this.continue();}, 20); return; } let continuetime = performance.now(); while (true) { if (this.n >= this.lines.length) { break; } let rawline = this.lines[this.n++]; if (rawline.length === 0) { continue; } if (rawline[0] === 37) { // Percent % sign is a special comment type. continue; } let tagline = ""; if (rawline[0] === 91) { let s = decoder.decode(rawline).trim(); if (s.endsWith(`"]`)) { tagline = s; } } if (tagline !== "") { if (this.games[this.games.length - 1].movebufs.length > 0) { // We have movetext already, so this must be a new game. Start it. this.games.push(new_pgn_record()); } // Parse the tag line... tagline = tagline.slice(1, -1); // So now it's like: Foo "bar etc" let quote_i = tagline.indexOf(`"`); if (quote_i === -1) { continue; } let key = tagline.slice(0, quote_i).trim(); let value = tagline.slice(quote_i + 1).trim(); if (value.endsWith(`"`)) { value = value.slice(0, -1); } this.games[this.games.length - 1].tags[key] = SafeStringHTML(value); // Escape evil characters. IMPORTANT! } else { this.games[this.games.length - 1].movebufs.push(rawline); } if (this.n % 1000 === 0) { if (performance.now() - continuetime > 20) { this.msg = `Preparsing... ${(100 * (this.n / this.lines.length)).toFixed(0)}%`; setTimeout(() => {this.continue();}, 20); return; } } } // Once, after the while loop is broken... let cb = this.callback; cb(this.games); this.shutdown(); }; setTimeout(() => {loader.continue();}, 0); // setTimeout especially required here since there's no async load() function in this one. return loader; } // ------------------------------------------------------------------------------------------------------------------------------ function NewLineSplitter(buf, callback) { // The original sync version of this is in misc/scraps.js and is easier to read. let loader = Object.create(null); loader.type = "?"; loader.callback = callback; loader.msg = "PGN: Splitting..."; loader.lines = []; loader.buf = buf; loader.a = 0; loader.b = 0; if (buf.length > 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) { loader.a = 3; // 1st slice will skip byte order mark (BOM). } loader.shutdown = function() { this.callback = null; this.msg = ""; this.lines = null; this.buf = null; }; loader.append = function(arr) { if (arr.length > 0 && arr[arr.length - 1] === 13) { // Discard \r this.lines.push(Buffer.from(arr.slice(0, -1))); } else { this.lines.push(Buffer.from(arr)); } }; loader.continue = function() { if (!this.callback) { return; } let continuetime = performance.now(); while (true) { if (this.b >= this.buf.length) { break; } if (this.buf[this.b] === 10) { // Split on \n let line = this.buf.slice(this.a, this.b); this.append(line); this.a = this.b + 1; } this.b++; if (this.lines.length % 1000 === 0) { if (performance.now() - continuetime > 20) { this.msg = `PGN: Splitting... ${(100 * (this.b / this.buf.length)).toFixed(0)}%`; setTimeout(() => {this.continue();}, 20); return; } } } // Once, after the while loop is broken... if (this.a !== this.b) { // We haven't added the last line before EOF. let line = this.buf.slice(this.a, this.b); this.append(line); } let cb = this.callback; cb(this.lines); this.shutdown(); }; setTimeout(() => {loader.continue();}, 0); // setTimeout especially required here since there's no async load() function in this one. return loader; } // ------------------------------------------------------------------------------------------------------------------------------ function split_buffer_alternative(buf) { // Split a binary buffer into an array of binary buffers corresponding to lines. let lines = []; let search = Buffer.from("\n"); let off = 0; if (buf.length > 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) { off = 3; // Skip byte order mark (BOM). } while (true) { let hi = buf.indexOf(search, off); if (hi === -1) { if (off < buf.length) { lines.push(buf.slice(off)); } return lines; } if (buf[hi - 1] === 13) { // Discard \r lines.push(buf.slice(off, hi - 1)); } else { lines.push(buf.slice(off, hi)); } off = hi + 1; } } ================================================ FILE: files/misc/state_logic.txt ================================================ State logic As of 2020-05-26: /-----------> halt() | 1 | "bestmove" --> move() --> position_changed() --> behave() -----> go() ^ | ^ ^ | | | | User changes POSITION --------/ \----> set_behaviour() | 2 ^ | | | User changes BEHAVIOUR ----------------------------/ | 3 | | User changes SEARCHMOVES -----> handle_searchmoves_change() ------/ | | User changes NODE LIMITS ------> handle_node_limit_change() ------/ Notes: 1. Only with relevant behaviour setting 2. This path only sets "halt" 3. Except that "analysis_locked" has its own function go_and_lock() ================================================ FILE: files/misc/uci.txt ================================================ Description of the universal chess interface (UCI) April 2006 ================================================================= * The specification is independent of the operating system. For Windows, the engine is a normal exe file, either a console or "real" windows application. * all communication is done via standard input and output with text commands, * The engine should boot and wait for input from the GUI, the engine should wait for the "isready" or "setoption" command to set up its internal parameters as the boot process should be as quick as possible. * the engine must always be able to process input from stdin, even while thinking. * all command strings the engine receives will end with '\n', also all commands the GUI receives should end with '\n', Note: '\n' can be 0x0d or 0x0a0d or any combination depending on your OS. If you use Engine and GUI in the same OS this should be no problem if you communicate in text mode, but be aware of this when for example running a Linux engine in a Windows GUI. * arbitrary white space between tokens is allowed Example: "debug on\n" and " debug on \n" and "\t debug \t \t\ton\t \n" all set the debug mode of the engine on. * The engine will always be in forced mode which means it should never start calculating or pondering without receiving a "go" command first. * Before the engine is asked to search on a position, there will always be a position command to tell the engine about the current position. * by default all the opening book handling is done by the GUI, but there is an option for the engine to use its own book ("OwnBook" option, see below) * if the engine or the GUI receives an unknown command or token it should just ignore it and try to parse the rest of the string in this line. Examples: "joho debug on\n" should switch the debug mode on given that joho is not defined, "debug joho on\n" will be undefined however. * if the engine receives a command which is not supposed to come, for example "stop" when the engine is not calculating, it should also just ignore it. Move format: ------------ The move format is in long algebraic notation. A nullmove from the Engine to the GUI should be sent as 0000. Examples: e2e4, e7e5, e1g1 (white short castling), e7e8q (for promotion) GUI to engine: -------------- These are all the command the engine gets from the interface. * uci tell engine to use the uci (universal chess interface), this will be sent once as a first command after program boot to tell the engine to switch to uci mode. After receiving the uci command the engine must identify itself with the "id" command and send the "option" commands to tell the GUI which engine settings the engine supports if any. After that the engine should send "uciok" to acknowledge the uci mode. If no uciok is sent within a certain time period, the engine task will be killed by the GUI. * debug [ on | off ] switch the debug mode of the engine on and off. In debug mode the engine should send additional infos to the GUI, e.g. with the "info string" command, to help debugging, e.g. the commands that the engine has received etc. This mode should be switched off by default and this command can be sent any time, also when the engine is thinking. * isready this is used to synchronize the engine with the GUI. When the GUI has sent a command or multiple commands that can take some time to complete, this command can be used to wait for the engine to be ready again or to ping the engine to find out if it is still alive. E.g. this should be sent after setting the path to the tablebases as this can take some time. This command is also required once before the engine is asked to do any search to wait for the engine to finish initializing. This command must always be answered with "readyok" and can be sent also when the engine is calculating in which case the engine should also immediately answer with "readyok" without stopping the search. * setoption name [value ] this is sent to the engine when the user wants to change the internal parameters of the engine. For the "button" type no value is needed. One string will be sent for each parameter and this will only be sent when the engine is waiting. The name and value of the option in should not be case sensitive and can inlude spaces. The substrings "value" and "name" should be avoided in and to allow unambiguous parsing, for example do not use = "draw value". Here are some strings for the example below: "setoption name Nullmove value true\n" "setoption name Selectivity value 3\n" "setoption name Style value Risky\n" "setoption name Clear Hash\n" "setoption name NalimovPath value c:\chess\tb\4;c:\chess\tb\5\n" * register this is the command to try to register an engine or to tell the engine that registration will be done later. This command should always be sent if the engine has sent "registration error" at program startup. The following tokens are allowed: * later the user doesn't want to register the engine now. * name the engine should be registered with the name * code the engine should be registered with the code Example: "register later" "register name Stefan MK code 4359874324" * ucinewgame this is sent to the engine when the next search (started with "position" and "go") will be from a different game. This can be a new game the engine should play or a new game it should analyse but also the next position from a testsuite with positions only. If the GUI hasn't sent a "ucinewgame" before the first "position" command, the engine shouldn't expect any further ucinewgame commands as the GUI is probably not supporting the ucinewgame command. So the engine should not rely on this command even though all new GUIs should support it. As the engine's reaction to "ucinewgame" can take some time the GUI should always send "isready" after "ucinewgame" to wait for the engine to finish its operation. * position [fen | startpos ] moves .... set up the position described in fenstring on the internal board and play the moves on the internal chess board. if the game was played from the start position the string "startpos" will be sent Note: no "new" command is needed. However, if this position is from a different game than the last position sent to the engine, the GUI should have sent a "ucinewgame" inbetween. * go start calculating on the current position set up with the "position" command. There are a number of commands that can follow this command, all will be sent in the same string. If one command is not sent its value should be interpreted as it would not influence the search. * searchmoves .... restrict search to this moves only Example: After "position startpos" and "go infinite searchmoves e2e4 d2d4" the engine should only search the two moves e2e4 and d2d4 in the initial position. * ponder start searching in pondering mode. Do not exit the search in ponder mode, even if it's mate! This means that the last move sent in in the position string is the ponder move. The engine can do what it wants to do, but after a "ponderhit" command it should execute the suggested move to ponder on. This means that the ponder move sent by the GUI can be interpreted as a recommendation about which move to ponder. However, if the engine decides to ponder on a different move, it should not display any mainlines as they are likely to be misinterpreted by the GUI because the GUI expects the engine to ponder on the suggested move. * wtime white has x msec left on the clock * btime black has x msec left on the clock * winc white increment per move in mseconds if x > 0 * binc black increment per move in mseconds if x > 0 * movestogo there are x moves to the next time control, this will only be sent if x > 0, if you don't get this and get the wtime and btime it's sudden death * depth search x plies only. * nodes search x nodes only, * mate search for a mate in x moves * movetime search exactly x mseconds * infinite search until the "stop" command. Do not exit the search without being told so in this mode! * stop stop calculating as soon as possible, don't forget the "bestmove" and possibly the "ponder" token when finishing the search * ponderhit the user has played the expected move. This will be sent if the engine was told to ponder on the same move the user has played. The engine should continue searching but switch from pondering to normal search. * quit quit the program as soon as possible Engine to GUI: -------------- * id * name this must be sent after receiving the "uci" command to identify the engine, e.g. "id name Shredder X.Y\n" * author this must be sent after receiving the "uci" command to identify the engine, e.g. "id author Stefan MK\n" * uciok Must be sent after the id and optional options to tell the GUI that the engine has sent all infos and is ready in uci mode. * readyok This must be sent when the engine has received an "isready" command and has processed all input and is ready to accept new commands now. It is usually sent after a command that can take some time to be able to wait for the engine, but it can be used anytime, even when the engine is searching, and must always be answered with "isready". * bestmove [ ponder ] the engine has stopped searching and found the move best in this position. the engine can send the move it likes to ponder on. The engine must not start pondering automatically. this command must always be sent if the engine stops searching, also in pondering mode if there is a "stop" command, so for every "go" command a "bestmove" command is needed! Directly before that the engine should send a final "info" command with the final search information, the the GUI has the complete statistics about the last search. * copyprotection this is needed for copyprotected engines. After the uciok command the engine can tell the GUI, that it will check the copy protection now. This is done by "copyprotection checking". If the check is ok the engine should send "copyprotection ok", otherwise "copyprotection error". If there is an error the engine should not function properly but should not quit alone. If the engine reports "copyprotection error" the GUI should not use this engine and display an error message instead! The code in the engine can look like this TellGUI("copyprotection checking\n"); // ... check the copy protection here ... if(ok) TellGUI("copyprotection ok\n"); else TellGUI("copyprotection error\n"); * registration this is needed for engines that need a username and/or a code to function with all features. Analog to the "copyprotection" command the engine can send "registration checking" after the uciok command followed by either "registration ok" or "registration error". Also after every attempt to register the engine it should answer with "registration checking" and then either "registration ok" or "registration error". In contrast to the "copyprotection" command, the GUI can use the engine after the engine has reported an error, but should inform the user that the engine is not properly registered and might not use all its features. In addition the GUI should offer to open a dialog to enable registration of the engine. To try to register an engine the GUI can send the "register" command. The GUI has to always answer with the "register" command if the engine sends "registration error" at engine startup (this can also be done with "register later") and tell the user somehow that the engine is not registered. This way the engine knows that the GUI can deal with the registration procedure and the user will be informed that the engine is not properly registered. * info the engine wants to send information to the GUI. This should be done whenever one of the info has changed. The engine can send only selected infos or multiple infos with one info command, e.g. "info currmove e2e4 currmovenumber 1" or "info depth 12 nodes 123456 nps 100000". Also all infos belonging to the pv should be sent together e.g. "info depth 2 score cp 214 time 1242 nodes 2124 nps 34928 pv e2e4 e7e5 g1f3" I suggest to start sending "currmove", "currmovenumber", "currline" and "refutation" only after one second to avoid too much traffic. Additional info: * depth search depth in plies * seldepth selective search depth in plies, if the engine sends seldepth there must also be a "depth" present in the same string. * time the time searched in ms, this should be sent together with the pv. * nodes x nodes searched, the engine should send this info regularly * pv ... the best line found * multipv this for the multi pv mode. for the best move/pv add "multipv 1" in the string when you send the pv. in k-best mode always send all k variants in k strings together. * score * cp the score from the engine's point of view in centipawns. * mate mate in y moves, not plies. If the engine is getting mated use negative values for y. * lowerbound the score is just a lower bound. * upperbound the score is just an upper bound. * currmove currently searching this move * currmovenumber currently searching move number x, for the first move x should be 1 not 0. * hashfull the hash is x permill full, the engine should send this info regularly * nps x nodes per second searched, the engine should send this info regularly * tbhits x positions where found in the endgame table bases * sbhits x positions where found in the shredder endgame databases * cpuload the cpu usage of the engine is x permill. * string any string str which will be displayed be the engine, if there is a string command the rest of the line will be interpreted as . * refutation ... move is refuted by the line ... , i can be any number >= 1. Example: after move d1h5 is searched, the engine can send "info refutation d1h5 g6h5" if g6h5 is the best answer after d1h5 or if g6h5 refutes the move d1h5. if there is no refutation for d1h5 found, the engine should just send "info refutation d1h5" The engine should only send this if the option "UCI_ShowRefutations" is set to true. * currline ... this is the current line the engine is calculating. is the number of the cpu if the engine is running on more than one cpu. = 1,2,3.... if the engine is just using one cpu, can be omitted. If is greater than 1, always send all k lines in k strings together. The engine should only send this if the option "UCI_ShowCurrLine" is set to true. * option This command tells the GUI which parameters can be changed in the engine. This should be sent once at engine startup after the "uci" and the "id" commands if any parameter can be changed in the engine. The GUI should parse this and build a dialog for the user to change the settings. Note that not every option needs to appear in this dialog as some options like "Ponder", "UCI_AnalyseMode", etc. are better handled elsewhere or are set automatically. If the user wants to change some settings, the GUI will send a "setoption" command to the engine. Note that the GUI need not send the setoption command when starting the engine for every option if it doesn't want to change the default value. For all allowed combinations see the examples below, as some combinations of this tokens don't make sense. One string will be sent for each parameter. * name The option has the name id. Certain options have a fixed value for , which means that the semantics of this option is fixed. Usually those options should not be displayed in the normal engine options window of the GUI but get a special treatment. "Pondering" for example should be set automatically when pondering is enabled or disabled in the GUI options. The same for "UCI_AnalyseMode" which should also be set automatically by the GUI. All those certain options have the prefix "UCI_" except for the first 6 options below. If the GUI gets an unknown Option with the prefix "UCI_", it should just ignore it and not display it in the engine's options dialog. * = Hash, type is spin the value in MB for memory for hash tables can be changed, this should be answered with the first "setoptions" command at program boot if the engine has sent the appropriate "option name Hash" command, which should be supported by all engines! So the engine should use a very small hash first as default. * = NalimovPath, type string this is the path on the hard disk to the Nalimov compressed format. Multiple directories can be concatenated with ";" * = NalimovCache, type spin this is the size in MB for the cache for the nalimov table bases These last two options should also be present in the initial options exchange dialog when the engine is booted if the engine supports it * = Ponder, type check this means that the engine is able to ponder. The GUI will send this whenever pondering is possible or not. Note: The engine should not start pondering on its own if this is enabled, this option is only needed because the engine might change its time management algorithm when pondering is allowed. * = OwnBook, type check this means that the engine has its own book which is accessed by the engine itself. if this is set, the engine takes care of the opening book and the GUI will never execute a move out of its book for the engine. If this is set to false by the GUI, the engine should not access its own book. * = MultiPV, type spin the engine supports multi best line or k-best mode. the default value is 1 * = UCI_ShowCurrLine, type check, should be false by default, the engine can show the current line it is calculating. see "info currline" above. * = UCI_ShowRefutations, type check, should be false by default, the engine can show a move and its refutation in a line. see "info refutations" above. * = UCI_LimitStrength, type check, should be false by default, The engine is able to limit its strength to a specific Elo number, This should always be implemented together with "UCI_Elo". * = UCI_Elo, type spin The engine can limit its strength in Elo within this interval. If UCI_LimitStrength is set to false, this value should be ignored. If UCI_LimitStrength is set to true, the engine should play with this specific strength. This should always be implemented together with "UCI_LimitStrength". * = UCI_AnalyseMode, type check The engine wants to behave differently when analysing or playing a game. For example when playing it can use some kind of learning. This is set to false if the engine is playing a game, otherwise it is true. * = UCI_Opponent, type string With this command the GUI can send the name, title, elo and if the engine is playing a human or computer to the engine. The format of the string has to be [GM|IM|FM|WGM|WIM|none] [|none] [computer|human] Examples: "setoption name UCI_Opponent value GM 2800 human Gary Kasparov" "setoption name UCI_Opponent value none none computer Shredder" * = UCI_EngineAbout, type string With this command, the engine tells the GUI information about itself, for example a license text, usually it doesn't make sense that the GUI changes this text with the setoption command. Example: "option name UCI_EngineAbout type string default Shredder by Stefan Meyer-Kahlen, see www.shredderchess.com" * = UCI_ShredderbasesPath, type string this is either the path to the folder on the hard disk containing the Shredder endgame databases or the path and filename of one Shredder endgame datbase. * = UCI_SetPositionValue, type string the GUI can send this to the engine to tell the engine to use a certain value in centipawns from white's point of view if evaluating this specifix position. The string can have the formats: + | clear + | clearall * type The option has type t. There are 5 different types of options the engine can send * check a checkbox that can either be true or false * spin a spin wheel that can be an integer in a certain range * combo a combo box that can have different predefined strings as a value * button a button that can be pressed to send a command to the engine * string a text field that has a string as a value, an empty string has the value "" * default the default value of this parameter is x * min the minimum value of this parameter is x * max the maximum value of this parameter is x * var a predefined value of this parameter is x Examples: Here are 5 strings for each of the 5 possible types of options "option name Nullmove type check default true\n" "option name Selectivity type spin default 2 min 0 max 4\n" "option name Style type combo default Normal var Solid var Normal var Risky\n" "option name NalimovPath type string default c:\\n" "option name Clear Hash type button\n" Examples: --------- This is how the communication when the engine boots can look like: GUI engine // tell the engine to switch to UCI mode uci // engine identify id name Shredder id author Stefan MK // engine sends the options it can change // the engine can change the hash size from 1 to 128 MB option name Hash type spin default 1 min 1 max 128 // the engine supports Nalimov endgame tablebases option name NalimovPath type string default option name NalimovCache type spin default 1 min 1 max 32 // the engine can switch off Nullmove and set the playing style option name Nullmove type check default true option name Style type combo default Normal var Solid var Normal var Risky // the engine has sent all parameters and is ready uciok // Note: here the GUI can already send a "quit" command if it just wants to find out // details about the engine, so the engine should not initialize its internal // parameters before here. // now the GUI sets some values in the engine // set hash to 32 MB setoption name Hash value 32 // init tbs setoption name NalimovCache value 1 setoption name NalimovPath value d:\tb;c\tb // waiting for the engine to finish initializing // this command and the answer is required here! isready // engine has finished setting up the internal values readyok // now we are ready to go // if the GUI is supporting it, tell the engine that is is // searching on a game that it hasn't searched on before ucinewgame // if the engine supports the "UCI_AnalyseMode" option and the next search is supposed to // be an analysis, the GUI should set "UCI_AnalyseMode" to true if it is currently // set to false with this engine setoption name UCI_AnalyseMode value true // tell the engine to search infinite from the start position after 1.e4 e5 position startpos moves e2e4 e7e5 go infinite // the engine starts sending infos about the search to the GUI // (only some examples are given) info depth 1 seldepth 0 info score cp 13 depth 1 nodes 13 time 15 pv f1b5 info depth 2 seldepth 2 info nps 15937 info score cp 14 depth 2 nodes 255 time 15 pv f1c4 f8c5 info depth 2 seldepth 7 nodes 255 info depth 3 seldepth 7 info nps 26437 info score cp 20 depth 3 nodes 423 time 15 pv f1c4 g8f6 b1c3 info nps 41562 .... // here the user has seen enough and asks to stop the searching stop // the engine has finished searching and is sending the bestmove command // which is needed for every "go" command sent to tell the GUI // that the engine is ready again bestmove g1f3 ponder d8f6 Chess960 ======== UCI could easily be extended to support Chess960 (also known as Fischer Random Chess). The engine has to tell the GUI that it is capable of playing Chess960 and the GUI has to tell the engine that is should play according to the Chess960 rules. This is done by the special engine option UCI_Chess960. If the engine knows about Chess960 it should send the command 'option name UCI_Chess960 type check default false' to the GUI at program startup. Whenever a Chess960 game is played, the GUI should set this engine option to 'true'. Castling is different in Chess960 and the white king move when castling short is not always e1g1. A king move could both be the castling king move or just a normal king move. This is why castling moves are sent in the form king "takes" his own rook. Example: e1h1 for the white short castle move in the normal chess start position. In EPD and FEN position strings specifying the castle rights with w and q is not enough as there could be more than one rook on the right or left side of the king. This is why the castle rights are specified with the letter of the castle rook's line. Upper case letters for white's and lower case letters for black's castling rights. Example: The normal chess position would be: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w AHah - ================================================ FILE: files/res/linux/nibbler.desktop ================================================ [Desktop Entry] Categories=Game;BoardGame; Comment=Nibbler is a real-time analysis GUI for Leela Chess Zero (Lc0). Exec=nibbler GenericName=Chess Analysis GUI Icon=nibbler Name=Nibbler Terminal=false Type=Application ================================================ FILE: files/scripts/builder.py ================================================ import json, os, shutil, zipfile zips = { "windows": "scripts/electron_zipped/electron-v9.4.4-win32-x64.zip", "linux": "scripts/electron_zipped/electron-v9.4.4-linux-x64.zip", } # To build Nibbler: (for info see https://electronjs.org/docs/tutorial/application-distribution) # # Obtain the appropriate Electron asset named above, from https://github.com/electron/electron/releases # Create a folder at scripts/electron_zipped and place the Electron asset in it # Run ./builder.py # # Note: later Electron versions work also, but with a couple minor glitches: # https://github.com/rooklift/nibbler/issues/140 os.chdir(os.path.dirname(os.path.realpath(__file__))) # Ensure we're in builder.py's directory. os.chdir("..") # Then come up one level. with open("src/package.json") as f: version = json.load(f)["version"] for key, value in zips.items(): # check if electron archives exist if not os.path.exists(value): print("Skipping {} build: {} not present.".format(key, value)) continue # make build directory build_dir = "scripts/dist/nibbler-{}-{}".format(version, key) os.makedirs(build_dir) # copy files build_res_dir = os.path.join(build_dir, "resources") shutil.copytree("res", build_res_dir) shutil.copytree("src", os.path.join(build_res_dir, "app")) # extract electron print("Extracting for {}...".format(key)) z = zipfile.ZipFile(value, "r") z.extractall(build_dir) z.close() # rename executable if os.path.exists(os.path.join(build_dir, "electron.exe")): os.rename(os.path.join(build_dir, "electron.exe"), os.path.join(build_dir, "nibbler.exe")) if os.path.exists(os.path.join(build_dir, "electron")): os.rename(os.path.join(build_dir, "electron"), os.path.join(build_dir, "nibbler")) ================================================ FILE: files/scripts/install.sh ================================================ #!/usr/bin/env bash set -e BASE_URL="https://github.com/rooklift/nibbler" # check curl if ! which curl 1>/dev/null 2>&1 ; then echo "Please install curl and make sure it's added to \$PATH" echo "Aborting" exit 1 fi # start echo "You are installing Nibbler" # get the latest release version VERSION=$(curl -fs -o /dev/null -w "%{redirect_url}" "${BASE_URL}/releases/latest" | xargs basename) echo "Latest release is ${VERSION}" ZIP_NAME="nibbler-${VERSION#v}-linux.zip" ZIP_URL="${BASE_URL}/releases/download/${VERSION}/${ZIP_NAME}" # create and enter temp dir TEMP_DIR=$(mktemp -d) cd "${TEMP_DIR}" # download echo "Downloading release from ${ZIP_URL}" if curl -fOL "${ZIP_URL}"; then echo "Successfully downloaded ${ZIP_NAME}" else echo "Failed to download ${ZIP_NAME}" echo "Exiting" exit 1 fi # extract echo "Extracting..." unzip -q "${ZIP_NAME}" echo "Successfully extracted Nibbler" UNZIPPED_NAME="${ZIP_NAME%.zip}" # prepare chmod +x "${UNZIPPED_NAME}/nibbler" mv "${UNZIPPED_NAME}/resources/"{nibbler.png,nibbler.svg,linux} ./ # check if already installed INSTALL_DIR="/opt/nibbler" if [[ -d "${INSTALL_DIR}" ]]; then echo "${INSTALL_DIR} already exists!" echo "It looks like there is an existing installation of Nibbler on your system" read -p "Would you like to overwrite it? [y/n]" -n 1 CONFIRM_INSTALL echo if ! [[ "$CONFIRM_INSTALL" =~ ^[Yy]$ ]]; then echo "Aborting" exit 1 fi fi # start install BIN_SYMLINK_PATH="/usr/local/bin/nibbler" DESKTOP_ENTRY_PATH="/usr/local/share/applications/nibbler.desktop" ICON_PNG_PATH="/usr/local/share/icons/hicolor/512x512/apps/nibbler.png" ICON_SVG_PATH="/usr/local/share/icons/hicolor/scalable/apps/nibbler.svg" echo "Installing Nibbler to ${INSTALL_DIR}" echo "Creating binary symlink at ${BIN_SYMLINK_PATH}" echo "Installing desktop entry to ${DESKTOP_ENTRY_PATH}" echo "Installing icons to ${ICON_PNG_PATH} and ${ICON_SVG_PATH}" echo "This will require sudo privilege." # remove old and make sure directories are created for FILE in "${INSTALL_DIR}" "${BIN_SYMLINK_PATH}" "${DESKTOP_ENTRY_PATH}" \ "${ICON_PNG_PATH}" "${ICON_SVG_PATH}"; do sudo rm -rf "$FILE" sudo mkdir -p $(dirname "$FILE") done # install new sudo mv "${UNZIPPED_NAME}" "${INSTALL_DIR}" sudo ln -s "${INSTALL_DIR}/nibbler" "${BIN_SYMLINK_PATH}" sed -i "s|^Exec=.*|Exec=nibbler --no-sandbox|" "linux/nibbler.desktop" sudo mv "linux/nibbler.desktop" "${DESKTOP_ENTRY_PATH}" sudo mv "nibbler.png" "${ICON_PNG_PATH}" sudo mv "nibbler.svg" "${ICON_SVG_PATH}" # done echo "Successfully installed Nibbler ${VERSION}" echo "You will be able to find Nibbler in your launcher shortly" ================================================ FILE: files/scripts/install_mac.sh ================================================ #!/bin/bash set -euo pipefail msg() { printf "=======================\n" printf "\033[93m%s\033[0m\n" "$1" } ok() { printf "\033[92m \u2714 %s\033[0m\n" "$1" } WORKDIR="/tmp/nibbler-install" VERSION="2.5.8" ELECTRON_VERSION="41.0.3" ARCH="$(uname -m)" case "$ARCH" in arm64) ELECTRON_ARCH="arm64" ;; x86_64) ELECTRON_ARCH="x64" ;; *) msg "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac # robustness fix... clean exit cleanup_on_failure() { local exit_code="$1" if [[ "$exit_code" -ne 0 ]]; then rm -rf "$WORKDIR" fi } trap 'cleanup_on_failure "$?"' EXIT rm -rf "$WORKDIR" mkdir -p "$WORKDIR" cd "$WORKDIR" msg "Downloading nibbler v${VERSION}..." curl -# -fL -O "https://github.com/rooklift/nibbler/archive/refs/tags/v${VERSION}.zip" unzip -q "v${VERSION}.zip" NIBBLER="nibbler-${VERSION}" [[ -d "$NIBBLER" ]] || { msg "Failed to fetch nibbler..."; exit 1; } ok "Fetched nibbler!" msg "Downloading electron v${ELECTRON_VERSION}..." ELECTRON_ZIP="electron-v${ELECTRON_VERSION}-darwin-${ELECTRON_ARCH}.zip" curl -# -fL -O "https://github.com/electron/electron/releases/download/v${ELECTRON_VERSION}/${ELECTRON_ZIP}" mkdir -p electron cd electron unzip -q "../${ELECTRON_ZIP}" [[ -d "$WORKDIR/electron/Electron.app" ]] || { msg "Failed to fetch electron..."; exit 1; } ok "Fetched electron!" msg "Assembling Nibbler.app..." APP="$WORKDIR/electron/Electron.app" APP_ROOT="$APP/Contents/Resources/app" rm -f "$APP/Contents/Resources/default_app.asar" rm -rf "$APP_ROOT" cp -R "$WORKDIR/nibbler-${VERSION}/files/src" "$APP_ROOT" PLIST="$APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName Nibbler" "$PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleName Nibbler" "$PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.rooklift.nibbler" "$PLIST" mv "$APP" "$WORKDIR/electron/Nibbler.app" ok "Built Nibbler.app!" APP_DIR="$WORKDIR/electron" APP="$APP_DIR/Nibbler.app" APPS_DIR="$HOME/Applications" APPS_APP="$APPS_DIR/Nibbler.app" read -r -p "Move Nibbler.app to ~/Applications and overwrite any existing copy? [Y/n] " MOVE_APP msg "Nice, here you go!" if [[ -z "$MOVE_APP" || "$MOVE_APP" =~ ^[Yy]$ ]]; then msg "Installing Nibbler.app to ~/Applications..." mkdir -p "$APPS_DIR" rm -rf "$APPS_APP" mv "$APP" "$APPS_DIR/" ok "Installed Nibbler.app to ${APPS_APP}!" open -R "$APPS_APP" else open "$APP_DIR" msg "IMPORTANT: Make sure to move app under Applications/ or ~/Applications!" fi ================================================ FILE: files/src/main.js ================================================ "use strict"; const electron = require("electron"); // The docs are a bit vague but it seems there's a limited timeframe // in which command line flags can be passed, so do this ASAP... electron.app.commandLine.appendSwitch("js-flags", "--expose_gc"); // Config... const config_io = require("./modules/config_io"); let config = config_io.load()[1]; // Do this early, it's a needed global. // disableHardwareAcceleration() needs to be called before the app is ready... let actually_disabled_hw_accel = false; if (config.disable_hw_accel) { try { electron.app.disableHardwareAcceleration(); actually_disabled_hw_accel = true; console.log("Hardware acceleration for Nibbler (GUI, not engine) disabled by config setting."); } catch (err) { console.log("Failed to disable hardware acceleration."); } } // Other requires... const alert = require("./modules/alert_main"); const custom_uci = require("./modules/custom_uci"); const engineconfig_io = require("./modules/engineconfig_io"); const messages = require("./modules/messages"); const path = require("path"); const running_as_electron = require("./modules/running_as_electron"); const stringify = require("./modules/stringify"); const translate = require("./modules/translate"); const url = require("url"); translate.register_startup_language(config.language); // We want sync save and open dialogs. In Electron 5 we could get these by calling // showSaveDialog or showOpenDialog without a callback, but in Electron 6 this no // longer works and we must call new functions. So find out if they exist... const save_dialog = electron.dialog.showSaveDialogSync || electron.dialog.showSaveDialog; const open_dialog = electron.dialog.showOpenDialogSync || electron.dialog.showOpenDialog; // Note that as the user adjusts menu items, our copy of the config will become // out of date. The renderer is responsible for having an up-to-date copy. let win; let menu = menu_build(); let menu_is_set = false; let have_sent_quit = false; let have_received_terminate = false; let loaded_engine = ""; let loaded_weights = ""; let loaded_evalfile = ""; let have_warned_hw_accel_setting = false; // Avoid a theoretical race by checking whether the ready event has already occurred, // otherwise set an event listener for it... if (electron.app.isReady()) { startup(); } else { electron.app.once("ready", () => { startup(); }); } // ---------------------------------------------------------------------------------- function startup() { let desired_zoomfactor = 1 / electron.screen.getPrimaryDisplay().scaleFactor; win = new electron.BrowserWindow({ width: config.width, height: config.height, backgroundColor: "#000000", resizable: true, show: false, useContentSize: true, webPreferences: { backgroundThrottling: false, contextIsolation: false, nodeIntegration: true, spellcheck: false, zoomFactor: desired_zoomfactor // Unreliable, see https://github.com/electron/electron/issues/10572 } }); win.once("ready-to-show", () => { try { win.webContents.setZoomFactor(desired_zoomfactor); // This seems to work, note issue 10572 above. } catch (err) { win.webContents.zoomFactor = desired_zoomfactor; // The method above "will be removed" in future. } win.show(); win.focus(); }); win.webContents.once("crashed", () => { alert(win, messages.renderer_crash); }); win.webContents.once("unresponsive", () => { alert(win, messages.renderer_hang); }); win.on("close", (event) => { // We used to use .once() but I suppose there's a race condition if two events happen rapidly. if (!have_received_terminate) { event.preventDefault(); // Only a "terminate" message from the Renderer should close the app. if (!have_sent_quit) { win.webContents.send("call", "quit"); // Renderer's "quit" method runs. It then sends "terminate" back. have_sent_quit = true; } // Create a setTimeout that will make the app close without the renderer's help if it takes too long (due to a crash)... setTimeout(() => { console.log("Renderer seems unresponsive, quitting anyway."); have_received_terminate = true; win.close(); }, 3000); } }); electron.ipcMain.on("terminate", () => { have_received_terminate = true; // Needed so the "close" handler (see above) knows to allow it. win.close(); }); electron.app.on("window-all-closed", () => { electron.app.quit(); }); electron.ipcMain.once("renderer_ready", () => { if (actually_disabled_hw_accel) { win.webContents.send("call", { fn: "console", args: ["Hardware acceleration is disabled."], }); } // Open a file via command line. We must wait until the renderer has properly loaded before we do this. // While it might seem like we could do this after "ready-to-show" I'm not 100% sure that the renderer // will have fully loaded when that fires. let filename = ""; if (running_as_electron()) { if (process.argv.length > 2) { filename = process.argv[process.argv.length - 1]; } } else { if (process.argv.length > 1) { filename = process.argv[process.argv.length - 1]; } } if (filename !== "") { win.webContents.send("call", { fn: "open", args: [filename] }); } }); electron.ipcMain.on("alert", (event, msg) => { alert(win, msg); }); electron.ipcMain.on("set_title", (event, msg) => { win.setTitle(msg); }); electron.ipcMain.on("web_link", (event, msg) => { electron.shell.openExternal(msg); }); electron.ipcMain.on("ack_engine", (event, msg) => { loaded_engine = msg; set_one_check(msg ? true : false, "Engine", "Choose engine..."); }); electron.ipcMain.on("ack_logfile", (event, msg) => { set_one_check(msg ? true : false, "Dev", "Logging", "Use logfile..."); }); electron.ipcMain.on("ack_book", (event, msg) => { set_one_check(msg === "polyglot", "Play", "Use Polyglot book..."); set_one_check(msg === "pgn", "Play", "Use PGN book..."); }); electron.ipcMain.on("ack_node_limit", (event, msg) => { set_checks("Engine", "Limit - normal", msg); }); electron.ipcMain.on("ack_special_node_limit", (event, msg) => { set_checks("Engine", "Limit - auto-eval / play", msg); }); electron.ipcMain.on("ack_limit_by_time", (event, msg) => { set_one_check(msg ? true : false, "Engine", "Limit by time instead of nodes"); }); electron.ipcMain.on("ack_setoption", (event, msg) => { // These are received whenever the renderer actually sends a setoption UCI command. // But we also sometimes query some option and get a response indicating what the // last value we sent was, or "" if not applicable. // Expect msg.key to be a lowercase string // Expect msg.val to be a string, possibly "" (can use the fact that "" is false-ish) // REMEMBER TO UPDATE engine.js GUI_WANTS_TO_KNOW const WHEN THINGS ARE ADDED... switch (msg.key) { case "weightsfile": loaded_weights = msg.val; set_one_check(msg.val ? true : false, "Engine", "Weights", "Lc0 WeightsFile..."); break; case "evalfile": loaded_evalfile = msg.val; set_one_check(msg.val ? true : false, "Engine", "Weights", "Stockfish EvalFile..."); break; case "syzygypath": set_one_check(msg.val ? true : false, "Engine", "Choose Syzygy path..."); break; case "backend": set_checks("Engine", "Backend", msg.val); break; case "threads": set_checks("Engine", "Threads", msg.val); break; case "hash": let mb = parseInt(msg.val, 10); if (Number.isNaN(mb) === false) { let gb = Math.floor(mb / 1024); set_checks("Engine", "Hash", `${gb} GB`); } else { set_checks("Engine", "Hash", ""); // i.e. clear all } break; case "multipv": set_checks("Engine", "MultiPV", msg.val); // If it's "500" it will clear all. break; case "temperature": // Sketchy because there are equivalent representations. if (msg.val === "0" || msg.val === "0.0") { set_checks("Play", "Temperature", "0"); } else if (msg.val === "1" || msg.val === "1.0") { set_checks("Play", "Temperature", "1.0"); } else { set_checks("Play", "Temperature", msg.val); } break; case "tempdecaymoves": // Not so sketchy because it should be a string of an integer. set_checks("Play", "Temp Decay Moves", msg.val === "0" ? "Infinite" : msg.val); break; case "contemptmode": // All the menu items are different from the UCI values... if (msg.val === "white_side_analysis") { set_checks("Engine", "Contempt Mode", "White analysis"); } else if (msg.val === "black_side_analysis") { set_checks("Engine", "Contempt Mode", "Black analysis"); } else { set_checks("Engine", "Contempt Mode", msg.val); } break; case "contempt": set_checks("Engine", "Contempt", msg.val); break; case "wdlcalibrationelo": set_checks("Engine", "WDL Calibration Elo", msg.val === "0" ? "Use default WDL" : msg.val); break; case "wdlevalobjectivity": if (msg.val === "1") { set_checks("Engine", "WDL Eval Objectivity", "Yes"); } else if (msg.val === "0") { set_checks("Engine", "WDL Eval Objectivity", "No"); } else { set_checks("Engine", "WDL Eval Objectivity", msg.val); } break; case "scoretype": set_checks("Engine", "Score Type", msg.val); break; // REMEMBER TO UPDATE engine.js GUI_WANTS_TO_KNOW const WHEN THINGS ARE ADDED... } }); electron.Menu.setApplicationMenu(menu); menu_is_set = true; // Actually load the page last, I guess, so the event handlers above are already set up. // Send some needed info as a query. let query = {}; query.user_data_path = electron.app.getPath("userData"); query.zoomfactor = desired_zoomfactor; win.loadFile( path.join(__dirname, "nibbler.html"), {query: query} ); } function menu_build() { const million = 1000 * 1000; const billion = 1000 * million; let scriptlist_in_menu = []; let template = [ { label: translate.t("File"), submenu: [ { label: translate.t("About"), click: () => { let s = `Nibbler ${electron.app.getVersion()} in Electron ${process.versions.electron}\n\n`; s += `Engine: ${loaded_engine}\nWeights: ${loaded_weights || loaded_evalfile || ""}`; alert(win, s); } }, { type: "separator" }, { label: translate.t("New game"), accelerator: "CommandOrControl+N", click: () => { win.webContents.send("call", "new_game"); } }, { label: translate.t("New 960 game"), accelerator: "CommandOrControl+Shift+N", click: () => { win.webContents.send("call", "new_960"); } }, { type: "separator" }, { label: translate.t("Open PGN..."), accelerator: "CommandOrControl+O", click: () => { let files = open_dialog(win, { defaultPath: config.pgn_dialog_folder, properties: ["openFile"], filters: [{name: "PGN", extensions: ["pgn"]}, {name: "All files", extensions: ["*"]}] }); if (Array.isArray(files) && files.length > 0) { let file = files[0]; win.webContents.send("call", { fn: "open", args: [file] }); // Save the dir as the new default dir, in both processes. config.pgn_dialog_folder = path.dirname(file); win.webContents.send("set", {pgn_dialog_folder: path.dirname(file)}); } } }, { type: "separator" }, { label: translate.t("Load FEN / PGN from clipboard"), accelerator: "CommandOrControl+Shift+V", click: () => { win.webContents.send("call", { fn: "load_fen_or_pgn_from_string", args: [electron.clipboard.readText()] }); } }, { type: "separator" }, { label: translate.t("Save this game..."), accelerator: "CommandOrControl+S", click: () => { if (config.save_enabled !== true) { // Note: exact test for true, not just any truthy value alert(win, messages.save_not_enabled); return; } let file = save_dialog(win, { defaultPath: config.pgn_dialog_folder, filters: [{name: "PGN", extensions: ["pgn"]}, {name: "All files", extensions: ["*"]}] }); if (typeof file === "string" && file.length > 0) { win.webContents.send("call", { fn: "save", args: [file] }); // Save the dir as the new default dir, in both processes. config.pgn_dialog_folder = path.dirname(file); win.webContents.send("set", {pgn_dialog_folder: path.dirname(file)}); } } }, { label: translate.t("Write PGN to clipboard"), accelerator: "CommandOrControl+K", click: () => { win.webContents.send("call", "pgn_to_clipboard"); } }, { label: translate.t("PGN saved statistics"), submenu: [ { label: translate.t("EV"), type: "checkbox", checked: config.pgn_ev, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_ev"], }); } }, { label: translate.t("Centipawns"), type: "checkbox", checked: config.pgn_cp, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_cp"], }); } }, { type: "separator" }, { label: translate.t("N (%)"), type: "checkbox", checked: config.pgn_n, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_n"], }); } }, { label: translate.t("N (absolute)"), type: "checkbox", checked: config.pgn_n_abs, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_n_abs"], }); } }, { label: translate.t("...out of total"), type: "checkbox", checked: config.pgn_of_n, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_of_n"], }); } }, { label: translate.t("Depth (A/B only)"), type: "checkbox", checked: config.pgn_depth, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_depth"], }); } }, { type: "separator" }, { label: translate.t("P"), type: "checkbox", checked: config.pgn_p, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_p"], }); } }, { label: translate.t("V"), type: "checkbox", checked: config.pgn_v, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_v"], }); } }, { type: "separator" }, { label: translate.t("Q"), type: "checkbox", checked: config.pgn_q, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_q"], }); } }, { label: translate.t("U"), type: "checkbox", checked: config.pgn_u, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_u"], }); } }, { label: translate.t("S"), type: "checkbox", checked: config.pgn_s, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_s"], }); } }, { type: "separator" }, { label: translate.t("M"), type: "checkbox", checked: config.pgn_m, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_m"], }); } }, { label: translate.t("WDL"), type: "checkbox", checked: config.pgn_wdl, click: () => { win.webContents.send("call", { fn: "toggle", args: ["pgn_wdl"], }); } }, ] }, { type: "separator" }, { label: translate.t("Cut"), accelerator: "CommandOrControl+X", role: "cut", }, { label: translate.t("Copy"), accelerator: "CommandOrControl+C", role: "copy", }, { label: translate.t("Paste"), accelerator: "CommandOrControl+V", role: "paste", }, { type: "separator" }, { label: translate.t("Quit"), // Presumably calls electron.app.quit(), which tries to accelerator: "CommandOrControl+Q", // close all windows, and quits iff it succeeds (which role: "quit" // it won't, because we prevent the initial close...) }, ] }, { label: translate.t("Tree"), submenu: [ { label: translate.t("Play engine choice"), submenu: [ { label: translate.t("1st"), accelerator: "F1", click: () => { win.webContents.send("call", { fn: "play_info_index", args: [0] }); } }, { label: translate.t("2nd"), accelerator: "F2", click: () => { win.webContents.send("call", { fn: "play_info_index", args: [1] }); } }, { label: translate.t("3rd"), accelerator: "F3", click: () => { win.webContents.send("call", { fn: "play_info_index", args: [2] }); } }, { label: translate.t("4th"), accelerator: "F4", click: () => { win.webContents.send("call", { fn: "play_info_index", args: [3] }); } }, ] }, { type: "separator" }, { label: translate.t("Root"), accelerator: "Home", click: () => { win.webContents.send("call", "goto_root"); } }, { label: translate.t("End"), accelerator: "End", click: () => { win.webContents.send("call", "goto_end"); } }, { label: translate.t("Backward"), accelerator: "Left", click: () => { win.webContents.send("call", "prev"); } }, { label: translate.t("Forward"), accelerator: "Right", click: () => { win.webContents.send("call", "next"); } }, { label: translate.t("Previous sibling"), accelerator: "Up", click: () => { win.webContents.send("call", "previous_sibling"); } }, { label: translate.t("Next sibling"), accelerator: "Down", click: () => { win.webContents.send("call", "next_sibling"); } }, { type: "separator" }, { label: translate.t("Return to main line"), accelerator: "CommandOrControl+R", click: () => { win.webContents.send("call", "return_to_main_line"); } }, { label: translate.t("Promote line to main line"), accelerator: "CommandOrControl+L", click: () => { win.webContents.send("call", "promote_to_main_line"); } }, { label: translate.t("Promote line by 1 level"), accelerator: "CommandOrControl+Up", click: () => { win.webContents.send("call", "promote"); } }, { type: "separator" }, { label: translate.t("Delete node"), accelerator: "CommandOrControl+Backspace", click: () => { win.webContents.send("call", "delete_node"); } }, { label: translate.t("Delete children"), click: () => { win.webContents.send("call", "delete_children"); } }, { label: translate.t("Delete siblings"), click: () => { win.webContents.send("call", "delete_siblings"); } }, { type: "separator" }, { label: translate.t("Delete ALL other lines"), click: () => { win.webContents.send("call", "delete_other_lines"); } }, { type: "separator" }, { label: translate.t("Show PGN games list"), accelerator: "CommandOrControl+P", click: () => { win.webContents.send("call", "show_pgn_chooser"); } }, { label: translate.t("Escape"), accelerator: "Escape", click: () => { win.webContents.send("call", "escape"); } }, ] }, { label: translate.t("Analysis"), submenu: [ { label: translate.t("Go"), accelerator: "CommandOrControl+G", click: () => { win.webContents.send("call", { fn: "set_behaviour", args: ["analysis_free"], }); } }, { label: translate.t("Go and lock engine"), accelerator: "CommandOrControl+Shift+G", click: () => { win.webContents.send("call", { fn: "set_behaviour", args: ["analysis_locked"], }); } }, { label: translate.t("Return to locked position"), click: () => { win.webContents.send("call", "return_to_lock"); } }, { type: "separator" }, { label: translate.t("Halt"), accelerator: "CommandOrControl+H", click: () => { win.webContents.send("call", { fn: "set_behaviour", args: ["halt"], }); } }, { type: "separator" }, { label: translate.t("Auto-evaluate line"), accelerator: "F12", click: () => { win.webContents.send("call", { fn: "set_behaviour", args: ["auto_analysis"] }); } }, { label: translate.t("Auto-evaluate line, backwards"), accelerator: "Shift+F12", click: () => { win.webContents.send("call", { fn: "set_behaviour", args: ["back_analysis"] }); } }, { type: "separator" }, { label: translate.t("Show focus (searchmoves) buttons"), type: "checkbox", checked: config.searchmoves_buttons, click: () => { win.webContents.send("call", { fn: "toggle", args: ["searchmoves_buttons"], }); } }, { label: translate.t("Clear focus"), click: () => { win.webContents.send("call", "clear_searchmoves"); } }, { label: translate.t("Invert focus"), accelerator: "CommandOrControl+I", click: () => { win.webContents.send("call", "invert_searchmoves"); } }, { type: "separator" }, { label: translate.t("Winrate POV"), submenu: [ { label: translate.t("Current"), type: "checkbox", checked: config.ev_pov !== "w" && config.ev_pov !== "b", click: () => { set_checks("Analysis", "Winrate POV", "Current"); win.webContents.send("set", {ev_pov: null}); } }, { label: translate.t("White"), type: "checkbox", checked: config.ev_pov === "w", click: () => { set_checks("Analysis", "Winrate POV", "White"); win.webContents.send("set", {ev_pov: "w"}); } }, { label: translate.t("Black"), type: "checkbox", checked: config.ev_pov === "b", click: () => { set_checks("Analysis", "Winrate POV", "Black"); win.webContents.send("set", {ev_pov: "b"}); } }, ] }, { label: translate.t("Centipawn POV"), submenu: [ { label: translate.t("Current"), type: "checkbox", checked: config.cp_pov !== "w" && config.cp_pov !== "b", click: () => { set_checks("Analysis", "Centipawn POV", "Current"); win.webContents.send("set", {cp_pov: null}); } }, { label: translate.t("White"), type: "checkbox", checked: config.cp_pov === "w", click: () => { set_checks("Analysis", "Centipawn POV", "White"); win.webContents.send("set", {cp_pov: "w"}); } }, { label: translate.t("Black"), type: "checkbox", checked: config.cp_pov === "b", click: () => { set_checks("Analysis", "Centipawn POV", "Black"); win.webContents.send("set", {cp_pov: "b"}); } }, ] }, { label: translate.t("Win / draw / loss POV"), submenu: [ { label: translate.t("Current"), type: "checkbox", checked: config.wdl_pov !== "w" && config.wdl_pov !== "b", click: () => { set_checks("Analysis", "Win / draw / loss POV", "Current"); win.webContents.send("set", {wdl_pov: null}); } }, { label: translate.t("White"), type: "checkbox", checked: config.wdl_pov === "w", click: () => { set_checks("Analysis", "Win / draw / loss POV", "White"); win.webContents.send("set", {wdl_pov: "w"}); } }, { label: translate.t("Black"), type: "checkbox", checked: config.wdl_pov === "b", click: () => { set_checks("Analysis", "Win / draw / loss POV", "Black"); win.webContents.send("set", {wdl_pov: "b"}); } }, ] }, { type: "separator" }, { label: translate.t("PV clicks"), submenu: [ { label: translate.t("Do nothing"), type: "checkbox", checked: config.pv_click_event === 0, click: () => { set_checks("Analysis", "PV clicks", "Do nothing"); win.webContents.send("set", {pv_click_event: 0}); } }, { label: translate.t("Go there"), type: "checkbox", checked: config.pv_click_event === 1, click: () => { set_checks("Analysis", "PV clicks", "Go there"); win.webContents.send("set", {pv_click_event: 1}); } }, { label: translate.t("Add to tree"), type: "checkbox", checked: config.pv_click_event === 2, click: () => { set_checks("Analysis", "PV clicks", "Add to tree"); win.webContents.send("set", {pv_click_event: 2}); } }, ] }, { type: "separator" }, { label: translate.t("Write infobox to clipboard"), click: () => { win.webContents.send("call", "infobox_to_clipboard"); } }, { type: "separator" }, { label: translate.t("Forget all analysis"), accelerator: "CommandOrControl+.", click: () => { win.webContents.send("call", "forget_analysis"); } }, ] }, { label: translate.t("Display"), submenu: [ { label: translate.t("Flip board"), accelerator: "CommandOrControl+F", click: () => { win.webContents.send("call", { fn: "toggle", args: ["flip"], }); } }, { type: "separator" }, { label: translate.t("Arrows"), type: "checkbox", checked: config.arrows_enabled, click: () => { win.webContents.send("call", { fn: "toggle", args: ["arrows_enabled"], }); } }, { label: translate.t("Piece-click spotlight"), type: "checkbox", checked: config.click_spotlight, click: () => { win.webContents.send("call", { fn: "toggle", args: ["click_spotlight"], }); } }, { label: translate.t("Always show actual move (if known)"), type: "checkbox", checked: config.next_move_arrow, click: () => { win.webContents.send("call", { fn: "toggle", args: ["next_move_arrow"], }); } }, { label: translate.t("...with unique colour"), type: "checkbox", checked: config.next_move_unique_colour, click: () => { win.webContents.send("call", { fn: "toggle", args: ["next_move_unique_colour"], }); } }, { label: translate.t("...with outline"), type: "checkbox", checked: config.next_move_outline, click: () => { win.webContents.send("call", { fn: "toggle", args: ["next_move_outline"], }); } }, { type: "separator" }, { label: translate.t("Arrowhead type"), submenu: [ { label: translate.t("Winrate"), type: "checkbox", checked: config.arrowhead_type === 0, accelerator: "F5", click: () => { set_checks("Display", "Arrowhead type", "Winrate"); win.webContents.send("set", {arrowhead_type: 0}); } }, { label: translate.t("Node %"), type: "checkbox", checked: config.arrowhead_type === 1, accelerator: "F6", click: () => { set_checks("Display", "Arrowhead type", "Node %"); win.webContents.send("set", {arrowhead_type: 1}); } }, { label: translate.t("Policy"), type: "checkbox", checked: config.arrowhead_type === 2, accelerator: "F7", click: () => { set_checks("Display", "Arrowhead type", "Policy"); win.webContents.send("set", {arrowhead_type: 2}); } }, { label: translate.t("MultiPV rank"), type: "checkbox", checked: config.arrowhead_type === 3, accelerator: "F8", click: () => { set_checks("Display", "Arrowhead type", "MultiPV rank"); win.webContents.send("set", {arrowhead_type: 3}); } }, { label: translate.t("Moves Left Head"), type: "checkbox", checked: config.arrowhead_type === 4, click: () => { set_checks("Display", "Arrowhead type", "Moves Left Head"); win.webContents.send("set", {arrowhead_type: 4}); } }, ] }, { type: "separator" }, { label: translate.t("Arrow filter (Lc0)"), submenu: [ { label: translate.t("All moves"), type: "checkbox", checked: config.arrow_filter_type === "all", click: () => { set_checks("Display", "Arrow filter (Lc0)", "All moves"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["all", 0], }); } }, { label: translate.t("Top move"), type: "checkbox", checked: config.arrow_filter_type === "top", click: () => { set_checks("Display", "Arrow filter (Lc0)", "Top move"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["top", 0], }); } }, { type: "separator" }, { label: translate.t("N > 0.5%"), type: "checkbox", checked: config.arrow_filter_type === "N" && config.arrow_filter_value === 0.005, click: () => { set_checks("Display", "Arrow filter (Lc0)", "N > 0.5%"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["N", 0.005], }); } }, { label: translate.t("N > 1%"), type: "checkbox", checked: config.arrow_filter_type === "N" && config.arrow_filter_value === 0.01, click: () => { set_checks("Display", "Arrow filter (Lc0)", "N > 1%"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["N", 0.01], }); } }, { label: translate.t("N > 2%"), type: "checkbox", checked: config.arrow_filter_type === "N" && config.arrow_filter_value === 0.02, click: () => { set_checks("Display", "Arrow filter (Lc0)", "N > 2%"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["N", 0.02], }); } }, { label: translate.t("N > 3%"), type: "checkbox", checked: config.arrow_filter_type === "N" && config.arrow_filter_value === 0.03, click: () => { set_checks("Display", "Arrow filter (Lc0)", "N > 3%"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["N", 0.03], }); } }, { label: translate.t("N > 4%"), type: "checkbox", checked: config.arrow_filter_type === "N" && config.arrow_filter_value === 0.04, click: () => { set_checks("Display", "Arrow filter (Lc0)", "N > 4%"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["N", 0.04], }); } }, { label: translate.t("N > 5%"), type: "checkbox", checked: config.arrow_filter_type === "N" && config.arrow_filter_value === 0.05, click: () => { set_checks("Display", "Arrow filter (Lc0)", "N > 5%"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["N", 0.05], }); } }, { label: translate.t("N > 10%"), type: "checkbox", checked: config.arrow_filter_type === "N" && config.arrow_filter_value === 0.1, click: () => { set_checks("Display", "Arrow filter (Lc0)", "N > 10%"); win.webContents.send("call", { fn: "set_arrow_filter", args: ["N", 0.1], }); } } ] }, { label: translate.t("Arrow filter (others)"), submenu: [ { label: translate.t("Diff < 15%"), type: "checkbox", checked: config.ab_filter_threshold === 0.15, click: () => { set_checks("Display", "Arrow filter (others)", "Diff < 15%"); win.webContents.send("set", {ab_filter_threshold: 0.15}); } }, { label: translate.t("Diff < 10%"), type: "checkbox", checked: config.ab_filter_threshold === 0.1, click: () => { set_checks("Display", "Arrow filter (others)", "Diff < 10%"); win.webContents.send("set", {ab_filter_threshold: 0.1}); } }, { label: translate.t("Diff < 5%"), type: "checkbox", checked: config.ab_filter_threshold === 0.05, click: () => { set_checks("Display", "Arrow filter (others)", "Diff < 5%"); win.webContents.send("set", {ab_filter_threshold: 0.05}); } }, ] }, { type: "separator" }, { label: translate.t("Infobox stats"), submenu: [ { label: translate.t("Centipawns"), accelerator: "CommandOrControl+T", type: "checkbox", checked: config.show_cp, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_cp"], }); } }, { type: "separator" }, { label: translate.t("N - nodes (%)"), type: "checkbox", checked: config.show_n, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_n"], }); } }, { label: translate.t("N - nodes (absolute)"), type: "checkbox", checked: config.show_n_abs, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_n_abs"], }); } }, { label: translate.t("Depth (A/B only)"), type: "checkbox", checked: config.show_depth, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_depth"], }); } }, { type: "separator" }, { label: translate.t("P - policy"), type: "checkbox", checked: config.show_p, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_p"], }); } }, { label: translate.t("V - static evaluation"), type: "checkbox", checked: config.show_v, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_v"], }); } }, { type: "separator" }, { label: translate.t("Q - evaluation"), type: "checkbox", checked: config.show_q, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_q"], }); } }, { label: translate.t("U - uncertainty"), type: "checkbox", checked: config.show_u, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_u"], }); } }, { label: translate.t("S - search priority"), type: "checkbox", checked: config.show_s, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_s"], }); } }, { type: "separator" }, { label: translate.t("M - moves left"), type: "checkbox", checked: config.show_m, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_m"], }); } }, { label: translate.t("WDL - win / draw / loss"), type: "checkbox", checked: config.show_wdl, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_wdl"], }); } }, { type: "separator" }, { label: translate.t("Linebreak before stats"), type: "checkbox", checked: config.infobox_stats_newline, click: () => { win.webContents.send("call", { fn: "toggle", args: ["infobox_stats_newline"], }); } } ] }, { label: translate.t("PV move numbers"), type: "checkbox", checked: config.infobox_pv_move_numbers, click: () => { win.webContents.send("call", { fn: "toggle", args: ["infobox_pv_move_numbers"], }); } }, { type: "separator" }, { label: translate.t("Online API"), submenu: [ { label: translate.t("None"), type: "checkbox", checked: typeof config.looker_api !== "string", click: () => { set_checks("Display", "Online API", "None"); win.webContents.send("call", { fn: "set_looker_api", args: [null] }); } }, { type: "separator" }, { label: translate.t("ChessDB.cn evals"), type: "checkbox", checked: config.looker_api === "chessdbcn", click: () => { set_checks("Display", "Online API", "ChessDB.cn evals"); win.webContents.send("call", { fn: "set_looker_api", args: ["chessdbcn"] }); } }, { type: "separator" }, { label: translate.t("Lichess results (masters)"), type: "checkbox", checked: config.looker_api === "lichess_masters", click: () => { set_checks("Display", "Online API", "Lichess results (masters)"); win.webContents.send("call", { fn: "set_looker_api", args: ["lichess_masters"] }); } }, { label: translate.t("Lichess results (plebs)"), type: "checkbox", checked: config.looker_api === "lichess_plebs", click: () => { set_checks("Display", "Online API", "Lichess results (plebs)"); win.webContents.send("call", { fn: "set_looker_api", args: ["lichess_plebs"] }); } }, { type: "separator" }, { label: translate.t("Set Lichess API token"), click : () => { win.webContents.send("call", { fn: "show_config_item_editor", args: ["lichess_token", "https://lichess.org/account/oauth/token/create", "Acquire a token here (no need for specific permissions)"] }); } }, ] }, { label: translate.t("Allow API after move 25"), type: "checkbox", checked: config.look_past_25, click: () => { win.webContents.send("call", { fn: "toggle", args: ["look_past_25"], }); } }, { type: "separator" }, { label: translate.t("Draw PV on mouseover"), accelerator: "CommandOrControl+D", type: "checkbox", checked: config.hover_draw, click: () => { win.webContents.send("call", { fn: "toggle", args: ["hover_draw"], }); } }, { label: translate.t("Draw PV method"), submenu: [ { label: translate.t("Animate"), type: "checkbox", checked: config.hover_method === 0, click: () => { set_checks("Display", "Draw PV method", "Animate"); win.webContents.send("set", {hover_method: 0}); } }, { label: translate.t("Single move"), type: "checkbox", checked: config.hover_method === 1, click: () => { set_checks("Display", "Draw PV method", "Single move"); win.webContents.send("set", {hover_method: 1}); } }, { label: translate.t("Final position"), type: "checkbox", checked: config.hover_method === 2, click: () => { set_checks("Display", "Draw PV method", "Final position"); win.webContents.send("set", {hover_method: 2}); } }, ] }, { type: "separator" }, { label: translate.t("Pieces"), submenu: [ { label: translate.t("Choose pieces folder..."), click: () => { let folders = open_dialog(win, { defaultPath: config.pieces_dialog_folder, properties: ["openDirectory"] }); if (Array.isArray(folders) && folders.length > 0) { let folder = folders[0]; win.webContents.send("call", { fn: "change_piece_set", args: [folder] }); // Save the dir as the new default dir, in both processes. config.pieces_dialog_folder = path.dirname(folder); win.webContents.send("set", {pieces_dialog_folder: path.dirname(folder)}); } } }, { label: translate.t("Default"), click: () => { win.webContents.send("call", { fn: "change_piece_set", args: [null] }); } }, { type: "separator" }, { label: translate.t("About custom pieces"), click: () => { alert(win, messages.about_custom_pieces); } } ] }, { label: translate.t("Background"), submenu: [ { label: translate.t("Choose background image..."), click: () => { let files = open_dialog(win, { defaultPath: config.background_dialog_folder, properties: ["openFile"] }); if (Array.isArray(files) && files.length > 0) { let file = files[0]; win.webContents.send("call", { fn: "change_background", args: [file] }); // Save the dir as the new default dir, in both processes. config.background_dialog_folder = path.dirname(file); win.webContents.send("set", {background_dialog_folder: path.dirname(file)}); } } }, { label: translate.t("Default"), click: () => { win.webContents.send("call", { fn: "change_background", args: [null] }); } }, ] }, { type: "separator" }, { label: translate.t("Book frequency arrows"), type: "checkbox", checked: config.book_explorer, // But this is never saved in the config file. click: () => { win.webContents.send("call", { fn: "toggle", args: ["book_explorer"] // The hub will automatically turn off lichess weights mode. }); set_one_check(false, "Display", "Lichess frequency arrows"); } }, { label: translate.t("Lichess frequency arrows"), type: "checkbox", accelerator: "CommandOrControl+E", checked: config.lichess_explorer, // But this is never saved in the config file. click: () => { win.webContents.send("call", { fn: "toggle", args: ["lichess_explorer"] // The hub will automatically turn off book weights mode. }); set_one_check(false, "Display", "Book frequency arrows"); } }, ] }, { label: translate.t("Sizes"), submenu: [ { label: translate.t("Infobox font"), submenu: [ { label: "32", type: "checkbox", checked: config.info_font_size === 32, click: () => { set_checks("Sizes", "Infobox font", "32"); win.webContents.send("call", { fn: "set_info_font_size", args: [32], }); } }, { label: "28", type: "checkbox", checked: config.info_font_size === 28, click: () => { set_checks("Sizes", "Infobox font", "28"); win.webContents.send("call", { fn: "set_info_font_size", args: [28], }); } }, { label: "24", type: "checkbox", checked: config.info_font_size === 24, click: () => { set_checks("Sizes", "Infobox font", "24"); win.webContents.send("call", { fn: "set_info_font_size", args: [24], }); } }, { label: "20", type: "checkbox", checked: config.info_font_size === 20, click: () => { set_checks("Sizes", "Infobox font", "20"); win.webContents.send("call", { fn: "set_info_font_size", args: [20], }); } }, { label: "18", type: "checkbox", checked: config.info_font_size === 18, click: () => { set_checks("Sizes", "Infobox font", "18"); win.webContents.send("call", { fn: "set_info_font_size", args: [18], }); } }, { label: "16", type: "checkbox", checked: config.info_font_size === 16, click: () => { set_checks("Sizes", "Infobox font", "16"); win.webContents.send("call", { fn: "set_info_font_size", args: [16], }); } }, ] }, { label: translate.t("Move history font"), submenu: [ { label: "32", type: "checkbox", checked: config.pgn_font_size === 32, click: () => { set_checks("Sizes", "Move history font", "32"); win.webContents.send("call", { fn: "set_pgn_font_size", args: [32], }); } }, { label: "28", type: "checkbox", checked: config.pgn_font_size === 28, click: () => { set_checks("Sizes", "Move history font", "28"); win.webContents.send("call", { fn: "set_pgn_font_size", args: [28], }); } }, { label: "24", type: "checkbox", checked: config.pgn_font_size === 24, click: () => { set_checks("Sizes", "Move history font", "24"); win.webContents.send("call", { fn: "set_pgn_font_size", args: [24], }); } }, { label: "20", type: "checkbox", checked: config.pgn_font_size === 20, click: () => { set_checks("Sizes", "Move history font", "20"); win.webContents.send("call", { fn: "set_pgn_font_size", args: [20], }); } }, { label: "18", type: "checkbox", checked: config.pgn_font_size === 18, click: () => { set_checks("Sizes", "Move history font", "18"); win.webContents.send("call", { fn: "set_pgn_font_size", args: [18], }); } }, { label: "16", type: "checkbox", checked: config.pgn_font_size === 16, click: () => { set_checks("Sizes", "Move history font", "16"); win.webContents.send("call", { fn: "set_pgn_font_size", args: [16], }); } }, ] }, { type: "separator" }, { label: translate.t("Board"), submenu: [ { label: "1280", type: "checkbox", checked: config.board_size === 1280, click: () => { set_checks("Sizes", "Board", "1280"); win.webContents.send("call", { fn: "set_board_size", args: [1280], }); } }, { label: "1120", type: "checkbox", checked: config.board_size === 1120, click: () => { set_checks("Sizes", "Board", "1120"); win.webContents.send("call", { fn: "set_board_size", args: [1120], }); } }, { label: "960", type: "checkbox", checked: config.board_size === 960, click: () => { set_checks("Sizes", "Board", "960"); win.webContents.send("call", { fn: "set_board_size", args: [960], }); } }, { label: "800", type: "checkbox", checked: config.board_size === 800, click: () => { set_checks("Sizes", "Board", "800"); win.webContents.send("call", { fn: "set_board_size", args: [800], }); } }, { label: "640", type: "checkbox", checked: config.board_size === 640, click: () => { set_checks("Sizes", "Board", "640"); win.webContents.send("call", { fn: "set_board_size", args: [640], }); } }, { label: "576", type: "checkbox", checked: config.board_size === 576, click: () => { set_checks("Sizes", "Board", "576"); win.webContents.send("call", { fn: "set_board_size", args: [576], }); } }, { label: "512", type: "checkbox", checked: config.board_size === 512, click: () => { set_checks("Sizes", "Board", "512"); win.webContents.send("call", { fn: "set_board_size", args: [512], }); } }, { label: "480", type: "checkbox", checked: config.board_size === 480, click: () => { set_checks("Sizes", "Board", "480"); win.webContents.send("call", { fn: "set_board_size", args: [480], }); } }, { label: "448", type: "checkbox", checked: config.board_size === 448, click: () => { set_checks("Sizes", "Board", "448"); win.webContents.send("call", { fn: "set_board_size", args: [448], }); } }, { label: "416", type: "checkbox", checked: config.board_size === 416, click: () => { set_checks("Sizes", "Board", "416"); win.webContents.send("call", { fn: "set_board_size", args: [416], }); } }, ] }, { label: translate.t("Arrows"), submenu: [ { label: translate.t("Giant"), click: () => { win.webContents.send("call", { fn: "set_arrow_size", args: [24, 32, 40] }); } }, { label: translate.t("Large"), click: () => { win.webContents.send("call", { fn: "set_arrow_size", args: [16, 24, 32] }); } }, { label: translate.t("Medium"), click: () => { win.webContents.send("call", { fn: "set_arrow_size", args: [12, 18, 24] }); } }, { label: translate.t("Small"), click: () => { win.webContents.send("call", { fn: "set_arrow_size", args: [8, 12, 18] }); } }, ] }, { type: "separator" }, { label: translate.t("Graph"), submenu: [ { label: "192", type: "checkbox", checked: config.graph_height === 192, click: () => { set_checks("Sizes", "Graph", "192"); win.webContents.send("call", { fn: "set_graph_height", args: [192], }); } }, { label: "160", type: "checkbox", checked: config.graph_height === 160, click: () => { set_checks("Sizes", "Graph", "160"); win.webContents.send("call", { fn: "set_graph_height", args: [160], }); } }, { label: "128", type: "checkbox", checked: config.graph_height === 128, click: () => { set_checks("Sizes", "Graph", "128"); win.webContents.send("call", { fn: "set_graph_height", args: [128], }); } }, { label: "96", type: "checkbox", checked: config.graph_height === 96, click: () => { set_checks("Sizes", "Graph", "96"); win.webContents.send("call", { fn: "set_graph_height", args: [96], }); } }, { label: "64", type: "checkbox", checked: config.graph_height === 64, click: () => { set_checks("Sizes", "Graph", "64"); win.webContents.send("call", { fn: "set_graph_height", args: [64], }); } }, { label: "48", type: "checkbox", checked: config.graph_height === 48, click: () => { set_checks("Sizes", "Graph", "48"); win.webContents.send("call", { fn: "set_graph_height", args: [48], }); } }, { label: "32", type: "checkbox", checked: config.graph_height === 32, click: () => { set_checks("Sizes", "Graph", "32"); win.webContents.send("call", { fn: "set_graph_height", args: [32], }); } }, { label: "0", type: "checkbox", checked: config.graph_height === 0, click: () => { set_checks("Sizes", "Graph", "0"); win.webContents.send("call", { fn: "set_graph_height", args: [0], }); } }, ] }, { label: translate.t("Graph lines"), submenu: [ { label: "8", type: "checkbox", checked: config.graph_line_width === 8, click: () => { set_checks("Sizes", "Graph lines", "8"); win.webContents.send("set", {graph_line_width: 8}); } }, { label: "7", type: "checkbox", checked: config.graph_line_width === 7, click: () => { set_checks("Sizes", "Graph lines", "7"); win.webContents.send("set", {graph_line_width: 7}); } }, { label: "6", type: "checkbox", checked: config.graph_line_width === 6, click: () => { set_checks("Sizes", "Graph lines", "6"); win.webContents.send("set", {graph_line_width: 6}); } }, { label: "5", type: "checkbox", checked: config.graph_line_width === 5, click: () => { set_checks("Sizes", "Graph lines", "5"); win.webContents.send("set", {graph_line_width: 5}); } }, { label: "4", type: "checkbox", checked: config.graph_line_width === 4, click: () => { set_checks("Sizes", "Graph lines", "4"); win.webContents.send("set", {graph_line_width: 4}); } }, { label: "3", type: "checkbox", checked: config.graph_line_width === 3, click: () => { set_checks("Sizes", "Graph lines", "3"); win.webContents.send("set", {graph_line_width: 3}); } }, { label: "2", type: "checkbox", checked: config.graph_line_width === 2, click: () => { set_checks("Sizes", "Graph lines", "2"); win.webContents.send("set", {graph_line_width: 2}); } }, ] }, { type: "separator" }, { label: translate.t("I want other size options!"), click: () => { alert(win, messages.about_sizes); } }, ] }, { label: translate.t("Engine"), submenu: [ { label: translate.t("Choose engine..."), type: "checkbox", checked: false, click: () => { let files = open_dialog(win, { defaultPath: config.engine_dialog_folder, properties: ["openFile"] }); if (Array.isArray(files) && files.length > 0) { let file = files[0]; if (file === process.argv[0] || path.basename(file).includes("client")) { alert(win, messages.wrong_engine_exe); win.webContents.send("call", "send_ack_engine"); // Force an ack IPC to fix our menu check state. return; } win.webContents.send("call", { fn: "switch_engine", args: [file] }); // Save the dir as the new default dir, in both processes. config.engine_dialog_folder = path.dirname(file); win.webContents.send("set", {engine_dialog_folder: path.dirname(file)}); } else { win.webContents.send("call", "send_ack_engine"); // Force an ack IPC to fix our menu check state. } }, }, { label: translate.t("Choose known engine..."), click: () => { win.webContents.send("call", "show_fast_engine_chooser"); } }, { label: translate.t("Weights"), submenu: [ { label: translate.t("Lc0 WeightsFile..."), type: "checkbox", checked: false, click: () => { let files = open_dialog(win, { defaultPath: config.weights_dialog_folder, properties: ["openFile"] }); if (Array.isArray(files) && files.length > 0) { let file = files[0]; win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["WeightsFile", file] }); // Will receive an ack IPC which sets menu checks. // Save the dir as the new default dir, in both processes. config.weights_dialog_folder = path.dirname(file); win.webContents.send("set", {weights_dialog_folder: path.dirname(file)}); } else { win.webContents.send("call", { // Force an ack IPC to fix our menu check state. fn: "send_ack_setoption", args: ["WeightsFile"], }); } }, }, { label: translate.t("Stockfish EvalFile..."), type: "checkbox", checked: false, click: () => { let files = open_dialog(win, { defaultPath: config.evalfile_dialog_folder, properties: ["openFile"] }); if (Array.isArray(files) && files.length > 0) { let file = files[0]; win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["EvalFile", file] }); // Will receive an ack IPC which sets menu checks. // Save the dir as the new default dir, in both processes. config.evalfile_dialog_folder = path.dirname(file); win.webContents.send("set", {evalfile_dialog_folder: path.dirname(file)}); } else { win.webContents.send("call", { // Force an ack IPC to fix our menu check state. fn: "send_ack_setoption", args: ["EvalFile"], }); } }, }, { label: translate.t("Set to "), click: () => { win.webContents.send("call", "auto_weights"); // Will receive an ack IPC which sets menu checks. } }, ] }, { label: translate.t("Backend"), submenu: [ { label: "cuda-auto", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "cuda-auto"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "cuda", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "cuda"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "cuda-fp16", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "cuda-fp16"] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: "cudnn-auto", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "cudnn-auto"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "cudnn", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "cudnn"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "cudnn-fp16", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "cudnn-fp16"] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: "blas", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "blas"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "dx12", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "dx12"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "eigen", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "eigen"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "metal", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "metal"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "onednn", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "onednn"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "opencl", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "opencl"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "xla", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "xla"] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: "tensorflow-cc", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "tensorflow-cc"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "tensorflow-cc-cpu", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "tensorflow-cc-cpu"] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: "onnx-cpu", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "onnx-cpu"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "onnx-cuda", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "onnx-cuda"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "onnx-dml", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "onnx-dml"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "onnx-rocm", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "onnx-rocm"] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: "random", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "random"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "trivial", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "trivial"] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: "demux", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "demux"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "multiplexing", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "multiplexing"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "roundrobin", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Backend", "roundrobin"] }); // Will receive an ack IPC which sets menu checks. } } ] }, { type: "separator" }, { label: translate.t("Choose Syzygy path..."), type: "checkbox", checked: false, click: () => { let folders = open_dialog(win, { defaultPath: config.syzygy_dialog_folder, properties: ["openDirectory"] }); if (Array.isArray(folders) && folders.length > 0) { let folder = folders[0]; win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["SyzygyPath", folder] // FIXME: should send all folders, separated by system separator. }); // Will receive an ack IPC which sets menu checks. // Save the dir as the new default dir, in both processes. config.syzygy_dialog_folder = path.dirname(folder); win.webContents.send("set", {syzygy_dialog_folder: path.dirname(folder)}); } else { win.webContents.send("call", { fn: "send_ack_setoption", args: ["SyzygyPath"] // Force an ack IPC to fix our menu check state. }); } } }, { label: translate.t("Unset"), click: () => { win.webContents.send("call", "disable_syzygy"); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: translate.t("Limit - normal"), submenu: [ { label: translate.t("Unlimited"), accelerator: "CommandOrControl+U", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [null] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: "1,000,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [1 * billion] }); // Will receive an ack IPC which sets menu checks. } }, { label: "100,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [100 * million] }); // Will receive an ack IPC which sets menu checks. } }, { label: "10,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [10 * million] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [1 * million] }); // Will receive an ack IPC which sets menu checks. } }, { label: "100,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [100000] }); // Will receive an ack IPC which sets menu checks. } }, { label: "10,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [10000] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [1000] }); // Will receive an ack IPC which sets menu checks. } }, { label: "100", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [100] }); // Will receive an ack IPC which sets menu checks. } }, { label: "10", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [10] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [2] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit", args: [1] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator", }, { label: translate.t("Up slightly"), accelerator: "CommandOrControl+=", click: () => { win.webContents.send("call", { fn: "adjust_node_limit", args: [1, false] }); } }, { label: translate.t("Down slightly"), accelerator: "CommandOrControl+-", click: () => { win.webContents.send("call", { fn: "adjust_node_limit", args: [-1, false] }); } }, ] }, { label: translate.t("Limit - auto-eval / play"), submenu: [ { label: "1,000,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [1 * billion] }); // Will receive an ack IPC which sets menu checks. } }, { label: "100,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [100 * million] }); // Will receive an ack IPC which sets menu checks. } }, { label: "10,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [10 * million] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1,000,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [1 * million] }); // Will receive an ack IPC which sets menu checks. } }, { label: "100,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [100000] }); // Will receive an ack IPC which sets menu checks. } }, { label: "10,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [10000] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1,000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [1000] }); // Will receive an ack IPC which sets menu checks. } }, { label: "100", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [100] }); // Will receive an ack IPC which sets menu checks. } }, { label: "10", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [10] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [2] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_node_limit_special", args: [1] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator", }, { label: translate.t("Up slightly"), accelerator: "CommandOrControl+]", click: () => { win.webContents.send("call", { fn: "adjust_node_limit", args: [1, true] }); } }, { label: translate.t("Down slightly"), accelerator: "CommandOrControl+[", click: () => { win.webContents.send("call", { fn: "adjust_node_limit", args: [-1, true] }); } }, ] }, { label: translate.t("Limit by time instead of nodes"), type: "checkbox", checked: false, click: () => { win.webContents.send("call", "toggle_limit_by_time"); } }, { type: "separator" }, { label: translate.t("Threads"), submenu: [ { label: "128", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 128], }); // Will receive an ack IPC which sets menu checks. } }, { label: "96", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 96], }); // Will receive an ack IPC which sets menu checks. } }, { label: "64", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 64], }); // Will receive an ack IPC which sets menu checks. } }, { label: "48", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 48], }); // Will receive an ack IPC which sets menu checks. } }, { label: "32", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 32], }); // Will receive an ack IPC which sets menu checks. } }, { label: "24", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 24], }); // Will receive an ack IPC which sets menu checks. } }, { label: "16", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 16], }); // Will receive an ack IPC which sets menu checks. } }, { label: "14", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 14], }); // Will receive an ack IPC which sets menu checks. } }, { label: "12", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 12], }); // Will receive an ack IPC which sets menu checks. } }, { label: "10", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 10], }); // Will receive an ack IPC which sets menu checks. } }, { label: "8", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 8], }); // Will receive an ack IPC which sets menu checks. } }, { label: "7", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 7], }); // Will receive an ack IPC which sets menu checks. } }, { label: "6", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 6], }); // Will receive an ack IPC which sets menu checks. } }, { label: "5", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 5], }); // Will receive an ack IPC which sets menu checks. } }, { label: "4", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 4], }); // Will receive an ack IPC which sets menu checks. } }, { label: "3", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 3], }); // Will receive an ack IPC which sets menu checks. } }, { label: "2", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 2], }); // Will receive an ack IPC which sets menu checks. } }, { label: "1", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Threads", 1], }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: translate.t("Warning about threads"), click: () => { alert(win, messages.thread_warning); } }, ] }, { label: translate.t("Hash"), submenu: [ { label: "120 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 120 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "56 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 56 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "24 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 24 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "12 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 12 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "8 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 8 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "6 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 6 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "4 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 4 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 2 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 1 * 1024] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0 GB", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Hash", 1] // 1 MB is Stockfish actual minimum. }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: translate.t("I want other hash options!"), click: () => { alert(win, messages.about_hashes); } } ] }, { label: translate.t("MultiPV"), submenu: [ { label: "5", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["MultiPV", 5] }); // Will receive an ack IPC which sets menu checks. } }, { label: "4", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["MultiPV", 4] }); // Will receive an ack IPC which sets menu checks. } }, { label: "3", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["MultiPV", 3] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["MultiPV", 2] }); // Will receive an ack IPC which sets menu checks. } }, { label: "1", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["MultiPV", 1] }); // Will receive an ack IPC which sets menu checks. } }, ] }, { type: "separator" }, { label: translate.t("Contempt Mode"),// Other valid options are "play" (which messes with normal analysis) and "disable" submenu: [ { label: translate.t("White analysis"), // Note string searched when ack'd. type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["ContemptMode", "white_side_analysis"] }); // Will receive an ack IPC which sets menu checks. } }, { label: translate.t("Black analysis"), // Note string searched when ack'd. type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["ContemptMode", "black_side_analysis"] }); // Will receive an ack IPC which sets menu checks. } }, ] }, { label: translate.t("Contempt"), submenu: [ { label: "250", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", 250] }); // Will receive an ack IPC which sets menu checks. } }, { label: "200", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", 200] }); // Will receive an ack IPC which sets menu checks. } }, { label: "150", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", 150] }); // Will receive an ack IPC which sets menu checks. } }, { label: "100", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", 100] }); // Will receive an ack IPC which sets menu checks. } }, { label: "50", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", 50] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", 0] }); // Will receive an ack IPC which sets menu checks. } }, { label: "-50", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", -50] }); // Will receive an ack IPC which sets menu checks. } }, { label: "-100", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", -100] }); // Will receive an ack IPC which sets menu checks. } }, { label: "-150", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", -150] }); // Will receive an ack IPC which sets menu checks. } }, { label: "-200", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", -200] }); // Will receive an ack IPC which sets menu checks. } }, { label: "-250", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["Contempt", -250] }); // Will receive an ack IPC which sets menu checks. } }, ] }, { label: translate.t("WDL Calibration Elo"), submenu: [ { label: "3600", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 3600] }); // Will receive an ack IPC which sets menu checks. } }, { label: "3400", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 3400] }); // Will receive an ack IPC which sets menu checks. } }, { label: "3200", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 3200] }); // Will receive an ack IPC which sets menu checks. } }, { label: "3000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 3000] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2800", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 2800] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2600", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 2600] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2400", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 2400] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2200", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 2200] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2000", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 2000] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: translate.t("Use default WDL"), // This string is searched for when receiving ack 0, don't edit this alone. type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent_and_cleartree", args: ["WDLCalibrationElo", 0] }); // Will receive an ack IPC which sets menu checks. } }, ] }, { label: translate.t("WDL Eval Objectivity"), submenu: [ { label: translate.t("Yes"), type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["WDLEvalObjectivity", 1] }); // Will receive an ack IPC which sets menu checks. } }, { label: translate.t("No"), type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["WDLEvalObjectivity", 0] }); // Will receive an ack IPC which sets menu checks. } } ] }, { label: translate.t("Score Type"), submenu: [ { label: "WDL_mu", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["ScoreType", "WDL_mu"] }); // Will receive an ack IPC which sets menu checks. } }, { label: "centipawn", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["ScoreType", "centipawn"] }); // Will receive an ack IPC which sets menu checks. } } ] }, { type: "separator" }, { label: translate.t("Custom scripts"), submenu: scriptlist_in_menu // Will be filled at the end, see below. }, { type: "separator" }, { label: translate.t("Restart engine"), click: () => { win.webContents.send("call", "restart_engine"); } }, { label: translate.t("Soft engine reset"), click: () => { win.webContents.send("call", "soft_engine_reset"); } }, ] }, { label: translate.t("Play"), submenu: [ { label: translate.t("Play this colour"), accelerator: "F9", click: () => { win.webContents.send("call", "play_this_colour"); } }, { type: "separator" }, { label: translate.t("Start self-play"), accelerator: "F11", click: () => { win.webContents.send("call", { fn: "set_behaviour", args: ["self_play"], }); } }, { label: translate.t("Halt"), click: () => { win.webContents.send("call", { fn: "set_behaviour", args: ["halt"], }); } }, { type: "separator" }, { label: translate.t("Use Polyglot book..."), type: "checkbox", checked: false, click: () => { let files = open_dialog(win, { defaultPath: config.book_dialog_folder, properties: ["openFile"], filters: [{name: "Polyglot", extensions: ["bin"]}, {name: "All files", extensions: ["*"]}] }); if (Array.isArray(files) && files.length > 0) { let file = files[0]; win.webContents.send("call", { fn: "load_polyglot_book", args: [file] }); // Will receive an ack IPC which sets menu checks. // Save the dir as the new default dir, in both processes. config.book_dialog_folder = path.dirname(file); win.webContents.send("set", {book_dialog_folder: path.dirname(file)}); } else { win.webContents.send("call", "send_ack_book"); // Force an ack IPC to fix our menu check state. } } }, { label: translate.t("Use PGN book..."), type: "checkbox", checked: false, click: () => { let files = open_dialog(win, { defaultPath: config.book_dialog_folder, properties: ["openFile"], filters: [{name: "PGN", extensions: ["pgn"]}, {name: "All files", extensions: ["*"]}] }); if (Array.isArray(files) && files.length > 0) { let file = files[0]; win.webContents.send("call", { fn: "load_pgn_book", args: [file] }); // Will receive an ack IPC which sets menu checks. // Save the dir as the new default dir, in both processes. config.book_dialog_folder = path.dirname(file); win.webContents.send("set", {book_dialog_folder: path.dirname(file)}); } else { win.webContents.send("call", "send_ack_book"); // Force an ack IPC to fix our menu check state. } } }, { label: translate.t("Unload book / abort load"), click: () => { win.webContents.send("call", "unload_book"); // Will receive an ack IPC which sets menu checks. } }, { label: translate.t("Book depth limit"), submenu: [ { label: translate.t("Unlimited"), type: "checkbox", checked: typeof config.book_depth !== "number", click: () => { set_checks("Play", "Book depth limit", "Unlimited"); win.webContents.send("set", {book_depth: null}); } }, { label: "20", type: "checkbox", checked: config.book_depth === 20, click: () => { set_checks("Play", "Book depth limit", "20"); win.webContents.send("set", {book_depth: 20}); } }, { label: "18", type: "checkbox", checked: config.book_depth === 18, click: () => { set_checks("Play", "Book depth limit", "18"); win.webContents.send("set", {book_depth: 18}); } }, { label: "16", type: "checkbox", checked: config.book_depth === 16, click: () => { set_checks("Play", "Book depth limit", "16"); win.webContents.send("set", {book_depth: 16}); } }, { label: "14", type: "checkbox", checked: config.book_depth === 14, click: () => { set_checks("Play", "Book depth limit", "14"); win.webContents.send("set", {book_depth: 14}); } }, { label: "12", type: "checkbox", checked: config.book_depth === 12, click: () => { set_checks("Play", "Book depth limit", "12"); win.webContents.send("set", {book_depth: 12}); } }, { label: "10", type: "checkbox", checked: config.book_depth === 10, click: () => { set_checks("Play", "Book depth limit", "10"); win.webContents.send("set", {book_depth: 10}); } }, { label: "8", type: "checkbox", checked: config.book_depth === 8, click: () => { set_checks("Play", "Book depth limit", "8"); win.webContents.send("set", {book_depth: 8}); } }, { label: "6", type: "checkbox", checked: config.book_depth === 6, click: () => { set_checks("Play", "Book depth limit", "6"); win.webContents.send("set", {book_depth: 6}); } }, { label: "4", type: "checkbox", checked: config.book_depth === 4, click: () => { set_checks("Play", "Book depth limit", "4"); win.webContents.send("set", {book_depth: 4}); } }, { label: "2", type: "checkbox", checked: config.book_depth === 2, click: () => { set_checks("Play", "Book depth limit", "2"); win.webContents.send("set", {book_depth: 2}); } }, ] }, { type: "separator" }, { label: translate.t("Temperature"), submenu: [ { label: "1.0", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 1.0] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.9", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.9] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.8", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.8] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.7", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.7] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.6", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.6] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.5", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.5] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.4", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.4] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.3", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.3] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.2", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.2] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0.1", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0.1] }); // Will receive an ack IPC which sets menu checks. } }, { label: "0", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["Temperature", 0] }); // Will receive an ack IPC which sets menu checks. } }, ] }, { label: translate.t("Temp Decay Moves"), submenu: [ { label: translate.t("Infinite"), type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 0] }); // Will receive an ack IPC which sets menu checks. } }, { label: "20", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 20] }); // Will receive an ack IPC which sets menu checks. } }, { label: "18", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 18] }); // Will receive an ack IPC which sets menu checks. } }, { label: "16", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 16] }); // Will receive an ack IPC which sets menu checks. } }, { label: "14", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 14] }); // Will receive an ack IPC which sets menu checks. } }, { label: "12", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 12] }); // Will receive an ack IPC which sets menu checks. } }, { label: "10", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 10] }); // Will receive an ack IPC which sets menu checks. } }, { label: "8", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 8] }); // Will receive an ack IPC which sets menu checks. } }, { label: "6", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 6] }); // Will receive an ack IPC which sets menu checks. } }, { label: "4", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 4] }); // Will receive an ack IPC which sets menu checks. } }, { label: "2", type: "checkbox", checked: false, click: () => { win.webContents.send("call", { fn: "set_uci_option_permanent", args: ["TempDecayMoves", 2] }); // Will receive an ack IPC which sets menu checks. } }, ] }, { type: "separator" }, { label: translate.t("About play modes"), click: () => { alert(win, messages.about_versus_mode); } } ] }, { label: translate.t("Dev"), submenu: [ { label: translate.t("Toggle Developer Tools"), role: "toggledevtools" }, { label: translate.t("Toggle Debug CSS"), click: () => { win.webContents.send("call", "toggle_debug_css"); } }, { type: "separator" }, { label: translate.t("Permanently enable save"), click: () => { config.save_enabled = true; // The main process actually uses this variable... win.webContents.send("set", {save_enabled: true}); // But it's the renderer process that saves the config file. } }, { type: "separator" }, { label: translate.t(`Show ${config_io.filename}`), // Ugh. click: () => { electron.shell.showItemInFolder(config_io.filepath); } }, { label: translate.t(`Show ${engineconfig_io.filename}`), // Ugh. click: () => { electron.shell.showItemInFolder(engineconfig_io.filepath); } }, { type: "separator" }, { label: translate.t(`Reload ${engineconfig_io.filename} (and restart engine)`), // Ugh. click: () => { win.webContents.send("call", "reload_engineconfig"); } }, { type: "separator" }, { label: translate.t("Random move"), accelerator: "CommandOrControl+/", click: () => { win.webContents.send("call", "random_move"); } }, { type: "separator" }, { label: translate.t("Disable hardware acceleration for GUI"), type: "checkbox", checked: config.disable_hw_accel, click: () => { win.webContents.send("call", { fn: "toggle", args: ["disable_hw_accel"], }); if (!have_warned_hw_accel_setting) { alert(win, "This will not take effect until you restart the GUI."); have_warned_hw_accel_setting = true; } } }, { label: translate.t("Spin rate"), submenu: [ { label: translate.t("Frenetic"), type: "checkbox", checked: config.update_delay === 25, click: () => { set_checks("Dev", "Spin rate", "Frenetic"); win.webContents.send("set", {update_delay: 25}); } }, { label: translate.t("Fast"), type: "checkbox", checked: config.update_delay === 60, click: () => { set_checks("Dev", "Spin rate", "Fast"); win.webContents.send("set", {update_delay: 60}); } }, { label: translate.t("Normal"), type: "checkbox", checked: config.update_delay === 125, click: () => { set_checks("Dev", "Spin rate", "Normal"); win.webContents.send("set", {update_delay: 125}); } }, { label: translate.t("Relaxed"), type: "checkbox", checked: config.update_delay === 170, click: () => { set_checks("Dev", "Spin rate", "Relaxed"); win.webContents.send("set", {update_delay: 170}); } }, { label: translate.t("Lazy"), type: "checkbox", checked: config.update_delay === 250, click: () => { set_checks("Dev", "Spin rate", "Lazy"); win.webContents.send("set", {update_delay: 250}); } }, ] }, { type: "separator" }, { label: translate.t("Show engine state"), type: "checkbox", checked: config.show_engine_state, click: () => { win.webContents.send("call", { fn: "toggle", args: ["show_engine_state"] }); } }, { label: translate.t("List sent options"), click: () => { win.webContents.send("call", "show_sent_options"); } }, { label: translate.t("Show error log"), click: () => { win.webContents.send("call", "show_error_log"); } }, { type: "separator" }, { label: translate.t("Hacks and kludges"), submenu: [ { label: translate.t("Allow arbitrary scripts"), type: "checkbox", checked: config.allow_arbitrary_scripts, click: () => { win.webContents.send("call", { fn: "toggle", args: ["allow_arbitrary_scripts"], }); } }, { label: translate.t("Accept any file size"), type: "checkbox", checked: config.ignore_filesize_limits, click: () => { win.webContents.send("call", { fn: "toggle", args: ["ignore_filesize_limits"], }); } }, { label: translate.t("Allow stopped analysis"), type: "checkbox", checked: config.allow_stopped_analysis, click: () => { win.webContents.send("call", { fn: "toggle", args: ["allow_stopped_analysis"], }); } }, { label: translate.t("Never hide focus buttons"), type: "checkbox", checked: config.never_suppress_searchmoves, click: () => { win.webContents.send("call", { fn: "toggle", args: ["never_suppress_searchmoves"], }); } }, { label: translate.t("Never grayout move info"), type: "checkbox", checked: config.never_grayout_infolines, click: () => { win.webContents.send("call", { fn: "toggle", args: ["never_grayout_infolines"], }); } }, { label: translate.t("Use lowerbound / upperbound info"), type: "checkbox", checked: config.accept_bounds, click: () => { win.webContents.send("call", { fn: "toggle", args: ["accept_bounds"], }); } }, { label: translate.t("Suppress ucinewgame"), type: "checkbox", checked: config.suppress_ucinewgame, click: () => { win.webContents.send("call", { fn: "toggle", args: ["suppress_ucinewgame"], }); } }, ] }, { type: "separator" }, { label: translate.t("Log RAM state to console"), click: () => { win.webContents.send("call", "log_ram"); } }, { label: translate.t("Fire GC"), click: () => { win.webContents.send("call", "fire_gc"); } }, { type: "separator" }, { label: translate.t("Logging"), submenu: [ { label: translate.t("Use logfile..."), type: "checkbox", checked: typeof config.logfile === "string" && config.logfile !== "", click: () => { let file = save_dialog(win, {}); if (typeof file === "string" && file.length > 0) { win.webContents.send("call", { fn: "set_logfile", args: [file] }); // Will receive an ack IPC which sets menu checks. } else { win.webContents.send("call", "send_ack_logfile"); // Force an ack IPC to fix our menu check state. } } }, { label: translate.t("Disable logging"), click: () => { win.webContents.send("call", { fn: "set_logfile", args: [null] }); // Will receive an ack IPC which sets menu checks. } }, { type: "separator" }, { label: translate.t("Clear log when opening"), type: "checkbox", checked: config.clear_log, click: () => { win.webContents.send("call", { fn: "toggle", args: ["clear_log"], }); } }, { label: translate.t("Use unique logfile each time"), type: "checkbox", checked: config.logfile_timestamp, click: () => { win.webContents.send("call", { fn: "toggle", args: ["logfile_timestamp"], }); } }, { type: "separator" }, { label: translate.t("Log illegal moves"), type: "checkbox", checked: config.log_illegal_moves, click: () => { win.webContents.send("call", { fn: "toggle", args: ["log_illegal_moves"], }); } }, { label: translate.t("Log positions"), type: "checkbox", checked: config.log_positions, click: () => { win.webContents.send("call", { fn: "toggle", args: ["log_positions"], }); } }, { label: translate.t("Log info lines"), type: "checkbox", checked: config.log_info_lines, click: () => { win.webContents.send("call", { fn: "toggle", args: ["log_info_lines"], }); } }, { label: translate.t("...including useless lines"), type: "checkbox", checked: config.log_useless_info, click: () => { win.webContents.send("call", { fn: "toggle", args: ["log_useless_info"], }); } }, ] }, ] }, { label: translate.t("Language"), submenu: language_choices_submenu() } ]; // Some special shennanigans to build the custom scripts menu... let scriptlist = custom_uci.load(); for (let script of scriptlist) { scriptlist_in_menu.push({ label: script.name, click: () => { win.webContents.send("call", { fn: "run_script", args: [script.path] }); } }); } if (scriptlist_in_menu.length > 0) { scriptlist_in_menu.push({type: "separator"}); } scriptlist_in_menu.push({ label: translate.t("How to add scripts"), click: () => { alert(win, messages.adding_scripts); } }); scriptlist_in_menu.push({ label: translate.t("Show scripts folder"), click: () => { electron.shell.showItemInFolder(custom_uci.script_dir_path); } }); // Actually build the menu... return electron.Menu.buildFromTemplate(template); } function language_choices_submenu() { let ret = []; for (let language of translate.all_languages()) { ret.push({ label: language, type: "checkbox", checked: config.language === language, click: () => { set_checks("Language", language); win.webContents.send("call", { fn: "set_language", args: [language] }); } }); } return ret; } function get_submenu_items(menupath) { // If the path is to a submenu, this returns a list of all items in the submenu. // If the path is to a specific menu item, it just returns that item. let o = menu.items; for (let p of menupath) { p = translate.t(stringify(p)); for (let item of o) { if (item.label === p) { if (item.submenu) { o = item.submenu.items; break; } else { return item; // No submenu so this must be the end. } } } } return o; } function set_checks(...menupath) { if (!menu_is_set) { return; } // Since I don't know precisely how the menu works behind the scenes, // give a little time for the original click to go through first. setTimeout(() => { let items = get_submenu_items(menupath.slice(0, -1)); for (let n = 0; n < items.length; n++) { if (items[n].checked !== undefined) { items[n].checked = items[n].label === translate.t(stringify(menupath[menupath.length - 1])); } } }, 50); } function set_one_check(state, ...menupath) { state = state ? true : false; if (!menu_is_set) { return; } let item = get_submenu_items(menupath); if (item.checked !== undefined) { item.checked = state; } } ================================================ FILE: files/src/modules/alert_main.js ================================================ "use strict"; // Exports a function we use to draw alert messages. // To be used in the main process only (so when the renderer needs to make an alert, it sends the message to main via IPC). const electron = require("electron"); const stringify = require("./stringify"); let major_version = (process && process.versions) ? parseInt(process.versions.electron, 10) : 0; if (Number.isNaN(major_version)) { major_version = 0; } let alerts_open = 0; module.exports = function(...args) { // Can be called as (msg) or as (win, msg) let win = (args.length < 2) ? undefined : args[0]; let msg = (args.length < 2) ? args[0] : args[1]; if (alerts_open >= 3) { console.log(msg); return; } alerts_open++; if (major_version <= 5) { // Old API. Providing a callback makes the window not block the process... // This is all rather untested. if (win) { electron.dialog.showMessageBox(win, {message: stringify(msg), title: "Alert", buttons: ["OK"]}, () => { alerts_open--; }); } else { electron.dialog.showMessageBox({message: stringify(msg), title: "Alert", buttons: ["OK"]}, () => { alerts_open--; }); } } else { // New promise-based API. Shouldn't block the process... if (win) { electron.dialog.showMessageBox(win, {message: stringify(msg), title: "Alert", buttons: ["OK"]}).then(() => { alerts_open--; }); } else { electron.dialog.showMessageBox({message: stringify(msg), title: "Alert", buttons: ["OK"]}).then(() => { alerts_open--; }); } } }; ================================================ FILE: files/src/modules/background.js ================================================ "use strict"; function background(light, dark, square_size) { let c = document.createElement("canvas"); c.width = square_size * 8; c.height = square_size * 8; let ctx = c.getContext("2d"); for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { ctx.fillStyle = (x + y) % 2 === 0 ? light : dark; ctx.fillRect(x * square_size, y * square_size, square_size, square_size); } } // I guess the canvas c gets garbage-collected? https://stackoverflow.com/questions/15320853 return `url("${c.toDataURL("image/png")}")`; } module.exports = background; ================================================ FILE: files/src/modules/config_io.js ================================================ "use strict"; const electron = require("electron"); const fs = require("fs"); const path = require("path"); const querystring = require("querystring"); const debork_json = require("./debork_json"); exports.filename = "config.json"; // To avoid using "remote", we rely on the main process passing userData location in the query... exports.filepath = electron.app ? path.join(electron.app.getPath("userData"), exports.filename) : // in Main process path.join(querystring.parse(global.location.search.slice(1))["user_data_path"], exports.filename); // in Renderer process function Config() {} // This exists solely to make instanceof work. Config.prototype = {}; exports.defaults = { "warning": "EDITING THIS FILE WHILE NIBBLER IS RUNNING WILL GENERALLY CAUSE YOUR EDITS TO BE LOST.", "language": "English", "path": null, // Not undefined, all normal keys should have an actual value. "args_unused": null, "options_unused": null, "disable_hw_accel": false, "width": 1280, "height": 835, "board_size": 640, "info_font_size": 16, "pgn_font_size": 16, "fen_font_size": 16, "arrow_width": 8, "arrowhead_radius": 12, "board_font": "18px Arial", "graph_height": 96, "graph_line_width": 2, "graph_minimum_length": 41, // Desired depth + 1 "light_square": "#dadada", "dark_square": "#b4b4b4", "active_square": "#66aaaa", "move_squares_with_alpha": "#ffff0026", "best_colour": "#66aaaa", "good_colour": "#66aa66", "bad_colour": "#cccc66", "terrible_colour": "#cc6666", "actual_move_colour": "#cc9966", "searchmoves_buttons": true, "focus_on_text": "focused:", "focus_off_text": "focus?", "accept_bounds": false, "max_info_lines": null, // Hidden option "bad_move_threshold": 0.02, "terrible_move_threshold": 0.04, "ab_filter_threshold": 0.1, "arrow_filter_type": "N", "arrow_filter_value": 0.01, "arrows_enabled": true, "click_spotlight": true, "next_move_arrow": false, "next_move_outline": false, "next_move_unique_colour": false, "arrowhead_type": 0, "ev_pov": null, "cp_pov": null, "wdl_pov": null, "show_cp": false, "show_n": true, "show_n_abs": true, "show_depth": true, "show_p": true, "show_v": false, "show_q": false, "show_u": false, "show_s": false, "show_m": false, "show_wdl": true, "infobox_stats_newline": false, "infobox_pv_move_numbers": false, "hover_draw": false, "hover_method": 2, "looker_api": null, "look_past_25": false, "lichess_token": "", "pv_click_event": 1, // 0: nothing, 1: goto, 2: tree "pgn_ev": true, "pgn_cp": false, "pgn_n": true, "pgn_n_abs": false, "pgn_of_n": true, "pgn_depth": false, "pgn_p": false, "pgn_v": false, "pgn_q": false, "pgn_u": false, "pgn_s": false, "pgn_m": false, "pgn_wdl": false, "pgn_dialog_folder": "", "engine_dialog_folder": "", "weights_dialog_folder": "", "evalfile_dialog_folder": "", "syzygy_dialog_folder": "", "pieces_dialog_folder": "", "background_dialog_folder": "", "book_dialog_folder": "", "update_delay": 170, "animate_delay_multiplier": 4, "allow_arbitrary_scripts": false, "ignore_filesize_limits": false, "allow_stopped_analysis": false, "never_suppress_searchmoves": true, "never_grayout_infolines": false, "suppress_ucinewgame": false, "show_engine_state": false, "book_depth": 10, "save_enabled": false, "override_piece_directory": null, "override_board": null, "leelaish_names": ["Lc0", "Leela", "Ceres"], // If this gets updated, will need to fix old config files. "logfile": null, "clear_log": true, "logfile_timestamp": false, "log_info_lines": false, "log_useless_info": false, "log_illegal_moves": true, "log_positions": true, }; function fix(cfg) { // We want to create a few temporary things (not saved to file)... cfg.flip = false; cfg.behaviour = "halt"; cfg.square_size = Math.floor(cfg.board_size / 8); // Make sure objectish things at least exist... if (Array.isArray(cfg.leelaish_names) === false) { cfg.leelaish_names = Array.from(exports.defaults.leelaish_names); } // Fix the board size... cfg.board_size = cfg.square_size * 8; // The uncertainty_cutoff key was removed. Filtering by U was also removed... if (cfg.uncertainty_cutoff !== undefined || cfg.arrow_filter_type === "U") { cfg.arrow_filter_type = "N"; cfg.arrow_filter_value = exports.defaults.arrow_filter_value; } // This can't be 0 because we divide by it... cfg.animate_delay_multiplier = Math.floor(cfg.animate_delay_multiplier); if (cfg.animate_delay_multiplier <= 0) { cfg.animate_delay_multiplier = 1; } // We used to expect font sizes to be strings with "px"... for (let key of ["info_font_size", "pgn_font_size", "fen_font_size"]) { if (typeof cfg[key] === "string") { cfg[key] = parseInt(cfg[key], 10); // Works even if string ends with "px" if (Number.isNaN(cfg[key])) { cfg[key] = exports.defaults[key]; } } } // Convert any strings of "false", "true" and "null"... for (let key of Object.keys(cfg)) { if (typeof cfg[key] === "string") { if (cfg[key].toLowerCase() === "true") { cfg[key] = true; } else if (cfg[key].toLowerCase() === "false") { cfg[key] = false; } else if (cfg[key].toLowerCase() === "null") { cfg[key] = null; } } } // These things need to be strings. They are used as defaultPath parameters // but versions of Electron >= 6 (I think) crash when they aren't strings. // Sadly we defaulted them to null in 1.2.1 so bad config files may exist. if (typeof cfg.pgn_dialog_folder !== "string") { cfg.pgn_dialog_folder = ""; } if (typeof cfg.engine_dialog_folder !== "string") { cfg.engine_dialog_folder = ""; } if (typeof cfg.weights_dialog_folder !== "string") { cfg.weights_dialog_folder = ""; } // These three vars were replaced... if (cfg.ev_white_pov) { cfg.ev_pov = "w"; } if (cfg.cp_white_pov) { cfg.cp_pov = "w"; } if (cfg.wdl_white_pov) { cfg.wdl_pov = "w"; } // Too many people are setting this... cfg.show_engine_state = exports.defaults.show_engine_state; } exports.load = () => { let cfg = new Config(); let err_to_return = null; try { if (fs.existsSync(exports.filepath)) { let raw = fs.readFileSync(exports.filepath, "utf8"); try { Object.assign(cfg, JSON.parse(raw)); } catch (err) { console.log(exports.filename, err.toString(), "...trying to debork..."); Object.assign(cfg, JSON.parse(debork_json(raw))); } } } catch (err) { console.log(err.toString()); // alert() might not be available. err_to_return = err.toString(); } // Copy default values for any missing keys into the config... // We use a copy so that any objects that are assigned are not the default objects. let defaults_copy = JSON.parse(JSON.stringify(exports.defaults)); for (let key of Object.keys(defaults_copy)) { if (cfg.hasOwnProperty(key) === false) { cfg[key] = defaults_copy[key]; } } fix(cfg); return [err_to_return, cfg]; }; exports.save = (cfg) => { if (cfg instanceof Config === false) { throw "Wrong type of object sent to config_io.save()"; } // Make a copy of the defaults. Doing it this way seems to // ensure the final JSON string has the same ordering... let out = JSON.parse(JSON.stringify(exports.defaults)); // Adjust that copy, but only for keys present in both. for (let key of Object.keys(cfg)) { if (out.hasOwnProperty(key)) { out[key] = cfg[key]; } } try { fs.writeFileSync(exports.filepath, JSON.stringify(out, null, "\t")); } catch (err) { console.log(err.toString()); // alert() might not be available. } }; exports.create_if_needed = (cfg) => { // Note that this must be called fairly late, when userData directory exists. if (cfg instanceof Config === false) { throw "Wrong type of object sent to config_io.create_if_needed()"; } if (fs.existsSync(exports.filepath)) { return; } exports.save(cfg); }; ================================================ FILE: files/src/modules/custom_uci.js ================================================ "use strict"; const electron = require("electron"); const fs = require("fs"); const path = require("path"); const querystring = require("querystring"); const scripts_dir = "scripts"; const example_file = "example.txt"; const example = `setoption name Something value WhoKnows setoption name Example value Whatever`; // To avoid using "remote", we rely on the main process passing userData location in the query... exports.script_dir_path = electron.app ? path.join(electron.app.getPath("userData"), scripts_dir) : path.join(querystring.parse(global.location.search.slice(1))["user_data_path"], scripts_dir); exports.load = () => { try { let files = fs.readdirSync(exports.script_dir_path); let ret = []; for (let file of files) { ret.push({ name: file, path: path.join(exports.script_dir_path, file) }); } return ret; } catch (err) { return [ { name: example_file, path: path.join(exports.script_dir_path, example_file) } ]; } }; exports.create_if_needed = () => { // Note that this must be called fairly late, when userData directory exists. try { if (!fs.existsSync(exports.script_dir_path)) { fs.mkdirSync(exports.script_dir_path); let example_path = path.join(exports.script_dir_path, example_file); fs.writeFileSync(example_path, example); } } catch (err) { console.log(err.toString()); } }; ================================================ FILE: files/src/modules/debork_json.js ================================================ "use strict"; function replace_all(s, search, replace) { if (!s.includes(search)) { // Seems to improve speed overall. return s; } return s.split(search).join(replace); } function debork_json(s) { // Convert totally blank files into {} if (s.length < 50 && s.trim() === "") { s = "{}"; } // Replace fruity quote characters. Note that these could exist in legit JSON, // which is why we only call this function if the first parse fails... s = replace_all(s, '“', '"'); s = replace_all(s, '”', '"'); // Replace single \ characters s = replace_all(s, "\\\\", "correct_zbcyg278gfdakjadjk"); s = replace_all(s, "\\", "\\\\"); s = replace_all(s, "correct_zbcyg278gfdakjadjk", "\\\\"); return s; } module.exports = debork_json; ================================================ FILE: files/src/modules/empty.js ================================================ "use strict"; // The most perfect JS module ever written. Extensive testing // shows that this module contains less than 20 bugs. ================================================ FILE: files/src/modules/engineconfig_io.js ================================================ "use strict"; const electron = require("electron"); const fs = require("fs"); const path = require("path"); const querystring = require("querystring"); const debork_json = require("./debork_json"); exports.filename = "engines.json"; // To avoid using "remote", we rely on the main process passing userData location in the query... exports.filepath = electron.app ? path.join(electron.app.getPath("userData"), exports.filename) : // in Main process path.join(querystring.parse(global.location.search.slice(1))["user_data_path"], exports.filename); // in Renderer process function EngineConfig() {} // This exists solely to make instanceof work. EngineConfig.prototype = {}; // --------------------------------------------------------------------------------------------------------------------------- function fix(cfg) { // The nameless dummy that hub creates at startup needs an entry... cfg[""] = exports.newentry(); // Fix any saved entries present in the file... for (let key of Object.keys(cfg)) { if (typeof cfg[key] !== "object" || cfg[key] === null) { cfg[key] = exports.newentry(); } if (Array.isArray(cfg[key].args) === false) { cfg[key].args = []; } if (typeof cfg[key].options !== "object" || cfg[key].options === null) { cfg[key].options = {}; } if (typeof cfg[key].limit_by_time !== "boolean") { cfg[key].limit_by_time = cfg[key].limit_by_time ? true : false; } // We don't really care about missing search_nodes and search_nodes_special properties. (?) } } exports.newentry = () => { return { "args": [], "options": {}, "search_nodes": null, "search_nodes_special": 10000, "limit_by_time": false, }; }; exports.load = () => { let cfg = new EngineConfig(); let err_to_return = null; try { if (fs.existsSync(exports.filepath)) { let raw = fs.readFileSync(exports.filepath, "utf8"); try { Object.assign(cfg, JSON.parse(raw)) } catch (err) { console.log(exports.filename, err.toString(), "...trying to debork..."); Object.assign(cfg, JSON.parse(debork_json(raw))); } } } catch (err) { console.log(err.toString()); // alert() might not be available. err_to_return = err.toString(); } fix(cfg); return [err_to_return, cfg]; }; exports.save = (cfg) => { if (cfg instanceof EngineConfig === false) { throw "Wrong type of object sent to engineconfig_io.save()"; } let blank = cfg[""]; delete cfg[""]; try { fs.writeFileSync(exports.filepath, JSON.stringify(cfg, null, "\t")); } catch (err) { console.log(err.toString()); // alert() might not be available. } cfg[""] = blank; }; exports.create_if_needed = (cfg) => { // Note that this must be called fairly late, when userData directory exists. if (cfg instanceof EngineConfig === false) { throw "Wrong type of object sent to engineconfig_io.create_if_needed()"; } if (fs.existsSync(exports.filepath)) { return; } exports.save(cfg); }; ================================================ FILE: files/src/modules/images.js ================================================ "use strict"; const fs = require("fs"); const path = require("path"); let sprites = { loads: 0, fully_loaded: function() { return this.loads === 12; }, validate_folder: function(directory) { if (typeof directory !== "string") { return false; } for (let c of "KkQqRrBbNnPp") { if (!fs.existsSync(path.join(directory, `${c.toUpperCase()}.svg`))) { if (!fs.existsSync(path.join(directory, `${c.toUpperCase()}.png`))) { return false; } } if (!fs.existsSync(path.join(directory, `_${c.toUpperCase()}.svg`))) { if (!fs.existsSync(path.join(directory, `_${c.toUpperCase()}.png`))) { return false; } } } return true; }, load_from: function(directory) { let urlsafe_directory = directory.replace(/#/g, "%23"); // Looks like replacing # with %23 is the only thing that's needed? Maybe some others?? sprites.loads = 0; for (let c of "KkQqRrBbNnPp") { sprites[c] = new Image(); sprites[c].addEventListener("load", () => {sprites.loads++;}, {once: true}); if (c === c.toUpperCase()) { sprites[c].addEventListener("error", () => {console.log(`Failed to load image ${c}.svg or ${c}.png`);}, {once: true}); if (fs.existsSync(path.join(directory, `${c}.svg`))) { sprites[c].src = path.join(urlsafe_directory, `${c}.svg`); } else if (fs.existsSync(path.join(directory, `${c}.png`))) { sprites[c].src = path.join(urlsafe_directory, `${c}.png`); } } else { sprites[c].addEventListener("error", () => {console.log(`Failed to load image _${c.toUpperCase()}.svg or _${c.toUpperCase()}.png`);}, {once: true}); if (fs.existsSync(path.join(directory, `_${c.toUpperCase()}.svg`))) { sprites[c].src = path.join(urlsafe_directory, `_${c.toUpperCase()}.svg`); } else if (fs.existsSync(path.join(directory, `_${c.toUpperCase()}.png`))) { sprites[c].src = path.join(urlsafe_directory, `_${c.toUpperCase()}.png`); } } // Note that, after the src is set above, it is automatically changed by the JS engine to be something like // "file:///C:/foo/bar/whatever.png" sprites[c].string_for_bg_style = `url("${sprites[c].src}")`; // Since the src path won't contain " this should be safe. } }, }; module.exports = sprites; ================================================ FILE: files/src/modules/messages.js ================================================ "use strict"; const config_io = require("./config_io"); const engineconfig_io = require("./engineconfig_io"); exports.about_versus_mode = `The "play this colour" option causes Leela to \ evaluate one side of the position only. The top move is automatically played on \ the board upon reaching the node limit (see the Engine menu). This allows you to \ play against Leela. The "self-play" option causes Leela to play itself. Higher temperature makes the moves less predictable, but at some cost to move \ correctness. Meanwhile, TempDecayMoves specifies how many moves the temperature \ effect lasts for. These settings have no effect on analysis, only actual move \ generation.`; exports.save_not_enabled = `Save is disabled until you read the following \ warning. Nibbler does not append to PGN files, nor does it save collections. It only \ writes the current game to file. When you try to save, you will be prompted with \ a standard "Save As" dialog. If you save to a file that already exists, that \ file will be DESTROYED and REPLACED with a file containing only the current \ game. You can enable save in the dev menu.`; exports.engine_not_present = `Engine not found. Please find the engine via the \ Engine menu. You might also need to locate the weights (neural network) file.`; exports.engine_failed_to_start = `Engine failed to start.`; exports.uncaught_exception = `There may have been an uncaught exception. If you \ could open the dev tools and the console tab therein, and report the contents to \ the author (ideally with a screenshot) that would be grand.`; exports.renderer_crash = `The renderer process has crashed. Experience suggests \ this happens when Leela runs out of RAM. If this doesn't apply, please tell the \ author how you made it happen.`; exports.renderer_hang = `The renderer process may have hung. Please tell the \ author how you made this happen.`; exports.about_sizes = `You can get more fine-grained control of font, board, \ graph, and window sizes via Nibbler's config file (which can be found via the \ Dev menu).`; exports.about_hashes = `You can set the Hash value directly via Nibbler's \ ${engineconfig_io.filename} file (which can be found via the Dev menu).`; exports.thread_warning = `Note that, for systems using a GPU, 2 threads is usually \ sufficient for Leela, and increasing this number can actually make Leela weaker! \ More threads should probably only be used on CPU-only systems, if at all. If no tick is present in this menu, the default is being used, which is probably \ what you want.`; exports.adding_scripts = `Nibbler has a scripts folder, inside which you can \ place scripts of raw input to send to the engine. A small example file is \ provided. This is for advanced users and devs who understand the UCI protocol. Note that this is for configuration only.`; exports.invalid_script = `Bad script; scripts are for configuration only.`; exports.wrong_engine_exe = `That is almost certainly the wrong file. What we \ need is likely to be called lc0.exe or lc0.`; exports.send_fail = `Sending to the engine failed. This usually means it has \ crashed.`; exports.invalid_pieces_directory = `Did not find all pieces required!`; exports.about_custom_pieces = `To use a custom piece set, select a folder \ containing SVG or PNG files with names such as "Q.png" (or "Q.svg") for white \ and "_Q.png" (or "_Q.svg") for black.`; exports.desync = `Desync... (restart engine via Engine menu)`; exports.c960_warning = `We appear to have entered a game of Chess960, however \ this engine does not support Chess960. Who knows what will happen. Probably not \ good things. Maybe bad things.`; exports.bad_bin_book = `This book contained unsorted keys and is therefore not a \ valid Polyglot book.`; exports.file_too_big = `Sorry, this file is probably too large to be safely \ loaded in Nibbler. If you want, you can suppress this warning in the Dev menu, \ and try to load the file anyway.`; exports.pgn_book_too_big = `This file is impractically large for a PGN book - \ consider converting it to Polyglot (.bin) format. If you want, you can suppress \ this warning in the Dev menu, and try to load the file anyway.`; exports.engine_options_reset = `As of v2.1.1, Nibbler will store engine options \ separately for each engine. To facilite this, your engine options have been \ reset. If you were using special (hand-edited) options, they are still present \ in your ${config_io.filename} file, and can be manually moved to \ ${engineconfig_io.filename}.`; exports.too_soon_to_set_options = `Please wait till the engine has loaded before \ setting options.`; exports.lichess_token_needed = `This likely will not work until you set a Lichess \ API token.`; ================================================ FILE: files/src/modules/running_as_electron.js ================================================ "use strict"; const path = require("path"); // Is there not a better way? Perhaps some Electron API somewhere? module.exports = () => { return path.basename(process.argv[0]).toLowerCase().includes("electron"); }; ================================================ FILE: files/src/modules/stringify.js ================================================ "use strict"; // Given anything, create a string from it. // Helps with sending messages over IPC, displaying alerts, etc. module.exports = (msg) => { try { if (msg instanceof Error) { msg = msg.toString(); } if (typeof msg === "object") { msg = JSON.stringify(msg); } if (typeof msg === "undefined") { msg = "undefined"; } msg = msg.toString().trim(); return msg; } catch (err) { return "stringify() failed"; } }; ================================================ FILE: files/src/modules/translate.js ================================================ "use strict"; const translations = require("./translations"); let startup_language = null; exports.register_startup_language = function(s) { // Will have to be called in both processes (assuming renderer ever uses this at all). startup_language = s; } exports.translate = function(key, force_language = null) { // Note that we usually use the language which was in config.json at startup so // that in-flight calls to translate() return consistent results even if the user // switches config.language at some point. (Thus, the user will need to restart // to see any change.) let language = force_language || startup_language; if (translations[language] && translations[language][key]) { return translations[language][key]; } else { return key; } } exports.t = exports.translate; exports.all_languages = function() { return Object.keys(translations); } ================================================ FILE: files/src/modules/translations.js ================================================ "use strict"; const config_io = require("./config_io"); const engineconfig_io = require("./engineconfig_io"); // Some special fake dynamic keys (not actually dynamic in any way)... const show_config = `Show ${config_io.filename}`; const show_engineconfig = `Show ${engineconfig_io.filename}`; const reload_engineconfig = `Reload ${engineconfig_io.filename} (and restart engine)`; // In general keys will just be the English version. // Exceptions will be in UPPERCASE_FORMAT. let translations = { "English": { "RESTART_REQUIRED": "The GUI must now be restarted.", }, // FRENCH ..................................................................................... "Français": { "File": "Fichier", "About": "À propos", "New game": "Nouvelle partie", "New 960 game": "Nouvelle partie 960", "Open PGN...": "Ouvrir PGN...", "Load FEN / PGN from clipboard": "Charger FEN / PGN du presse-papiers", "Save this game...": "Sauvegarder cette partie...", "Write PGN to clipboard": "Copier PGN dans le presse-papiers", "PGN saved statistics": "Statistiques PGN sauvegardées", "EV": "EV", "Centipawns": "Centipions", "N (%)": "N (%)", "N (absolute)": "N (absolu)", "...out of total": "...sur total", "Depth (A/B only)": "Profondeur (A/B uniquement)", "Cut": "Couper", "Copy": "Copier", "Paste": "Coller", "Quit": "Quitter", "Tree": "Arbre", "Play engine choice": "Jouer le choix du moteur", "1st": "1er", "2nd": "2ème", "3rd": "3ème", "4th": "4ème", "Root": "Racine", "End": "Fin", "Backward": "Reculer", "Forward": "Avancer", "Previous sibling": "Variante précédente", "Next sibling": "Variante suivante", "Return to main line": "Retour à la ligne principale", "Promote line to main line": "Promouvoir en ligne principale", "Promote line by 1 level": "Promouvoir d'un niveau", "Delete node": "Supprimer le nœud", "Delete children": "Supprimer les variantes", "Delete siblings": "Supprimer les alternatives", "Delete ALL other lines": "Supprimer TOUTES les autres lignes", "Show PGN games list": "Afficher la liste des parties PGN", "Escape": "Échapper", "Analysis": "Analyse", "Go": "Démarrer", "Go and lock engine": "Démarrer et verrouiller le moteur", "Return to locked position": "Retour à la position verrouillée", "Halt": "Arrêter", "Auto-evaluate line": "Auto-évaluer la ligne", "Auto-evaluate line, backwards": "Auto-évaluer la ligne, en arrière", "Show focus (searchmoves) buttons": "Afficher les boutons de focus", "Clear focus": "Effacer le focus", "Invert focus": "Inverser le focus", "Winrate POV": "PDV taux de victoire", "Current": "Actuel", "White": "Blancs", "Black": "Noirs", "Centipawn POV": "PDV centipions", "Win / draw / loss POV": "PDV gain / nulle / perte", "PV clicks": "Clics PV", "Do nothing": "Ne rien faire", "Go there": "Y aller", "Add to tree": "Ajouter à l'arbre", "Write infobox to clipboard": "Copier infobox dans le presse-papiers", "Forget all analysis": "Oublier toutes les analyses", "Display": "Affichage", "Flip board": "Retourner l'échiquier", "Arrows": "Flèches", "Piece-click spotlight": "Surbrillance pièce-clic", "Always show actual move (if known)": "Toujours montrer le coup joué (si connu)", "...with unique colour": "...avec couleur unique", "...with outline": "...avec contour", "Arrowhead type": "Type de flèche", "Winrate": "Taux de victoire", "Node %": "Nœud %", "Policy": "Policy", "MultiPV rank": "Rang MultiPV", "Moves Left Head": "Coups restants", "Arrow filter (Lc0)": "Filtre flèches (Lc0)", "All moves": "Tous les coups", "Top move": "Meilleur coup", "Arrow filter (others)": "Filtre flèches (autres)", "Diff < 15%": "Diff < 15%", "Diff < 10%": "Diff < 10%", "Diff < 5%": "Diff < 5%", "Infobox stats": "Stats infobox", "N - nodes (%)": "N - nœuds (%)", "N - nodes (absolute)": "N - nœuds (absolu)", "P - policy": "P - Policy", "V - static evaluation": "V - évaluation statique", "Q - evaluation": "Q - évaluation", "U - uncertainty": "U - incertitude", "S - search priority": "S - priorité de recherche", "M - moves left": "M - coups restants", "WDL - win / draw / loss": "WDL - gain / nulle / perte", "Linebreak before stats": "Retour à la ligne avant stats", "PV move numbers": "Numéros des coups PV", "Online API": "API en ligne", "None": "Aucun", "ChessDB.cn evals": "Évals ChessDB.cn", "Lichess results (masters)": "Résultats Lichess (maîtres)", "Lichess results (plebs)": "Résultats Lichess (ordinaires)", "Set Lichess API token": "Définir le jeton API Lichess", "Allow API after move 25": "Autoriser API après coup 25", "Draw PV on mouseover": "Afficher PV au survol", "Draw PV method": "Méthode d'affichage PV", "Animate": "Animer", "Single move": "Coup unique", "Final position": "Position finale", "Pieces": "Pièces", "Choose pieces folder...": "Choisir dossier pièces...", "Default": "Par défaut", "About custom pieces": "À propos des pièces personnalisées", "Background": "Arrière-plan", "Choose background image...": "Choisir image d'arrière-plan...", "Book frequency arrows": "Flèches fréquence bibliothèque", "Lichess frequency arrows": "Flèches fréquence Lichess", "Sizes": "Tailles", "Infobox font": "Police infobox", "Move history font": "Police historique coups", "Board": "Échiquier", "Giant": "Géant", "Large": "Grand", "Medium": "Moyen", "Small": "Petit", "Graph": "Graphique", "Graph lines": "Lignes graphique", "I want other size options!": "Je veux d'autres options de taille !", "Engine": "Moteur", "Choose engine...": "Choisir moteur...", "Choose known engine...": "Choisir moteur connu...", "Weights": "Poids", "Lc0 WeightsFile...": "Fichier poids Lc0...", "Stockfish EvalFile...": "Fichier éval Stockfish...", "Set to ": "Définir sur ", "Backend": "Backend", "Choose Syzygy path...": "Choisir chemin Syzygy...", "Unset": "Désactiver", "Limit - normal": "Limite - normal", "Unlimited": "Illimité", "Up slightly": "Augmenter légèrement", "Down slightly": "Diminuer légèrement", "Limit - auto-eval / play": "Limite - auto-éval / jeu", "Limit by time instead of nodes": "Limiter par temps au lieu des nœuds", "Threads": "Threads", "Warning about threads": "Avertissement threads", "Hash": "Hash", "I want other hash options!": "Je veux d'autres options de hash !", "MultiPV": "MultiPV", "Contempt Mode": "Mode mépris", "White analysis": "Analyse blancs", "Black analysis": "Analyse noirs", "Contempt": "Mépris", "WDL Calibration Elo": "Calibration Elo WDL", "Use default WDL": "Utiliser WDL par défaut", "WDL Eval Objectivity": "Objectivité éval WDL", "Yes": "Oui", "No": "Non", "Score Type": "Type de score", "Custom scripts": "Scripts personnalisés", "How to add scripts": "Comment ajouter des scripts", "Show scripts folder": "Afficher dossier scripts", "Restart engine": "Redémarrer moteur", "Soft engine reset": "Réinitialisation douce moteur", "Play": "Jouer", "Play this colour": "Jouer cette couleur", "Start self-play": "Démarrer auto-jeu", "Use Polyglot book...": "Utiliser bibliothèque Polyglot...", "Use PGN book...": "Utiliser bibliothèque PGN...", "Unload book / abort load": "Décharger biblio / annuler chargement", "Book depth limit": "Limite profondeur bibliothèque", "Temperature": "Température", "Temp Decay Moves": "Décroissance temp. coups", "Infinite": "Infini", "About play modes": "À propos des modes de jeu", "Dev": "Dev", "Toggle Developer Tools": "Basculer outils développeur", "Toggle Debug CSS": "Basculer CSS debug", "Permanently enable save": "Activer sauvegarde permanente", [show_config]: "Afficher config.json", [show_engineconfig]: "Afficher engines.json", [reload_engineconfig]: "Recharger engines.json (et redémarrer moteur)", "Random move": "Coup aléatoire", "Disable hardware acceleration for GUI": "Désactiver accélération matérielle GUI", "Spin rate": "Taux rotation", "Frenetic": "Frénétique", "Fast": "Rapide", "Normal": "Normal", "Relaxed": "Détendu", "Lazy": "Paresseux", "Show engine state": "Afficher état moteur", "List sent options": "Lister options envoyées", "Show error log": "Afficher journal erreurs", "Hacks and kludges": "Hacks et bidouilles", "Allow arbitrary scripts": "Autoriser scripts arbitraires", "Accept any file size": "Accepter toute taille fichier", "Allow stopped analysis": "Autoriser analyse arrêtée", "Never hide focus buttons": "Ne jamais cacher boutons focus", "Never grayout move info": "Ne jamais griser info coup", "Use lowerbound / upperbound info": "Utiliser info borne inf/sup", "Suppress ucinewgame": "Supprimer ucinewgame", "Log RAM state to console": "Logger état RAM console", "Fire GC": "Lancer GC", "Logging": "Journalisation", "Use logfile...": "Utiliser fichier journal...", "Disable logging": "Désactiver journalisation", "Clear log when opening": "Effacer journal à l'ouverture", "Use unique logfile each time": "Utiliser fichier journal unique", "Log illegal moves": "Logger coups illégaux", "Log positions": "Logger positions", "Log info lines": "Logger lignes info", "...including useless lines": "...lignes inutiles incluses", "Language": "Langue", "RESTART_REQUIRED": "L'interface doit maintenant être redémarrée.", }, // SPANISH .................................................................................... "Español": { "File": "Archivo", "About": "Acerca de", "New game": "Nueva partida", "New 960 game": "Nueva partida 960", "Open PGN...": "Abrir PGN...", "Load FEN / PGN from clipboard": "Cargar FEN / PGN del portapapeles", "Save this game...": "Guardar esta partida...", "Write PGN to clipboard": "Copiar PGN al portapapeles", "PGN saved statistics": "Estadísticas guardadas en PGN", "EV": "EV", "Centipawns": "Centipeones", "N (%)": "N (%)", "N (absolute)": "N (absoluto)", "...out of total": "...del total", "Depth (A/B only)": "Profundidad (solo A/B)", "Cut": "Cortar", "Copy": "Copiar", "Paste": "Pegar", "Quit": "Salir", "Tree": "Árbol", "Play engine choice": "Jugar elección del motor", "1st": "1ro", "2nd": "2do", "3rd": "3ro", "4th": "4to", "Root": "Raíz", "End": "Final", "Backward": "Atrás", "Forward": "Adelante", "Previous sibling": "Variante anterior", "Next sibling": "Variante siguiente", "Return to main line": "Volver a línea principal", "Promote line to main line": "Promover a línea principal", "Promote line by 1 level": "Promover línea 1 nivel", "Delete node": "Eliminar nodo", "Delete children": "Eliminar variantes", "Delete siblings": "Eliminar hermanos", "Delete ALL other lines": "Eliminar TODAS las otras líneas", "Show PGN games list": "Mostrar lista de partidas PGN", "Escape": "Esc", "Analysis": "Análisis", "Go": "Iniciar", "Go and lock engine": "Iniciar y bloquear motor", "Return to locked position": "Volver a posición bloqueada", "Halt": "Detener", "Auto-evaluate line": "Auto-evaluar línea", "Auto-evaluate line, backwards": "Auto-evaluar línea, hacia atrás", "Show focus (searchmoves) buttons": "Mostrar botones de enfoque", "Clear focus": "Limpiar enfoque", "Invert focus": "Invertir enfoque", "Winrate POV": "Perspectiva de victoria", "Current": "Actual", "White": "Blancas", "Black": "Negras", "Centipawn POV": "Perspectiva de centipeones", "Win / draw / loss POV": "Perspectiva victoria / tablas / derrota", "PV clicks": "Clics en VP", "Do nothing": "No hacer nada", "Go there": "Ir allí", "Add to tree": "Añadir al árbol", "Write infobox to clipboard": "Copiar info al portapapeles", "Forget all analysis": "Olvidar todo el análisis", "Display": "Visualización", "Flip board": "Girar tablero", "Arrows": "Flechas", "Piece-click spotlight": "Resaltar movimientos legales", "Always show actual move (if known)": "Mostrar siempre jugada real", "...with unique colour": "...con color único", "...with outline": "...con contorno", "Arrowhead type": "Tipo de flecha", "Winrate": "Porcentaje victoria", "Node %": "Nodo %", "Policy": "Política", "MultiPV rank": "Rango MultiPV", "Moves Left Head": "Jugadas restantes", "Arrow filter (Lc0)": "Filtro flechas (Lc0)", "All moves": "Todas las jugadas", "Top move": "Mejor jugada", "Arrow filter (others)": "Filtro flechas (otros)", "Diff < 15%": "Dif < 15%", "Diff < 10%": "Dif < 10%", "Diff < 5%": "Dif < 5%", "Infobox stats": "Estadísticas", "N - nodes (%)": "N - nodos (%)", "N - nodes (absolute)": "N - nodos (absoluto)", "P - policy": "P - política", "V - static evaluation": "V - evaluación estática", "Q - evaluation": "Q - evaluación", "U - uncertainty": "U - incertidumbre", "S - search priority": "S - prioridad búsqueda", "M - moves left": "M - jugadas restantes", "WDL - win / draw / loss": "WDL - victoria / tablas / derrota", "Linebreak before stats": "Salto línea antes de stats", "PV move numbers": "Números de jugada VP", "Online API": "API en línea", "None": "Ninguno", "ChessDB.cn evals": "Evaluaciones ChessDB.cn", "Lichess results (masters)": "Resultados Lichess (maestros)", "Lichess results (plebs)": "Resultados Lichess (usuarios)", "Set Lichess API token": "Establecer token API de Lichess", "Allow API after move 25": "Permitir API después jugada 25", "Draw PV on mouseover": "Mostrar VP al pasar ratón", "Draw PV method": "Método dibujo VP", "Animate": "Animar", "Single move": "Jugada única", "Final position": "Posición final", "Pieces": "Piezas", "Choose pieces folder...": "Elegir carpeta piezas...", "Default": "Predeterminado", "About custom pieces": "Sobre piezas personalizadas", "Background": "Fondo", "Choose background image...": "Elegir imagen de fondo...", "Book frequency arrows": "Flechas frecuencia libro", "Lichess frequency arrows": "Flechas frecuencia Lichess", "Sizes": "Tamaños", "Infobox font": "Fuente info", "Move history font": "Fuente historial", "Board": "Tablero", "Giant": "Gigante", "Large": "Grande", "Medium": "Mediano", "Small": "Pequeño", "Graph": "Gráfico", "Graph lines": "Líneas gráfico", "I want other size options!": "¡Quiero otras opciones de tamaño!", "Engine": "Motor", "Choose engine...": "Elegir motor...", "Choose known engine...": "Elegir motor conocido...", "Weights": "Pesos", "Lc0 WeightsFile...": "Archivo pesos Lc0...", "Stockfish EvalFile...": "Archivo eval Stockfish...", "Set to ": "Establecer a ", "Backend": "Backend", "Choose Syzygy path...": "Elegir ruta Syzygy...", "Unset": "Desactivar", "Limit - normal": "Límite - normal", "Unlimited": "Ilimitado", "Up slightly": "Aumentar poco", "Down slightly": "Reducir poco", "Limit - auto-eval / play": "Límite - auto-eval / juego", "Limit by time instead of nodes": "Limitar por tiempo en vez de nodos", "Threads": "Hilos", "Warning about threads": "Advertencia sobre hilos", "Hash": "Hash", "I want other hash options!": "¡Quiero otras opciones de hash!", "MultiPV": "MultiPV", "Contempt Mode": "Modo desprecio", "White analysis": "Análisis blancas", "Black analysis": "Análisis negras", "Contempt": "Desprecio", "WDL Calibration Elo": "Calibración Elo WDL", "Use default WDL": "Usar WDL predeterminado", "WDL Eval Objectivity": "Objetividad eval WDL", "Yes": "Sí", "No": "No", "Score Type": "Tipo puntuación", "Custom scripts": "Scripts personalizados", "How to add scripts": "Cómo añadir scripts", "Show scripts folder": "Mostrar carpeta scripts", "Restart engine": "Reiniciar motor", "Soft engine reset": "Reinicio suave motor", "Play": "Jugar", "Play this colour": "Jugar este color", "Start self-play": "Iniciar auto-juego", "Use Polyglot book...": "Usar libro Polyglot...", "Use PGN book...": "Usar libro PGN...", "Unload book / abort load": "Descargar libro / abortar carga", "Book depth limit": "Límite profundidad libro", "Temperature": "Temperatura", "Temp Decay Moves": "Decaimiento temp jugadas", "Infinite": "Infinito", "About play modes": "Sobre modos juego", "Dev": "Desarrollo", "Toggle Developer Tools": "Alternar herramientas desarrollo", "Toggle Debug CSS": "Alternar CSS depuración", "Permanently enable save": "Habilitar guardado permanente", [show_config]: "Mostrar config.json", [show_engineconfig]: "Mostrar engines.json", [reload_engineconfig]: "Recargar engines.json (y reiniciar motor)", "Random move": "Jugada aleatoria", "Disable hardware acceleration for GUI": "Desactivar aceleración hardware GUI", "Spin rate": "Velocidad actualización", "Frenetic": "Frenético", "Fast": "Rápido", "Normal": "Normal", "Relaxed": "Relajado", "Lazy": "Perezoso", "Show engine state": "Mostrar estado motor", "List sent options": "Listar opciones enviadas", "Show error log": "Mostrar registro errores", "Hacks and kludges": "Trucos y parches", "Allow arbitrary scripts": "Permitir scripts arbitrarios", "Accept any file size": "Aceptar cualquier tamaño archivo", "Allow stopped analysis": "Permitir análisis detenido", "Never hide focus buttons": "Nunca ocultar botones enfoque", "Never grayout move info": "Nunca atenuar info jugadas", "Use lowerbound / upperbound info": "Usar info límite inferior/superior", "Suppress ucinewgame": "Suprimir ucinewgame", "Log RAM state to console": "Registrar estado RAM en consola", "Fire GC": "Ejecutar GC", "Logging": "Registro", "Use logfile...": "Usar archivo registro...", "Disable logging": "Desactivar registro", "Clear log when opening": "Limpiar registro al abrir", "Use unique logfile each time": "Usar archivo registro único", "Log illegal moves": "Registrar jugadas ilegales", "Log positions": "Registrar posiciones", "Log info lines": "Registrar líneas info", "...including useless lines": "...incluyendo líneas inútiles", "Language": "Idioma", "RESTART_REQUIRED": "La interfaz debe reiniciarse ahora." }, // PORTUGUESE ................................................................................. "Português": { "File": "Ficheiro", "About": "Sobre", "New game": "Novo jogo", "New 960 game": "Novo jogo 960", "Open PGN...": "Abrir PGN...", "Load FEN / PGN from clipboard": "Carregar FEN / PGN da área de transferência", "Save this game...": "Guardar este jogo...", "Write PGN to clipboard": "Copiar PGN para área de transferência", "PGN saved statistics": "Estatísticas guardadas no PGN", "EV": "EV", "Centipawns": "Centipawns", "N (%)": "N (%)", "N (absolute)": "N (absoluto)", "...out of total": "...do total", "Depth (A/B only)": "Profundidade (apenas A/B)", "Cut": "Cortar", "Copy": "Copiar", "Paste": "Colar", "Quit": "Sair", "Tree": "Árvore", "Play engine choice": "Jogar escolha do motor", "1st": "1º", "2nd": "2º", "3rd": "3º", "4th": "4º", "Root": "Raiz", "End": "Fim", "Backward": "Recuar", "Forward": "Avançar", "Previous sibling": "Variante anterior", "Next sibling": "Variante seguinte", "Return to main line": "Voltar à linha principal", "Promote line to main line": "Promover linha a principal", "Promote line by 1 level": "Promover linha 1 nível", "Delete node": "Eliminar nó", "Delete children": "Eliminar descendentes", "Delete siblings": "Eliminar variantes", "Delete ALL other lines": "Eliminar TODAS as outras linhas", "Show PGN games list": "Mostrar lista de jogos PGN", "Escape": "Sair", "Analysis": "Análise", "Go": "Iniciar", "Go and lock engine": "Iniciar e fixar motor", "Return to locked position": "Voltar à posição fixa", "Halt": "Parar", "Auto-evaluate line": "Auto-avaliar linha", "Auto-evaluate line, backwards": "Auto-avaliar linha, inversamente", "Show focus (searchmoves) buttons": "Mostrar botões de foco (searchmoves)", "Clear focus": "Limpar foco", "Invert focus": "Inverter foco", "Winrate POV": "POV taxa de vitória", "Current": "Atual", "White": "Brancas", "Black": "Pretas", "Centipawn POV": "POV centipawns", "Win / draw / loss POV": "POV vitória / empate / derrota", "PV clicks": "Cliques PV", "Do nothing": "Não fazer nada", "Go there": "Ir para lá", "Add to tree": "Adicionar à árvore", "Write infobox to clipboard": "Copiar caixa de informação", "Forget all analysis": "Esquecer toda a análise", "Display": "Exibir", "Flip board": "Rodar tabuleiro", "Arrows": "Setas", "Piece-click spotlight": "Destaque ao clicar peça", "Always show actual move (if known)": "Mostrar sempre lance atual (se conhecido)", "...with unique colour": "...com cor única", "...with outline": "...com contorno", "Arrowhead type": "Tipo de seta", "Winrate": "Taxa de vitória", "Node %": "% nós", "Policy": "Política", "MultiPV rank": "Rank MultiPV", "Moves Left Head": "Lances restantes", "Arrow filter (Lc0)": "Filtro de setas (Lc0)", "All moves": "Todos os lances", "Top move": "Melhor lance", "Arrow filter (others)": "Filtro de setas (outros)", "Diff < 15%": "Dif < 15%", "Diff < 10%": "Dif < 10%", "Diff < 5%": "Dif < 5%", "Infobox stats": "Estatísticas", "N - nodes (%)": "N - nós (%)", "N - nodes (absolute)": "N - nós (absoluto)", "P - policy": "P - política", "V - static evaluation": "V - avaliação estática", "Q - evaluation": "Q - avaliação", "U - uncertainty": "U - incerteza", "S - search priority": "S - prioridade de busca", "M - moves left": "M - lances restantes", "WDL - win / draw / loss": "WDL - vitória / empate / derrota", "Linebreak before stats": "Quebra de linha antes das estatísticas", "PV move numbers": "Números dos lances PV", "Online API": "API online", "None": "Nenhum", "ChessDB.cn evals": "Avaliações ChessDB.cn", "Lichess results (masters)": "Resultados Lichess (mestres)", "Lichess results (plebs)": "Resultados Lichess (amadores)", "Set Lichess API token": "Definir token API do Lichess", "Allow API after move 25": "Permitir API após lance 25", "Draw PV on mouseover": "Mostrar PV ao passar rato", "Draw PV method": "Método de mostrar PV", "Animate": "Animar", "Single move": "Lance único", "Final position": "Posição final", "Pieces": "Peças", "Choose pieces folder...": "Escolher pasta de peças...", "Default": "Padrão", "About custom pieces": "Sobre peças personalizadas", "Background": "Fundo", "Choose background image...": "Escolher imagem de fundo...", "Book frequency arrows": "Setas de frequência do livro", "Lichess frequency arrows": "Setas de frequência do Lichess", "Sizes": "Tamanhos", "Infobox font": "Fonte da caixa de informação", "Move history font": "Fonte do histórico de lances", "Board": "Tabuleiro", "Giant": "Gigante", "Large": "Grande", "Medium": "Médio", "Small": "Pequeno", "Graph": "Gráfico", "Graph lines": "Linhas do gráfico", "I want other size options!": "Quero outras opções de tamanho!", "Engine": "Motor", "Choose engine...": "Escolher motor...", "Choose known engine...": "Escolher motor conhecido...", "Weights": "Pesos", "Lc0 WeightsFile...": "Ficheiro de pesos Lc0...", "Stockfish EvalFile...": "Ficheiro de avaliação Stockfish...", "Set to ": "Definir como ", "Backend": "Backend", "Choose Syzygy path...": "Escolher caminho Syzygy...", "Unset": "Remover", "Limit - normal": "Limite - normal", "Unlimited": "Ilimitado", "Up slightly": "Aumentar ligeiramente", "Down slightly": "Diminuir ligeiramente", "Limit - auto-eval / play": "Limite - auto-avaliação / jogo", "Limit by time instead of nodes": "Limitar por tempo em vez de nós", "Threads": "Threads", "Warning about threads": "Aviso sobre threads", "Hash": "Hash", "I want other hash options!": "Quero outras opções de hash!", "MultiPV": "MultiPV", "Contempt Mode": "Modo de desprezo", "White analysis": "Análise das brancas", "Black analysis": "Análise das pretas", "Contempt": "Desprezo", "WDL Calibration Elo": "Calibração WDL Elo", "Use default WDL": "Usar WDL padrão", "WDL Eval Objectivity": "Objetividade da avaliação WDL", "Yes": "Sim", "No": "Não", "Score Type": "Tipo de pontuação", "Custom scripts": "Scripts personalizados", "How to add scripts": "Como adicionar scripts", "Show scripts folder": "Mostrar pasta de scripts", "Restart engine": "Reiniciar motor", "Soft engine reset": "Reinicialização suave do motor", "Play": "Jogar", "Play this colour": "Jogar esta cor", "Start self-play": "Iniciar auto-jogo", "Use Polyglot book...": "Usar livro Polyglot...", "Use PGN book...": "Usar livro PGN...", "Unload book / abort load": "Descarregar livro / abortar carregamento", "Book depth limit": "Limite de profundidade do livro", "Temperature": "Temperatura", "Temp Decay Moves": "Decaimento de temperatura", "Infinite": "Infinito", "About play modes": "Sobre modos de jogo", "Dev": "Dev", "Toggle Developer Tools": "Alternar ferramentas de desenvolvimento", "Toggle Debug CSS": "Alternar CSS de depuração", "Permanently enable save": "Ativar guardar permanentemente", [show_config]: "Mostrar config.json", [show_engineconfig]: "Mostrar engines.json", [reload_engineconfig]: "Recarregar engines.json (e reiniciar motor)", "Random move": "Lance aleatório", "Disable hardware acceleration for GUI": "Desativar aceleração de hardware para GUI", "Spin rate": "Taxa de atualização", "Frenetic": "Frenético", "Fast": "Rápido", "Normal": "Normal", "Relaxed": "Relaxado", "Lazy": "Preguiçoso", "Show engine state": "Mostrar estado do motor", "List sent options": "Listar opções enviadas", "Show error log": "Mostrar registo de erros", "Hacks and kludges": "Hacks e gambiarras", "Allow arbitrary scripts": "Permitir scripts arbitrários", "Accept any file size": "Aceitar qualquer tamanho de ficheiro", "Allow stopped analysis": "Permitir análise parada", "Never hide focus buttons": "Nunca ocultar botões de foco", "Never grayout move info": "Nunca esmaecer info de lance", "Use lowerbound / upperbound info": "Usar info de limite inferior/superior", "Suppress ucinewgame": "Suprimir ucinewgame", "Log RAM state to console": "Registar estado RAM na consola", "Fire GC": "Executar GC", "Logging": "Registo", "Use logfile...": "Usar ficheiro de registo...", "Disable logging": "Desativar registo", "Clear log when opening": "Limpar registo ao abrir", "Use unique logfile each time": "Usar ficheiro de registo único cada vez", "Log illegal moves": "Registar lances ilegais", "Log positions": "Registar posições", "Log info lines": "Registar linhas de info", "...including useless lines": "...incluindo linhas inúteis", "Language": "Idioma", "RESTART_REQUIRED": "O GUI deve ser reiniciado agora." }, // GERMAN ..................................................................................... "Deutsch": { "File": "Datei", "About": "Über", "New game": "Neue Partie", "New 960 game": "Neue 960-Partie", "Open PGN...": "PGN öffnen...", "Load FEN / PGN from clipboard": "FEN / PGN aus Zwischenablage laden", "Save this game...": "Diese Partie speichern...", "Write PGN to clipboard": "PGN in Zwischenablage kopieren", "PGN saved statistics": "PGN-Statistiken speichern", "EV": "EV", "Centipawns": "Bauerneinheiten", "N (%)": "N (%)", "N (absolute)": "N (absolut)", "...out of total": "...von Gesamt", "Depth (A/B only)": "Tiefe (nur A/B)", "Cut": "Ausschneiden", "Copy": "Kopieren", "Paste": "Einfügen", "Quit": "Beenden", "Tree": "Baum", "Play engine choice": "Engine-Zug spielen", "1st": "1.", "2nd": "2.", "3rd": "3.", "4th": "4.", "Root": "Wurzel", "End": "Ende", "Backward": "Zurück", "Forward": "Vorwärts", "Previous sibling": "Vorheriger Zweig", "Next sibling": "Nächster Zweig", "Return to main line": "Zur Hauptvariante", "Promote line to main line": "Zur Hauptvariante befördern", "Promote line by 1 level": "Um 1 Ebene befördern", "Delete node": "Knoten löschen", "Delete children": "Nachfolger löschen", "Delete siblings": "Geschwister löschen", "Delete ALL other lines": "ALLE anderen Varianten löschen", "Show PGN games list": "PGN-Partieliste anzeigen", "Escape": "Escape", "Analysis": "Analyse", "Go": "Starten", "Go and lock engine": "Starten und Analyseposition fixieren", "Return to locked position": "Zur Analyseposition zurückkehren", "Halt": "Anhalten", "Auto-evaluate line": "Variante automatisch analysieren", "Auto-evaluate line, backwards": "Variante rückwärts analysieren", "Show focus (searchmoves) buttons": "Fokus-Buttons anzeigen", "Clear focus": "Fokus löschen", "Invert focus": "Fokus umkehren", "Winrate POV": "Gewinnrate POV", "Current": "Aktuell", "White": "Weiß", "Black": "Schwarz", "Centipawn POV": "Bauerneinheiten POV", "Win / draw / loss POV": "Sieg / Remis / Niederlage POV", "PV clicks": "PV-Klicks", "Do nothing": "Nichts tun", "Go there": "Hinspringen", "Add to tree": "Zum Baum hinzufügen", "Write infobox to clipboard": "Infobox in Zwischenablage", "Forget all analysis": "Analyse zurücksetzen", "Display": "Anzeige", "Flip board": "Brett drehen", "Arrows": "Pfeile", "Piece-click spotlight": "Zugmöglichkeiten markieren", "Always show actual move (if known)": "Ausgeführten Zug anzeigen", "...with unique colour": "...in eigener Farbe", "...with outline": "...mit Umriss", "Arrowhead type": "Pfeilspitzen-Typ", "Winrate": "Gewinnrate", "Node %": "Knoten %", "Policy": "Policy", "MultiPV rank": "MultiPV-Rang", "Moves Left Head": "Verbleibende Züge", "Arrow filter (Lc0)": "Pfeilfilter (Lc0)", "All moves": "Alle Züge", "Top move": "Bester Zug", "Arrow filter (others)": "Pfeilfilter (andere)", "Diff < 15%": "Diff < 15%", "Diff < 10%": "Diff < 10%", "Diff < 5%": "Diff < 5%", "Infobox stats": "Infobox-Statistiken", "N - nodes (%)": "N - Knoten (%)", "N - nodes (absolute)": "N - Knoten (absolut)", "P - policy": "P - Policy", "V - static evaluation": "V - Statische Bewertung", "Q - evaluation": "Q - Bewertung", "U - uncertainty": "U - Unsicherheit", "S - search priority": "S - Suchpriorität", "M - moves left": "M - Verbleibende Züge", "WDL - win / draw / loss": "WDL - Sieg / Remis / Niederlage", "Linebreak before stats": "Zeilenumbruch vor Statistiken", "PV move numbers": "PV-Zugnummern", "Online API": "Online-API", "None": "Keine", "ChessDB.cn evals": "ChessDB.cn Bewertungen", "Lichess results (masters)": "Lichess Ergebnisse (Meister)", "Lichess results (plebs)": "Lichess Ergebnisse (Amateure)", "Set Lichess API token": "Lichess-API-Token festlegen", "Allow API after move 25": "API nach Zug 25 erlauben", "Draw PV on mouseover": "PV bei Mausberührung", "Draw PV method": "PV-Anzeigemethode", "Animate": "Animieren", "Single move": "Einzelzug", "Final position": "Endposition", "Pieces": "Figuren", "Choose pieces folder...": "Figurenordner wählen...", "Default": "Standard", "About custom pieces": "Über eigene Figuren", "Background": "Hintergrund", "Choose background image...": "Hintergrundbild wählen...", "Book frequency arrows": "Eröffnungsbuch-Häufigkeitspfeile", "Lichess frequency arrows": "Lichess-Häufigkeitspfeile", "Sizes": "Größen", "Infobox font": "Infobox-Schrift", "Move history font": "Zugverlauf-Schrift", "Board": "Brett", "Giant": "Riesig", "Large": "Groß", "Medium": "Mittel", "Small": "Klein", "Graph": "Graph", "Graph lines": "Graphlinien", "I want other size options!": "Ich möchte andere Größenoptionen!", "Engine": "Engine", "Choose engine...": "Engine wählen...", "Choose known engine...": "Bekannte Engine wählen...", "Weights": "Gewichte", "Lc0 WeightsFile...": "Lc0 Gewichtsdatei...", "Stockfish EvalFile...": "Stockfish Evaluierungsdatei...", "Set to ": "Auf setzen", "Backend": "Backend", "Choose Syzygy path...": "Syzygy-Pfad wählen...", "Unset": "Zurücksetzen", "Limit - normal": "Limit - normal", "Unlimited": "Unbegrenzt", "Up slightly": "Leicht erhöhen", "Down slightly": "Leicht verringern", "Limit - auto-eval / play": "Limit - Auto-Eval / Spiel", "Limit by time instead of nodes": "Zeitlimit statt Knotenlimit", "Threads": "Threads", "Warning about threads": "Warnung zu Threads", "Hash": "Hash", "I want other hash options!": "Ich möchte andere Hash-Optionen!", "MultiPV": "MultiPV", "Contempt Mode": "Contempt-Modus", "White analysis": "Weiß-Analyse", "Black analysis": "Schwarz-Analyse", "Contempt": "Contempt", "WDL Calibration Elo": "WDL-Kalibrierungs-Elo", "Use default WDL": "Standard-WDL verwenden", "WDL Eval Objectivity": "WDL-Eval-Objektivität", "Yes": "Ja", "No": "Nein", "Score Type": "Bewertungstyp", "Custom scripts": "Eigene Skripte", "How to add scripts": "Skripte hinzufügen", "Show scripts folder": "Skriptordner anzeigen", "Restart engine": "Engine neustarten", "Soft engine reset": "Engine soft reset", "Play": "Partie", "Play this colour": "Diese Farbe spielen", "Start self-play": "Selbstspiel starten", "Use Polyglot book...": "Polyglot-Buch verwenden...", "Use PGN book...": "PGN-Buch verwenden...", "Unload book / abort load": "Buch entladen / Laden abbrechen", "Book depth limit": "Buchlimit-Tiefe", "Temperature": "Temperatur", "Temp Decay Moves": "Temp-Verfall-Züge", "Infinite": "Unendlich", "About play modes": "Über Spielmodi", "Dev": "Entwicklung", "Toggle Developer Tools": "Entwicklertools ein/aus", "Toggle Debug CSS": "Debug-CSS ein/aus", "Permanently enable save": "Speichern dauerhaft aktivieren", [show_config]: "config.json anzeigen", [show_engineconfig]: "engines.json anzeigen", [reload_engineconfig]: "engines.json neu laden (und Engine neustarten)", "Random move": "Zufallszug", "Disable hardware acceleration for GUI": "Hardware-Beschleunigung für GUI deaktivieren", "Spin rate": "Aktualisierungsrate", "Frenetic": "Frenetisch", "Fast": "Schnell", "Normal": "Normal", "Relaxed": "Entspannt", "Lazy": "Träge", "Show engine state": "Engine-Status anzeigen", "List sent options": "Gesendete Optionen anzeigen", "Show error log": "Fehlerprotokoll anzeigen", "Hacks and kludges": "Hacks und Tricks", "Allow arbitrary scripts": "Beliebige Skripte erlauben", "Accept any file size": "Beliebige Dateigröße akzeptieren", "Allow stopped analysis": "Gestoppte Analyse erlauben", "Never hide focus buttons": "Fokus-Buttons nie verstecken", "Never grayout move info": "Zuginfo nie ausgrauen", "Use lowerbound / upperbound info": "Unter-/Obergrenze-Info verwenden", "Suppress ucinewgame": "ucinewgame unterdrücken", "Log RAM state to console": "RAM-Status protokollieren", "Fire GC": "GC ausführen", "Logging": "Protokollierung", "Use logfile...": "Protokolldatei verwenden...", "Disable logging": "Protokollierung deaktivieren", "Clear log when opening": "Protokoll beim Öffnen löschen", "Use unique logfile each time": "Eindeutige Protokolldatei je Start", "Log illegal moves": "Ungültige Züge protokollieren", "Log positions": "Positionen protokollieren", "Log info lines": "Info-Zeilen protokollieren", "...including useless lines": "...inkl. nutzloser Zeilen", "Language": "Sprache", "RESTART_REQUIRED": "Die GUI muss neu gestartet werden." }, // NORWEGIAN .................................................................................. "Norsk": { "File": "Fil", "About": "Om", "New game": "Nytt parti", "New 960 game": "Nytt 960-parti", "Open PGN...": "Åpne PGN...", "Load FEN / PGN from clipboard": "Last inn FEN / PGN fra utklippstavle", "Save this game...": "Lagre dette partiet...", "Write PGN to clipboard": "Kopier PGN til utklippstavle", "PGN saved statistics": "PGN lagrede statistikker", "EV": "EV", "Centipawns": "Centipawns", "N (%)": "N (%)", "N (absolute)": "N (absolutt)", "...out of total": "...av totalt", "Depth (A/B only)": "Dybde (kun A/B)", "Cut": "Klipp ut", "Copy": "Kopier", "Paste": "Lim inn", "Quit": "Avslutt", "Tree": "Tre", "Play engine choice": "Spill motorens valg", "1st": "1.", "2nd": "2.", "3rd": "3.", "4th": "4.", "Root": "Rot", "End": "Slutt", "Backward": "Tilbake", "Forward": "Fremover", "Previous sibling": "Forrige variant", "Next sibling": "Neste variant", "Return to main line": "Tilbake til hovedlinjen", "Promote line to main line": "Forfrem til hovedlinje", "Promote line by 1 level": "Forfrem linje ett nivå", "Delete node": "Slett node", "Delete children": "Slett barn", "Delete siblings": "Slett søsken", "Delete ALL other lines": "Slett ALLE andre linjer", "Show PGN games list": "Vis PGN-partiliste", "Escape": "Escape", "Analysis": "Analyse", "Go": "Start", "Go and lock engine": "Start og lås motor", "Return to locked position": "Tilbake til låst stilling", "Halt": "Stopp", "Auto-evaluate line": "Auto-evaluer linje", "Auto-evaluate line, backwards": "Auto-evaluer linje bakover", "Show focus (searchmoves) buttons": "Vis fokus-knapper (searchmoves)", "Clear focus": "Fjern fokus", "Invert focus": "Inverter fokus", "Winrate POV": "Vinstrate POV", "Current": "Nåværende", "White": "Hvit", "Black": "Svart", "Centipawn POV": "Centipawn POV", "Win / draw / loss POV": "Vinn / remis / tap POV", "PV clicks": "PV-klikk", "Do nothing": "Gjør ingenting", "Go there": "Gå dit", "Add to tree": "Legg til i tre", "Write infobox to clipboard": "Kopier infoboks til utklippstavle", "Forget all analysis": "Glem all analyse", "Display": "Visning", "Flip board": "Snu brett", "Arrows": "Piler", "Piece-click spotlight": "Brikke-klikk spotlight", "Always show actual move (if known)": "Alltid vis faktisk trekk (hvis kjent)", "...with unique colour": "...med unik farge", "...with outline": "...med omriss", "Arrowhead type": "Pilhode-type", "Winrate": "Vinstrate", "Node %": "Node %", "Policy": "Policy", "MultiPV rank": "MultiPV rang", "Moves Left Head": "Gjenværende trekk", "Arrow filter (Lc0)": "Pilfilter (Lc0)", "All moves": "Alle trekk", "Top move": "Beste trekk", "Arrow filter (others)": "Pilfilter (andre)", "Diff < 15%": "Diff < 15%", "Diff < 10%": "Diff < 10%", "Diff < 5%": "Diff < 5%", "Infobox stats": "Infoboks-statistikk", "N - nodes (%)": "N - noder (%)", "N - nodes (absolute)": "N - noder (absolutt)", "P - policy": "P - policy", "V - static evaluation": "V - statisk evaluering", "Q - evaluation": "Q - evaluering", "U - uncertainty": "U - usikkerhet", "S - search priority": "S - søkeprioritet", "M - moves left": "M - trekk igjen", "WDL - win / draw / loss": "WDL - vinn / remis / tap", "Linebreak before stats": "Linjeskift før statistikk", "PV move numbers": "PV trekknumre", "Online API": "Online API", "None": "Ingen", "ChessDB.cn evals": "ChessDB.cn evalueringer", "Lichess results (masters)": "Lichess resultater (mestre)", "Lichess results (plebs)": "Lichess resultater (amatører)", "Set Lichess API token": "Angi Lichess API-token", "Allow API after move 25": "Tillat API etter trekk 25", "Draw PV on mouseover": "Vis PV ved musepeker", "Draw PV method": "PV visningsmetode", "Animate": "Animer", "Single move": "Enkelt trekk", "Final position": "Sluttstilling", "Pieces": "Brikker", "Choose pieces folder...": "Velg brikkemappe...", "Default": "Standard", "About custom pieces": "Om egendefinerte brikker", "Background": "Bakgrunn", "Choose background image...": "Velg bakgrunnsbilde...", "Book frequency arrows": "Åpningsbokfrekvens-piler", "Lichess frequency arrows": "Lichess-frekvens-piler", "Sizes": "Størrelser", "Infobox font": "Infoboks-skrift", "Move history font": "Trekkhistorie-skrift", "Board": "Brett", "Giant": "Kjempe", "Large": "Stor", "Medium": "Medium", "Small": "Liten", "Graph": "Graf", "Graph lines": "Graf-linjer", "I want other size options!": "Jeg vil ha andre størrelsesvalg!", "Engine": "Motor", "Choose engine...": "Velg motor...", "Choose known engine...": "Velg kjent motor...", "Weights": "Vekter", "Lc0 WeightsFile...": "Lc0 vektfil...", "Stockfish EvalFile...": "Stockfish evalueringsfil...", "Set to ": "Sett til ", "Backend": "Backend", "Choose Syzygy path...": "Velg Syzygy-sti...", "Unset": "Fjern", "Limit - normal": "Grense - normal", "Unlimited": "Ubegrenset", "Up slightly": "Litt opp", "Down slightly": "Litt ned", "Limit - auto-eval / play": "Grense - auto-eval / spill", "Limit by time instead of nodes": "Begrens med tid i stedet for noder", "Threads": "Tråder", "Warning about threads": "Advarsel om tråder", "Hash": "Hash", "I want other hash options!": "Jeg vil ha andre hash-valg!", "MultiPV": "MultiPV", "Contempt Mode": "Contempt-modus", "White analysis": "Hvit analyse", "Black analysis": "Svart analyse", "Contempt": "Contempt", "WDL Calibration Elo": "WDL kalibrering Elo", "Use default WDL": "Bruk standard WDL", "WDL Eval Objectivity": "WDL eval objektivitet", "Yes": "Ja", "No": "Nei", "Score Type": "Poengtype", "Custom scripts": "Egendefinerte skript", "How to add scripts": "Hvordan legge til skript", "Show scripts folder": "Vis skriptmappe", "Restart engine": "Start motor på nytt", "Soft engine reset": "Myk tilbakestilling av motor", "Play": "Spill", "Play this colour": "Spill denne fargen", "Start self-play": "Start selvspill", "Use Polyglot book...": "Bruk Polyglot-bok...", "Use PGN book...": "Bruk PGN-bok...", "Unload book / abort load": "Fjern bok / avbryt lasting", "Book depth limit": "Bokdybdegrense", "Temperature": "Temperatur", "Temp Decay Moves": "Temp nedbrytingstrekk", "Infinite": "Uendelig", "About play modes": "Om spillmoduser", "Dev": "Utvikling", "Toggle Developer Tools": "Utviklerverktøy av/på", "Toggle Debug CSS": "Debug CSS av/på", "Permanently enable save": "Aktiver lagring permanent", [show_config]: "Vis config.json", [show_engineconfig]: "Vis engines.json", [reload_engineconfig]: "Last engines.json på nytt (og start motor på nytt)", "Random move": "Tilfeldig trekk", "Disable hardware acceleration for GUI": "Deaktiver maskinvareakselerasjon for GUI", "Spin rate": "Oppdateringsfrekvens", "Frenetic": "Frenetisk", "Fast": "Rask", "Normal": "Normal", "Relaxed": "Avslappet", "Lazy": "Lat", "Show engine state": "Vis motorstatus", "List sent options": "List sendte alternativer", "Show error log": "Vis feillogg", "Hacks and kludges": "Hacks og kludges", "Allow arbitrary scripts": "Tillat vilkårlige skript", "Accept any file size": "Aksepter alle filstørrelser", "Allow stopped analysis": "Tillat stoppet analyse", "Never hide focus buttons": "Aldri skjul fokusknapper", "Never grayout move info": "Aldri gråtone trekkinfo", "Use lowerbound / upperbound info": "Bruk lowerbound / upperbound info", "Suppress ucinewgame": "Undertrykk ucinewgame", "Log RAM state to console": "Logg RAM-status til konsoll", "Fire GC": "Kjør GC", "Logging": "Logging", "Use logfile...": "Bruk loggfil...", "Disable logging": "Deaktiver logging", "Clear log when opening": "Tøm logg ved åpning", "Use unique logfile each time": "Bruk unik loggfil hver gang", "Log illegal moves": "Logg ugyldige trekk", "Log positions": "Logg stillinger", "Log info lines": "Logg info-linjer", "...including useless lines": "...inkludert ubrukelige linjer", "Language": "Språk", "RESTART_REQUIRED": "GUI må startes på nytt." }, // POLISH ..................................................................................... "Polski": { "File": "Plik", "About": "O programie", "New game": "Nowa partia", "New 960 game": "Nowa partia 960", "Open PGN...": "Otwórz PGN...", "Load FEN / PGN from clipboard": "Wczytaj FEN / PGN ze schowka", "Save this game...": "Zapisz partię...", "Write PGN to clipboard": "Zapisz PGN do schowka", "PGN saved statistics": "Statystyki zapisane w PGN", "EV": "EV", "Centipawns": "Setne piona", "N (%)": "N (%)", "N (absolute)": "N (bezwzględne)", "...out of total": "...z całości", "Depth (A/B only)": "Głębokość (tylko A/B)", "Cut": "Wytnij", "Copy": "Kopiuj", "Paste": "Wklej", "Quit": "Zamknij", "Tree": "Drzewo", "Play engine choice": "Zagraj wybór silnika", "1st": "1-szy", "2nd": "2-gi", "3rd": "3-ci", "4th": "4-ty", "Root": "Początek", "End": "Koniec", "Backward": "Wstecz", "Forward": "Naprzód", "Previous sibling": "Poprzedni wariant", "Next sibling": "Następny wariant", "Return to main line": "Powrót do głównej linii", "Promote line to main line": "Promuj linię na główną", "Promote line by 1 level": "Promuj linię o 1 poziom", "Delete node": "Usuń ruch", "Delete children": "Usuń poboczne", "Delete siblings": "Usuń równorzędne", "Delete ALL other lines": "Usuń WSZYSTKIE inne linie", "Show PGN games list": "Pokaż listę partii PGN", "Escape": "Wyjście", "Analysis": "Analiza", "Go": "Start", "Go and lock engine": "Rusz i zablokuj silnik", "Return to locked position": "Powrót do zablokowanej pozycji", "Halt": "Stój", "Auto-evaluate line": "Auto-analiza linii", "Auto-evaluate line, backwards": "Auto-analiza linii, odwrotnie", "Show focus (searchmoves) buttons": "Pokaż przycisk skupić (searchmoves)", "Clear focus": "Usuń skupienie", "Invert focus": "Odwróć skupienie", "Winrate POV": "Perspektywa szans", "Current": "Aktualny", "White": "Biały", "Black": "Czarny", "Centipawn POV": "Perspektywa CP", "Win / draw / loss POV": "Perspektywa Wygrana / remis / przegrana", "PV clicks": "Kliknięcia PV", "Do nothing": "Nic nie rób", "Go there": "Przejdź tam", "Add to tree": "Dodaj do drzewa", "Write infobox to clipboard": "Zapisz infobox do schowka", "Forget all analysis": "Zapomnij analizę", "Display": "Widok", "Flip board": "Obróć szachownicę", "Arrows": "Strzałki", "Piece-click spotlight": "Podświetlanie figur przy kliknięciu", "Always show actual move (if known)": "Pokazuj faktyczny ruch (jeśli znany)", "...with unique colour": "...z unikalnym kolorem", "...with outline": "...z konturem", "Arrowhead type": "Typ strzałek", "Winrate": "Szansa wygranej", "Node %": "Węzły %", "Policy": "Polityka", "MultiPV rank": "Ranga MultiPV", "Moves Left Head": "Przewidywana długość (ML)", "Arrow filter (Lc0)": "Filtr strzałek (Lc0)", "All moves": "Wszystkie ruchy", "Top move": "Najlepszy ruch", "Arrow filter (others)": "Filtr strzałek (inne)", "Diff < 15%": "Różnica < 15%", "Diff < 10%": "Różnica < 10%", "Diff < 5%": "Różnica < 5%", "Infobox stats": "Statystyki infoboxu", "N - nodes (%)": "N - węzły (%)", "N - nodes (absolute)": "N - węzły (bezwzględne)", "P - policy": "P - polityka", "V - static evaluation": "V - ocena statyczna", "Q - evaluation": "Q - ocena", "U - uncertainty": "U - niepewność", "S - search priority": "S - priorytet wyszukiwania", "M - moves left": "M - pozostało ruchów", "WDL - win / draw / loss": "WDL - wygrana / remis / przegrana", "Linebreak before stats": "Nowy wiersz przed statystykami", "PV move numbers": "Numery ruchów PV", "Online API": "API online", "None": "Żaden", "ChessDB.cn evals": "Oceny ChessDB.cn", "Lichess results (masters)": "Wyniki Lichess (mistrzowie)", "Lichess results (plebs)": "Wyniki Lichess (amatorzy)", "Set Lichess API token": "Ustaw token API Lichess", "Allow API after move 25": "Zezwól na API po ruchu 25", "Draw PV on mouseover": "Pokaż PV przy najechaniu", "Draw PV method": "Metoda rysowania PV", "Animate": "Animacja", "Single move": "Pojedynczy ruch", "Final position": "Pozycja końcowa", "Pieces": "Figury", "Choose pieces folder...": "Wybierz folder figur...", "Default": "Domyślne", "About custom pieces": "O własnych figurach", "Background": "Tło", "Choose background image...": "Wybierz obraz tła...", "Book frequency arrows": "Strzałki częstości debiutowej", "Lichess frequency arrows": "Strzałki częstości Lichess", "Sizes": "Rozmiary", "Infobox font": "Czcionka infoboxu", "Move history font": "Czcionka historii ruchów", "Board": "Szachownica", "Giant": "Ogromna", "Large": "Duża", "Medium": "Średnia", "Small": "Mała", "Graph": "Wykres", "Graph lines": "Linie wykresu", "I want other size options!": "Chcę innych opcji rozmiaru!", "Engine": "Silnik", "Choose engine...": "Wybierz silnik...", "Choose known engine...": "Wybierz znany silnik...", "Weights": "Neurony", "Lc0 WeightsFile...": "Plik neuronowy Lc0...", "Stockfish EvalFile...": "Plik NNUE Stockfish'a...", "Set to ": "Ustaw na ", "Backend": "Backend", "Choose Syzygy path...": "Wybierz ścieżkę Syzygy...", "Unset": "Usuń Syzygy", "Limit - normal": "Limit - normalny", "Unlimited": "Bez limitu", "Up slightly": "Zwiększ trochę", "Down slightly": "Zmniejsz trochę", "Limit - auto-eval / play": "Limit - auto-analizy / gry", "Limit by time instead of nodes": "Limituj czasem zamiast węzłami", "Threads": "Wątki", "Warning about threads": "Ostrzeżenie o wątkach", "Hash": "Pamięć", "I want other hash options!": "Chcę innych opcji Ram'u!", "MultiPV": "MultiPV", "Contempt Mode": "Tryb pogardy", "White analysis": "Analiza białych", "Black analysis": "Analiza czarnych", "Contempt": "Pogarda", "WDL Calibration Elo": "Kalibracja WDL Elo", "Use default WDL": "Użyj domyślnego WDL", "WDL Eval Objectivity": "Obiektywność oceny WDL", "Yes": "Tak", "No": "Nie", "Score Type": "Typ wyniku", "Custom scripts": "Własne skrypty", "How to add scripts": "Jak dodać skrypty", "Show scripts folder": "Pokaż folder skryptów", "Restart engine": "Zrestartuj silnik", "Soft engine reset": "Miękki reset silnika", "Play": "Gra", "Play this colour": "Graj tym kolorem", "Start self-play": "Rozpocznij grę automatyczną", "Use Polyglot book...": "Użyj księgi Polyglot...", "Use PGN book...": "Użyj księgi PGN...", "Unload book / abort load": "Wyładuj księgę / przerwij", "Book depth limit": "Limit głębokości księgi", "Temperature": "Temperatura", "Temp Decay Moves": "Zanik temperatury", "Infinite": "Nieskończony", "About play modes": "O trybach gry", "Dev": "Dev", "Toggle Developer Tools": "Przełącz narzędzia deweloperskie", "Toggle Debug CSS": "Przełącz debugowanie CSS", "Permanently enable save": "Trwale włącz zapis", [show_config]: "Pokaż config.json", [show_engineconfig]: "Pokaż engines.json", [reload_engineconfig]: "Przeładuj engines.json (i zrestartuj silnik)", "Random move": "Losowy ruch", "Disable hardware acceleration for GUI": "Wyłącz akcelerację sprzętową GUI", "Spin rate": "Częstość odświeżania", "Frenetic": "Frenetyczna", "Fast": "Szybka", "Normal": "Normalna", "Relaxed": "Zrelaksowana", "Lazy": "Leniwa", "Show engine state": "Pokaż stan silnika", "List sent options": "Lista wysłanych opcji", "Show error log": "Pokaż log błędów", "Hacks and kludges": "Modyfikacje i obejścia", "Allow arbitrary scripts": "Zezwól na dowolne skrypty", "Accept any file size": "Akceptuj każdy rozmiar pliku", "Allow stopped analysis": "Zezwól na zatrzymaną analizę", "Never hide focus buttons": "Nigdy nie ukrywaj przycisków skupienia", "Never grayout move info": "Nigdy nie wyszarzaj info o ruchu", "Use lowerbound / upperbound info": "Użyj info o dolnej/górnej granicy", "Suppress ucinewgame": "Pomiń ucinewgame", "Log RAM state to console": "Loguj stan RAM do konsoli", "Fire GC": "Uruchom GC", "Logging": "Logowanie", "Use logfile...": "Użyj pliku logu...", "Disable logging": "Wyłącz logowanie", "Clear log when opening": "Wyczyść log przy otwarciu", "Use unique logfile each time": "Użyj unikalnego pliku logu", "Log illegal moves": "Loguj nieprawidłowe ruchy", "Log positions": "Loguj pozycje", "Log info lines": "Loguj linie info", "...including useless lines": "...włącznie z bezużytecznymi liniami", "Language": "Język", "RESTART_REQUIRED": "Program musi zostać uruchomiony ponownie." }, // TURKISH .................................................................................... "Türkçe": { "File": "Dosya", "About": "Hakkında", "New game": "Yeni oyun", "New 960 game": "Yeni 960 oyunu", "Open PGN...": "PGN aç...", "Load FEN / PGN from clipboard": "Panodan FEN / PGN yükle", "Save this game...": "Bu oyunu kaydet...", "Write PGN to clipboard": "PGN'yi panoya kopyala", "PGN saved statistics": "PGN kayıt istatistikleri", "EV": "EV", "Centipawns": "Centipawns", "N (%)": "N (%)", "N (absolute)": "N (mutlak)", "...out of total": "...toplam içinde", "Depth (A/B only)": "Derinlik (sadece A/B)", "Cut": "Kes", "Copy": "Kopyala", "Paste": "Yapıştır", "Quit": "Çıkış", "Tree": "Ağaç", "Play engine choice": "Motor seçimini oyna", "1st": "1.", "2nd": "2.", "3rd": "3.", "4th": "4.", "Root": "Başlangıç", "End": "Son", "Backward": "Geri", "Forward": "İleri", "Previous sibling": "Önceki varyant", "Next sibling": "Sonraki varyant", "Return to main line": "Ana varyanta dön", "Promote line to main line": "Varyantı ana varyant yap", "Promote line by 1 level": "Varyantı 1 seviye yükselt", "Delete node": "Hamleyi sil", "Delete children": "Alt hamleleri sil", "Delete siblings": "Kardeş hamleleri sil", "Delete ALL other lines": "TÜM diğer varyantları sil", "Show PGN games list": "PGN oyun listesini göster", "Escape": "Çıkış", "Analysis": "Analiz", "Go": "Başla", "Go and lock engine": "Başla ve motoru kilitle", "Return to locked position": "Kilitli pozisyona dön", "Halt": "Durdur", "Auto-evaluate line": "Varyantı otomatik değerlendir", "Auto-evaluate line, backwards": "Varyantı geriye doğru değerlendir", "Show focus (searchmoves) buttons": "Odak (searchmoves) düğmelerini göster", "Clear focus": "Odağı temizle", "Invert focus": "Odağı tersine çevir", "Winrate POV": "Kazanma oranı bakış açısı", "Current": "Mevcut", "White": "Beyaz", "Black": "Siyah", "Centipawn POV": "Centipawn bakış açısı", "Win / draw / loss POV": "Kazanç / beraberlik / kayıp bakış açısı", "PV clicks": "PV tıklamaları", "Do nothing": "Hiçbir şey yapma", "Go there": "Oraya git", "Add to tree": "Ağaca ekle", "Write infobox to clipboard": "Bilgi kutusunu panoya kopyala", "Forget all analysis": "Tüm analizi unut", "Display": "Görünüm", "Flip board": "Tahtayı çevir", "Arrows": "Oklar", "Piece-click spotlight": "Taş tıklama ışıklandırması", "Always show actual move (if known)": "Gerçek hamleyi her zaman göster (biliniyorsa)", "...with unique colour": "...benzersiz renkle", "...with outline": "...çerçeveyle", "Arrowhead type": "Ok ucu tipi", "Winrate": "Kazanma oranı", "Node %": "Düğüm %", "Policy": "Policy", "MultiPV rank": "MultiPV sırası", "Moves Left Head": "Kalan Hamleler", "Arrow filter (Lc0)": "Ok filtresi (Lc0)", "All moves": "Tüm hamleler", "Top move": "En iyi hamle", "Arrow filter (others)": "Ok filtresi (diğerleri)", "Diff < 15%": "Fark < %15", "Diff < 10%": "Fark < %10", "Diff < 5%": "Fark < %5", "Infobox stats": "Bilgi kutusu istatistikleri", "N - nodes (%)": "N - düğümler (%)", "N - nodes (absolute)": "N - düğümler (mutlak)", "P - policy": "P - policy", "V - static evaluation": "V - statik değerlendirme", "Q - evaluation": "Q - değerlendirme", "U - uncertainty": "U - belirsizlik", "S - search priority": "S - arama önceliği", "M - moves left": "M - kalan hamleler", "WDL - win / draw / loss": "WDL - kazanç / beraberlik / kayıp", "Linebreak before stats": "İstatistiklerden önce satır sonu", "PV move numbers": "PV hamle numaraları", "Online API": "Çevrimiçi API", "None": "Hiçbiri", "ChessDB.cn evals": "ChessDB.cn değerlendirmeleri", "Lichess results (masters)": "Lichess sonuçları (ustalar)", "Lichess results (plebs)": "Lichess sonuçları (normal)", "Set Lichess API token": "Lichess API jetonunu ayarla", "Allow API after move 25": "25. hamleden sonra API'ye izin ver", "Draw PV on mouseover": "Fare üzerindeyken PV'yi çiz", "Draw PV method": "PV çizim yöntemi", "Animate": "Animasyon", "Single move": "Tek hamle", "Final position": "Son konum", "Pieces": "Taşlar", "Choose pieces folder...": "Taş klasörü seç...", "Default": "Varsayılan", "About custom pieces": "Özel taşlar hakkında", "Background": "Arkaplan", "Choose background image...": "Arkaplan resmi seç...", "Book frequency arrows": "Açılış kitabı sıklık okları", "Lichess frequency arrows": "Lichess sıklık okları", "Sizes": "Boyutlar", "Infobox font": "Bilgi kutusu yazı tipi", "Move history font": "Hamle geçmişi yazı tipi", "Board": "Tahta", "Giant": "Çok büyük", "Large": "Büyük", "Medium": "Orta", "Small": "Küçük", "Graph": "Grafik", "Graph lines": "Grafik çizgileri", "I want other size options!": "Başka boyut seçenekleri istiyorum!", "Engine": "Motor", "Choose engine...": "Motor seç...", "Choose known engine...": "Bilinen motor seç...", "Weights": "Ağırlıklar", "Lc0 WeightsFile...": "Lc0 WeightsFile...", "Stockfish EvalFile...": "Stockfish EvalFile...", "Set to ": " olarak ayarla", "Backend": "Backend", "Choose Syzygy path...": "Syzygy yolu seç...", "Unset": "Kaldır", "Limit - normal": "Limit - normal", "Unlimited": "Limitsiz", "Up slightly": "Biraz artır", "Down slightly": "Biraz azalt", "Limit - auto-eval / play": "Limit - otomatik değerlendirme / oyun", "Limit by time instead of nodes": "Düğüm yerine zamanla sınırla", "Threads": "İş parçacığı", "Warning about threads": "İş parçacığı uyarısı", "Hash": "Hash", "I want other hash options!": "Başka hash seçenekleri istiyorum!", "MultiPV": "MultiPV", "Contempt Mode": "Contempt Modu", "White analysis": "Beyaz analizi", "Black analysis": "Siyah analizi", "Contempt": "Contempt", "WDL Calibration Elo": "WDL Kalibrasyon Elo", "Use default WDL": "Varsayılan WDL kullan", "WDL Eval Objectivity": "WDL Değerlendirme Objektifliği", "Yes": "Evet", "No": "Hayır", "Score Type": "Skor Tipi", "Custom scripts": "Özel scriptler", "How to add scripts": "Script nasıl eklenir", "Show scripts folder": "Script klasörünü göster", "Restart engine": "Motoru yeniden başlat", "Soft engine reset": "Yumuşak motor sıfırlama", "Play": "Oyna", "Play this colour": "Bu rengi oyna", "Start self-play": "Kendi kendine oyunu başlat", "Use Polyglot book...": "Polyglot kitabı kullan...", "Use PGN book...": "PGN kitabı kullan...", "Unload book / abort load": "Kitabı kaldır / yüklemeyi iptal et", "Book depth limit": "Kitap derinlik limiti", "Temperature": "Sıcaklık", "Temp Decay Moves": "Sıcaklık Azalma Hamleleri", "Infinite": "Sonsuz", "About play modes": "Oyun modları hakkında", "Dev": "Geliştirici", "Toggle Developer Tools": "Geliştirici Araçlarını Aç/Kapat", "Toggle Debug CSS": "Hata Ayıklama CSS'ini Aç/Kapat", "Permanently enable save": "Kaydetmeyi kalıcı olarak etkinleştir", [show_config]: "config.json'ı göster", [show_engineconfig]: "engines.json'ı göster", [reload_engineconfig]: "engines.json yenile + motor yeniden başlat", "Random move": "Rastgele hamle", "Disable hardware acceleration for GUI": "GUI donanım hızlandırma kapalı", "Spin rate": "Yenileme hızı", "Frenetic": "Çok hızlı", "Fast": "Hızlı", "Normal": "Normal", "Relaxed": "Rahat", "Lazy": "Yavaş", "Show engine state": "Motor durumunu göster", "List sent options": "Gönderilen seçenekleri listele", "Show error log": "Hata günlüğünü göster", "Hacks and kludges": "Geçici çözümler", "Allow arbitrary scripts": "Rastgele scriptlere izin ver", "Accept any file size": "Her dosya boyutunu kabul et", "Allow stopped analysis": "Durmuş analize izin ver", "Never hide focus buttons": "Odak düğmelerini asla gizleme", "Never grayout move info": "Hamle bilgisini asla grilendirme", "Use lowerbound / upperbound info": "Alt sınır / üst sınır bilgisini kullan", "Suppress ucinewgame": "ucinewgame'i bastır", "Log RAM state to console": "RAM durumunu konsola kaydet", "Fire GC": "Çöp toplayıcıyı çalıştır", "Logging": "Günlük kaydı", "Use logfile...": "Günlük dosyası kullan...", "Disable logging": "Günlük kaydını devre dışı bırak", "Clear log when opening": "Açarken günlüğü temizle", "Use unique logfile each time": "Her seferinde benzersiz günlük dosyası kullan", "Log illegal moves": "Geçersiz hamleleri kaydet", "Log positions": "Konumları kaydet", "Log info lines": "Bilgi satırlarını kaydet", "...including useless lines": "...gereksiz satırlar dahil", "Language": "Dil", "RESTART_REQUIRED": "Arayüz şimdi yeniden başlatılmalı." }, // RUSSIAN .................................................................................... "Русский": { "File": "Файл", "About": "О программе", "New game": "Новая партия", "New 960 game": "Новая партия 960", "Open PGN...": "Открыть PGN...", "Load FEN / PGN from clipboard": "Загрузить FEN / PGN из буфера", "Save this game...": "Сохранить партию...", "Write PGN to clipboard": "Копировать PGN в буфер", "PGN saved statistics": "Статистика в PGN", "EV": "Оценка", "Centipawns": "Сотые пешки", "N (%)": "N (%)", "N (absolute)": "N (абс.)", "...out of total": "...из всего", "Depth (A/B only)": "Глубина (А/Б)", "Cut": "Вырезать", "Copy": "Копировать", "Paste": "Вставить", "Quit": "Выход", "Tree": "Дерево", "Play engine choice": "Сыграть выбор движка", "1st": "1-й", "2nd": "2-й", "3rd": "3-й", "4th": "4-й", "Root": "В начало", "End": "В конец", "Backward": "Назад", "Forward": "Вперёд", "Previous sibling": "Пред. вариант", "Next sibling": "След. вариант", "Return to main line": "Вернуться в главную линию", "Promote line to main line": "Сделать главной линией", "Promote line by 1 level": "Повысить на уровень", "Delete node": "Удалить ход", "Delete children": "Удалить продолжения", "Delete siblings": "Удалить варианты", "Delete ALL other lines": "Удалить ВСЕ другие линии", "Show PGN games list": "Показать список партий", "Escape": "Escape", "Analysis": "Анализ", "Go": "Старт", "Go and lock engine": "Старт с фиксацией", "Return to locked position": "К фиксированной позиции", "Halt": "Стоп", "Auto-evaluate line": "Авто-анализ линии", "Auto-evaluate line, backwards": "Авто-анализ линии назад", "Show focus (searchmoves) buttons": "Показать кнопки фокуса", "Clear focus": "Очистить фокус", "Invert focus": "Инвертировать фокус", "Winrate POV": "Шанс победы", "Current": "Текущий", "White": "Белые", "Black": "Чёрные", "Centipawn POV": "Оценка в пешках", "Win / draw / loss POV": "Победа/ничья/проигрыш", "PV clicks": "Клики по вариантам", "Do nothing": "Ничего", "Go there": "Перейти", "Add to tree": "Добавить в дерево", "Write infobox to clipboard": "Копировать инфо в буфер", "Forget all analysis": "Забыть весь анализ", "Display": "Вид", "Flip board": "Перевернуть доску", "Arrows": "Стрелки", "Piece-click spotlight": "Подсветка ходов", "Always show actual move (if known)": "Показывать реальный ход", "...with unique colour": "...уникальным цветом", "...with outline": "...с контуром", "Arrowhead type": "Тип стрелок", "Winrate": "Шанс победы", "Node %": "Узлы %", "Policy": "Политика", "MultiPV rank": "Ранг MultiPV", "Moves Left Head": "Оставшиеся ходы", "Arrow filter (Lc0)": "Фильтр стрелок (Lc0)", "All moves": "Все ходы", "Top move": "Лучший ход", "Arrow filter (others)": "Фильтр стрелок (другие)", "Diff < 15%": "Разн. < 15%", "Diff < 10%": "Разн. < 10%", "Diff < 5%": "Разн. < 5%", "Infobox stats": "Статистика", "N - nodes (%)": "N - узлы (%)", "N - nodes (absolute)": "N - узлы (абс.)", "P - policy": "P - политика", "V - static evaluation": "V - статич. оценка", "Q - evaluation": "Q - оценка", "U - uncertainty": "U - неопред.", "S - search priority": "S - приоритет", "M - moves left": "M - ходов осталось", "WDL - win / draw / loss": "WDL - победа/ничья/проигрыш", "Linebreak before stats": "Перенос перед статистикой", "PV move numbers": "Нумерация ходов", "Online API": "Онлайн API", "None": "Нет", "ChessDB.cn evals": "Оценки ChessDB.cn", "Lichess results (masters)": "Lichess (мастера)", "Lichess results (plebs)": "Lichess (любители)", "Set Lichess API token": "Установить токен API Lichess", "Allow API after move 25": "Разрешить API после 25 хода", "Draw PV on mouseover": "Показывать вариант при наведении", "Draw PV method": "Метод показа варианта", "Animate": "Анимация", "Single move": "Один ход", "Final position": "Конечная позиция", "Pieces": "Фигуры", "Choose pieces folder...": "Выбрать папку фигур...", "Default": "По умолчанию", "About custom pieces": "О своих фигурах", "Background": "Фон", "Choose background image...": "Выбрать фоновое изображение...", "Book frequency arrows": "Стрелки частот дебюта", "Lichess frequency arrows": "Стрелки частот Lichess", "Sizes": "Размеры", "Infobox font": "Шрифт инфо", "Move history font": "Шрифт истории", "Board": "Доска", "Giant": "Огромный", "Large": "Большой", "Medium": "Средний", "Small": "Малый", "Graph": "График", "Graph lines": "Линии графика", "I want other size options!": "Хочу другие размеры!", "Engine": "Движок", "Choose engine...": "Выбрать движок...", "Choose known engine...": "Выбрать из известных...", "Weights": "Веса", "Lc0 WeightsFile...": "Файл весов Lc0...", "Stockfish EvalFile...": "Файл оценки Stockfish...", "Set to ": "Установить <авто>", "Backend": "Бэкенд", "Choose Syzygy path...": "Путь к Syzygy...", "Unset": "Сбросить", "Limit - normal": "Лимит - обычный", "Unlimited": "Без лимита", "Up slightly": "Чуть больше", "Down slightly": "Чуть меньше", "Limit - auto-eval / play": "Лимит - авто-анализ/игра", "Limit by time instead of nodes": "Лимит по времени", "Threads": "Потоки", "Warning about threads": "Внимание о потоках", "Hash": "Хэш", "I want other hash options!": "Хочу другие опции хэша!", "MultiPV": "MultiPV", "Contempt Mode": "Режим презрения", "White analysis": "Анализ белых", "Black analysis": "Анализ чёрных", "Contempt": "Презрение", "WDL Calibration Elo": "Калибровка WDL Эло", "Use default WDL": "WDL по умолчанию", "WDL Eval Objectivity": "Объективность WDL", "Yes": "Да", "No": "Нет", "Score Type": "Тип оценки", "Custom scripts": "Свои скрипты", "How to add scripts": "Как добавить скрипты", "Show scripts folder": "Показать папку скриптов", "Restart engine": "Перезапуск движка", "Soft engine reset": "Мягкий сброс движка", "Play": "Игра", "Play this colour": "Играть этим цветом", "Start self-play": "Начать игру с собой", "Use Polyglot book...": "Использовать книгу Polyglot...", "Use PGN book...": "Использовать книгу PGN...", "Unload book / abort load": "Выгрузить/прервать загрузку", "Book depth limit": "Лимит глубины книги", "Temperature": "Температура", "Temp Decay Moves": "Убывание температуры", "Infinite": "Бесконечно", "About play modes": "О режимах игры", "Dev": "Разработка", "Toggle Developer Tools": "Инструменты разработчика", "Toggle Debug CSS": "Отладка CSS", "Permanently enable save": "Включить сохранение", [show_config]: "Показать config.json", [show_engineconfig]: "Показать engines.json", [reload_engineconfig]: "Перезагрузить engines.json", "Random move": "Случайный ход", "Disable hardware acceleration for GUI": "Отключить аппаратное ускорение", "Spin rate": "Частота обновления", "Frenetic": "Безумная", "Fast": "Быстрая", "Normal": "Нормальная", "Relaxed": "Спокойная", "Lazy": "Ленивая", "Show engine state": "Состояние движка", "List sent options": "Список отправленных опций", "Show error log": "Показать лог ошибок", "Hacks and kludges": "Хаки и костыли", "Allow arbitrary scripts": "Разрешить любые скрипты", "Accept any file size": "Принимать любой размер", "Allow stopped analysis": "Разрешить остановленный анализ", "Never hide focus buttons": "Не скрывать кнопки фокуса", "Never grayout move info": "Не затемнять инфо ходов", "Use lowerbound / upperbound info": "Использовать границы оценки", "Suppress ucinewgame": "Подавить ucinewgame", "Log RAM state to console": "Лог RAM в консоль", "Fire GC": "Запуск GC", "Logging": "Логирование", "Use logfile...": "Использовать лог-файл...", "Disable logging": "Отключить логирование", "Clear log when opening": "Очищать при открытии", "Use unique logfile each time": "Уникальный файл каждый раз", "Log illegal moves": "Лог невозможных ходов", "Log positions": "Лог позиций", "Log info lines": "Лог инфо-строк", "...including useless lines": "...включая бесполезные", "Language": "Язык", "RESTART_REQUIRED": "Требуется перезапуск программы" }, // UKRAINIAN .................................................................................. "Українська": { "File": "Файл", "About": "Про програму", "New game": "Нова гра", "New 960 game": "Нова гра 960", "Open PGN...": "Відкрити PGN...", "Load FEN / PGN from clipboard": "Завантажити FEN / PGN з буфера", "Save this game...": "Зберегти гру...", "Write PGN to clipboard": "Копіювати PGN до буфера", "PGN saved statistics": "Статистика збереження PGN", "EV": "EV", "Centipawns": "Сентипішки", "N (%)": "N (%)", "N (absolute)": "N (абсолютне)", "...out of total": "...від загального", "Depth (A/B only)": "Глибина (тільки A/B)", "Cut": "Вирізати", "Copy": "Копіювати", "Paste": "Вставити", "Quit": "Вийти", "Tree": "Дерево", "Play engine choice": "Зіграти вибір двигуна", "1st": "1-й", "2nd": "2-й", "3rd": "3-й", "4th": "4-й", "Root": "Початок", "End": "Кінець", "Backward": "Назад", "Forward": "Вперед", "Previous sibling": "Попередній варіант", "Next sibling": "Наступний варіант", "Return to main line": "До головної лінії", "Promote line to main line": "Зробити лінію головною", "Promote line by 1 level": "Підвищити лінію на 1 рівень", "Delete node": "Видалити вузол", "Delete children": "Видалити дочірні", "Delete siblings": "Видалити сусідні", "Delete ALL other lines": "Видалити ВСІ інші лінії", "Show PGN games list": "Показати список партій PGN", "Escape": "Escape", "Analysis": "Аналіз", "Go": "Старт", "Go and lock engine": "Старт і заблокувати двигун", "Return to locked position": "До заблокованої позиції", "Halt": "Стоп", "Auto-evaluate line": "Автоаналіз лінії", "Auto-evaluate line, backwards": "Автоаналіз лінії назад", "Show focus (searchmoves) buttons": "Показати кнопки фокусу", "Clear focus": "Очистити фокус", "Invert focus": "Інвертувати фокус", "Winrate POV": "Шанси на перемогу від", "Current": "Поточний", "White": "Білі", "Black": "Чорні", "Centipawn POV": "Сентипішки від", "Win / draw / loss POV": "Вигр./ніч./прогр. від", "PV clicks": "Кліки PV", "Do nothing": "Нічого", "Go there": "Перейти", "Add to tree": "Додати до дерева", "Write infobox to clipboard": "Копіювати інфобокс", "Forget all analysis": "Забути весь аналіз", "Display": "Відображення", "Flip board": "Перевернути дошку", "Arrows": "Стрілки", "Piece-click spotlight": "Підсвітка ходів фігури", "Always show actual move (if known)": "Показувати зроблений хід", "...with unique colour": "...унікальним кольором", "...with outline": "...з контуром", "Arrowhead type": "Тип стрілок", "Winrate": "Шанс перемоги", "Node %": "Вузли %", "Policy": "Політика", "MultiPV rank": "Ранг MultiPV", "Moves Left Head": "Ходів до кінця", "Arrow filter (Lc0)": "Фільтр стрілок (Lc0)", "All moves": "Всі ходи", "Top move": "Найкращий хід", "Arrow filter (others)": "Фільтр стрілок (інші)", "Diff < 15%": "Різниця < 15%", "Diff < 10%": "Різниця < 10%", "Diff < 5%": "Різниця < 5%", "Infobox stats": "Статистика інфобоксу", "N - nodes (%)": "N - вузли (%)", "N - nodes (absolute)": "N - вузли (абс.)", "P - policy": "P - політика", "V - static evaluation": "V - статична оцінка", "Q - evaluation": "Q - оцінка", "U - uncertainty": "U - невизначеність", "S - search priority": "S - пріоритет пошуку", "M - moves left": "M - ходів до кінця", "WDL - win / draw / loss": "WDL - виграш/нічия/програш", "Linebreak before stats": "Перенос перед статистикою", "PV move numbers": "Номери ходів PV", "Online API": "Онлайн API", "None": "Немає", "ChessDB.cn evals": "Оцінки ChessDB.cn", "Lichess results (masters)": "Результати Lichess (майстри)", "Lichess results (plebs)": "Результати Lichess (гравці)", "Set Lichess API token": "Встановити токен API Lichess", "Allow API after move 25": "Дозволити API після 25 ходу", "Draw PV on mouseover": "Показувати PV при наведенні", "Draw PV method": "Метод показу PV", "Animate": "Анімація", "Single move": "Один хід", "Final position": "Кінцева позиція", "Pieces": "Фігури", "Choose pieces folder...": "Вибрати теку фігур...", "Default": "За замовчуванням", "About custom pieces": "Про власні фігури", "Background": "Фон", "Choose background image...": "Вибрати зображення фону...", "Book frequency arrows": "Стрілки частоти дебютів", "Lichess frequency arrows": "Стрілки частоти Lichess", "Sizes": "Розміри", "Infobox font": "Шрифт інфобоксу", "Move history font": "Шрифт історії ходів", "Board": "Дошка", "Giant": "Величезна", "Large": "Велика", "Medium": "Середня", "Small": "Мала", "Graph": "Графік", "Graph lines": "Лінії графіка", "I want other size options!": "Хочу інші розміри!", "Engine": "Двигун", "Choose engine...": "Вибрати двигун...", "Choose known engine...": "Вибрати відомий двигун...", "Weights": "Ваги", "Lc0 WeightsFile...": "Файл ваг Lc0...", "Stockfish EvalFile...": "Файл оцінки Stockfish...", "Set to ": "Встановити <авто>", "Backend": "Бекенд", "Choose Syzygy path...": "Шлях до Syzygy...", "Unset": "Скинути", "Limit - normal": "Ліміт - звичайний", "Unlimited": "Без обмежень", "Up slightly": "Трохи більше", "Down slightly": "Трохи менше", "Limit - auto-eval / play": "Ліміт - автоаналіз/гра", "Limit by time instead of nodes": "Ліміт часу замість вузлів", "Threads": "Потоки", "Warning about threads": "Попередження про потоки", "Hash": "Хеш", "I want other hash options!": "Хочу інші опції хешу!", "MultiPV": "MultiPV", "Contempt Mode": "Режим зневаги", "White analysis": "Аналіз білих", "Black analysis": "Аналіз чорних", "Contempt": "Зневага", "WDL Calibration Elo": "WDL калібрування Elo", "Use default WDL": "Типовий WDL", "WDL Eval Objectivity": "WDL об'єктивність оцінки", "Yes": "Так", "No": "Ні", "Score Type": "Тип оцінки", "Custom scripts": "Власні скрипти", "How to add scripts": "Як додавати скрипти", "Show scripts folder": "Показати теку скриптів", "Restart engine": "Перезапустити двигун", "Soft engine reset": "М'який перезапуск двигуна", "Play": "Гра", "Play this colour": "Грати цим кольором", "Start self-play": "Почати самогру", "Use Polyglot book...": "Використати книгу Polyglot...", "Use PGN book...": "Використати книгу PGN...", "Unload book / abort load": "Вивантажити/скасувати книгу", "Book depth limit": "Ліміт глибини книги", "Temperature": "Температура", "Temp Decay Moves": "Спад температури ходів", "Infinite": "Нескінченно", "About play modes": "Про режими гри", "Dev": "Розробка", "Toggle Developer Tools": "Інструменти розробника", "Toggle Debug CSS": "Перемкнути Debug CSS", "Permanently enable save": "Постійно увімкнути збереження", [show_config]: "Показати config.json", [show_engineconfig]: "Показати engines.json", [reload_engineconfig]: "Перезавантажити engines.json", "Random move": "Випадковий хід", "Disable hardware acceleration for GUI": "Вимкнути апаратне прискорення", "Spin rate": "Частота оновлення", "Frenetic": "Шалена", "Fast": "Швидка", "Normal": "Нормальна", "Relaxed": "Розслаблена", "Lazy": "Лінива", "Show engine state": "Показати стан двигуна", "List sent options": "Список надісланих опцій", "Show error log": "Показати журнал помилок", "Hacks and kludges": "Хаки та костилі", "Allow arbitrary scripts": "Дозволити довільні скрипти", "Accept any file size": "Приймати будь-який розмір", "Allow stopped analysis": "Дозволити зупинений аналіз", "Never hide focus buttons": "Не ховати кнопки фокусу", "Never grayout move info": "Не затемняти інфо ходу", "Use lowerbound / upperbound info": "Використовувати межі", "Suppress ucinewgame": "Пропускати ucinewgame", "Log RAM state to console": "Логувати стан RAM", "Fire GC": "Запустити GC", "Logging": "Логування", "Use logfile...": "Використати лог-файл...", "Disable logging": "Вимкнути логування", "Clear log when opening": "Очищати лог при відкритті", "Use unique logfile each time": "Унікальний лог-файл щоразу", "Log illegal moves": "Логувати неправильні ходи", "Log positions": "Логувати позиції", "Log info lines": "Логувати інфо-рядки", "...including useless lines": "...включно з непотрібними", "Language": "Мова", "RESTART_REQUIRED": "Необхідно перезапустити GUI." }, // BELARUSIAN ................................................................................. "Беларуская": { "File": "Файл", "About": "Пра праграму", "New game": "Новая гульня", "New 960 game": "Новая гульня 960", "Open PGN...": "Адкрыць PGN...", "Load FEN / PGN from clipboard": "Загрузіць FEN / PGN з буфера абмену", "Save this game...": "Захаваць гэтую гульню...", "Write PGN to clipboard": "Запісаць PGN у буфер абмену", "PGN saved statistics": "PGN захаваная статыстыка", "EV": "Ацэнка", "Centipawns": "Сантыпешкі", "N (%)": "N (%)", "N (absolute)": "N (абсалют)", "...out of total": "...з агульнай колькасці", "Depth (A/B only)": "Глыбіня (толькі A/B)", "Cut": "Выразаць", "Copy": "Капіяваць", "Paste": "Уставіць", "Quit": "Выйсці", "Tree": "Дрэва", "Play engine choice": "Згуляць выбар рухавіка", "1st": "1-ы", "2nd": "2-і", "3rd": "3-і", "4th": "4-ы", "Root": "Корань", "End": "Канец", "Backward": "Назад", "Forward": "Наперад", "Previous sibling": "Папярэдні брат", "Next sibling": "Наступны брат", "Return to main line": "Вярнуцца да галоўнай лініі", "Promote line to main line": "Зрабіць лінію галоўнай", "Promote line by 1 level": "Павысіць лінію на 1 узровень", "Delete node": "Выдаліць вузел", "Delete children": "Выдаліць дзяцей", "Delete siblings": "Выдаліць братоў", "Delete ALL other lines": "Выдаліць УСЕ іншыя лініі", "Show PGN games list": "Паказаць спіс гульняў PGN", "Escape": "Esc", "Analysis": "Аналіз", "Go": "Старт", "Go and lock engine": "Старт і зафіксаваць рухавік", "Return to locked position": "Вярнуцца да зафіксаванай пазіцыі", "Halt": "Спыніць", "Auto-evaluate line": "Аўтаматычна ацаніць лінію", "Auto-evaluate line, backwards": "Аўтаматычна ацаніць лінію, назад", "Show focus (searchmoves) buttons": "Паказаць кнопкі фокуса (хадоў пошуку)", "Clear focus": "Ачысціць фокус", "Invert focus": "Інвертаваць фокус", "Winrate POV": "Вінрэйт POV", "Current": "Бягучы", "White": "Белыя", "Black": "Чорныя", "Centipawn POV": "Сантыпешкі POV", "Win / draw / loss POV": "Перамога / нічыя / параза POV", "PV clicks": "Націсканні PV", "Do nothing": "Нічога не рабіць", "Go there": "Перайсці туды", "Add to tree": "Дадаць у дрэва", "Write infobox to clipboard": "Запісаць інфармацыйную панэль у буфер абмену", "Forget all analysis": "Забыць увесь аналіз", "Display": "Паказ", "Flip board": "Перавярнуць дошку", "Arrows": "Стрэлкі", "Piece-click spotlight": "Клік па фігуры - пражэктар", "Always show actual move (if known)": "Заўсёды паказваць фактычны ход (калі вядомы)", "...with unique colour": "...з унікальным колерам", "...with outline": "...з контурам", "Arrowhead type": "Тып наканечніка стрэлкі", "Winrate": "Вінрэйт", "Node %": "Вузел %", "Policy": "Палітыка", "MultiPV rank": "Ранг MultiPV", "Moves Left Head": "Хады да канца", "Arrow filter (Lc0)": "Фільтр стрэлак (Lc0)", "All moves": "Усе хады", "Top move": "Лепшы ход", "Arrow filter (others)": "Фільтр стрэлак (іншыя)", "Diff < 15%": "Розн. < 15%", "Diff < 10%": "Розн. < 10%", "Diff < 5%": "Розн. < 5%", "Infobox stats": "Статыстыка інфармацыйнай панэлі", "N - nodes (%)": "N - вузлы (%)", "N - nodes (absolute)": "N - вузлы (абсалют)", "P - policy": "P - палітыка", "V - static evaluation": "V - статычная ацэнка", "Q - evaluation": "Q - ацэнка", "U - uncertainty": "U - нявызначанасць", "S - search priority": "S - прыярытэт пошуку", "M - moves left": "M - хады да канца", "WDL - win / draw / loss": "WDL - перамога / нічыя / параза", "Linebreak before stats": "Перанос радка перад статыстыкай", "PV move numbers": "Нумары хадоў PV", "Online API": "Онлайн API", "None": "Няма", "ChessDB.cn evals": "Ацэнкі ChessDB.cn", "Lichess results (masters)": "Вынікі Lichess (майстры)", "Lichess results (plebs)": "Вынікі Lichess (звычайныя гульцы)", "Set Lichess API token": "Усталяваць токен API Lichess", "Allow API after move 25": "Дазволіць API пасля 25 ходу", "Draw PV on mouseover": "Маляваць PV пры навядзенні", "Draw PV method": "Метад малявання PV", "Animate": "Анімаваць", "Single move": "Адзін ход", "Final position": "Канчатковая пазіцыя", "Pieces": "Фігуры", "Choose pieces folder...": "Выбраць тэчку з фігурамі...", "Default": "Па змаўчанні", "About custom pieces": "Пра карыстальніцкія фігуры", "Background": "Фон", "Choose background image...": "Выбраць фонавую выяву...", "Book frequency arrows": "Стрэлкі частаты кнігі", "Lichess frequency arrows": "Стрэлкі частаты Lichess", "Sizes": "Памеры", "Infobox font": "Шрыфт інфармацыйнай панэлі", "Move history font": "Шрыфт гісторыі хадоў", "Board": "Дошка", "Giant": "Гіганцкая", "Large": "Вялікая", "Medium": "Сярэдняя", "Small": "Малая", "Graph": "Графік", "Graph lines": "Лініі графіка", "I want other size options!": "Я хачу іншыя варыянты памеру!", "Engine": "Рухавік", "Choose engine...": "Выбраць рухавік...", "Choose known engine...": "Выбраць вядомы рухавік...", "Weights": "Вагі", "Lc0 WeightsFile...": "Файл вагаў Lc0...", "Stockfish EvalFile...": "Файл ацэнкі Stockfish...", "Set to ": "Устанавіць <аўто>", "Backend": "Бэкэнд", "Choose Syzygy path...": "Выбраць шлях Syzygy...", "Unset": "Не зададзена", "Limit - normal": "Ліміт - звычайны", "Unlimited": "Неабмежавана", "Up slightly": "Злёгку ўверх", "Down slightly": "Злёгку ўніз", "Limit - auto-eval / play": "Ліміт - аўтаацэнка / гульня", "Limit by time instead of nodes": "Лімітаваць па часе, а не па вузлах", "Threads": "Патокі", "Warning about threads": "Папярэджанне пра патокі", "Hash": "Хэш", "I want other hash options!": "Я хачу іншыя варыянты хэшу!", "MultiPV": "MultiPV", "Contempt Mode": "Рэжым пагарды", "White analysis": "Аналіз белых", "Black analysis": "Аналіз чорных", "Contempt": "Пагарда", "WDL Calibration Elo": "WDL Каліброўка Эла", "Use default WDL": "Выкарыстоўваць WDL па змаўчанні", "WDL Eval Objectivity": "WDL Аб'ектыўнасць ацэнкі", "Yes": "Так", "No": "Не", "Score Type": "Тып ліку", "Custom scripts": "Карыстальніцкія скрыпты", "How to add scripts": "Як дадаць скрыпты", "Show scripts folder": "Паказаць тэчку са скрыптамі", "Restart engine": "Перазапусціць рухавік", "Soft engine reset": "Мяккі скід рухавіка", "Play": "Гуляць", "Play this colour": "Гуляць гэтым колерам", "Start self-play": "Пачаць самагульню", "Use Polyglot book...": "Выкарыстоўваць кнігу Polyglot...", "Use PGN book...": "Выкарыстоўваць кнігу PGN...", "Unload book / abort load": "Выгрузіць кнігу / адмяніць загрузку", "Book depth limit": "Ліміт глыбіні кнігі", "Temperature": "Тэмпература", "Temp Decay Moves": "Хады згасання тэмпературы", "Infinite": "Бясконца", "About play modes": "Пра рэжымы гульні", "Dev": "Распрацоўка", "Toggle Developer Tools": "Пераключыць інструменты распрацоўніка", "Toggle Debug CSS": "Пераключыць CSS для адладкі", "Permanently enable save": "Заўсёды дазволіць захаванне", [show_config]: "Паказаць config.json", [show_engineconfig]: "Паказаць engines.json", [reload_engineconfig]: "Перазагрузіць engines.json (і перазапусціць рухавік)", "Random move": "Выпадковы ход", "Disable hardware acceleration for GUI": "Адключыць апаратнае паскарэнне для GUI", "Spin rate": "Хуткасць пракруткі", "Frenetic": "Шалёны", "Fast": "Хуткі", "Normal": "Звычайны", "Relaxed": "Спакойны", "Lazy": "Лянівы", "Show engine state": "Паказаць стан рухавіка", "List sent options": "Спіс адпраўленых опцый", "Show error log": "Паказаць часопіс памылак", "Hacks and kludges": "Хакі і хітрыкі", "Allow arbitrary scripts": "Дазволіць адвольны скрыпты", "Accept any file size": "Прымаць любы памер файла", "Allow stopped analysis": "Дазволіць спынены аналіз", "Never hide focus buttons": "Ніколі не хаваць кнопкі фокуса", "Never grayout move info": "Ніколі не рабіць інфармацыю пра ход шэрай", "Use lowerbound / upperbound info": "Выкарыстоўваць інфармацыю пра ніжнюю / верхнюю мяжу", "Suppress ucinewgame": "Падавіць ucinewgame", "Log RAM state to console": "Запісаць стан RAM у кансоль", "Fire GC": "Запусціць GC", "Logging": "Вядзенне часопіса", "Use logfile...": "Выкарыстоўваць файл часопіса...", "Disable logging": "Адключыць вядзенне часопіса", "Clear log when opening": "Ачысціць часопіс пры адкрыцці", "Use unique logfile each time": "Выкарыстоўваць унікальны файл часопіса кожны раз", "Log illegal moves": "Запісваць нелегальныя хады", "Log positions": "Запісваць пазіцыі", "Log info lines": "Запісваць інфармацыйныя радкі", "...including useless lines": "...у тым ліку бескарысныя радкі", "Language": "Мова", "RESTART_REQUIRED": "Праграму трэба перазапусціць.", }, // TRADITIONAL CHINESE ........................................................................ "繁體中文": { "File": "檔案", "About": "關於", "New game": "新棋局", "New 960 game": "新 960 棋局", "Open PGN...": "打開 PGN…", "Load FEN / PGN from clipboard": "從剪貼簿載入 FEN / PGN", "Save this game...": "儲存此棋局…", "Write PGN to clipboard": "將 PGN 寫入剪貼簿", "PGN saved statistics": "PGN 儲存統計", "EV": "評估值", "Centipawns": "百分子", "N (%)": "節點數 (%)", "N (absolute)": "節點數 (絕對值)", "...out of total": "…總節點數", "Depth (A/B only)": "深度 (僅 A/B)", "Cut": "剪下", "Copy": "複製", "Paste": "貼上", "Quit": "退出", "Tree": "樹狀結構", "Play engine choice": "執行引擎選擇", "1st": "第一", "2nd": "第二", "3rd": "第三", "4th": "第四", "Root": "根節點", "End": "末梢節點", "Backward": "向後", "Forward": "向前", "Previous sibling": "前一兄弟節點", "Next sibling": "下一兄弟節點", "Return to main line": "返回主變化", "Promote line to main line": "將分支升為主變化", "Promote line by 1 level": "升級分支 1 級", "Delete node": "刪除節點", "Delete children": "刪除子節點", "Delete siblings": "刪除兄弟節點", "Delete ALL other lines": "刪除所有其他變化", "Show PGN games list": "顯示 PGN 棋局列表", "Escape": "取消", "Analysis": "分析", "Go": "執行", "Go and lock engine": "執行並鎖定引擎", "Return to locked position": "返回鎖定位置", "Halt": "停止", "Auto-evaluate line": "自動評估變化", "Auto-evaluate line, backwards": "自動反向評估變化", "Show focus (searchmoves) buttons": "顯示焦點(搜尋走法)按鈕", "Clear focus": "清除焦點", "Invert focus": "反轉焦點", "Winrate POV": "勝率視角", "Current": "當前", "White": "白方", "Black": "黑方", "Centipawn POV": "百分子視角", "Win / draw / loss POV": "勝/和/負視角", "PV clicks": "PV 點擊", "Do nothing": "不執行任何操作", "Go there": "移動到此處", "Add to tree": "加入樹結構", "Write infobox to clipboard": "將信息框寫入剪貼簿", "Forget all analysis": "忘記所有分析", "Display": "顯示", "Flip board": "翻轉棋盤", "Arrows": "箭頭", "Piece-click spotlight": "棋子點擊高亮", "Always show actual move (if known)": "始終顯示實際走法(如果已知)", "...with unique colour": "…使用唯一顏色", "...with outline": "…加上外框", "Arrowhead type": "箭頭類型", "Winrate": "勝率", "Node %": "節點 %", "Policy": "策略值", "MultiPV rank": "多 PV 排名", "Moves Left Head": "剩餘走法", "Arrow filter (Lc0)": "箭頭過濾器(Lc0)", "All moves": "所有走法", "Top move": "最佳走法", "Arrow filter (others)": "箭頭過濾器(其他)", "Diff < 15%": "差異 < 15%", "Diff < 10%": "差異 < 10%", "Diff < 5%": "差異 < 5%", "Infobox stats": "信息框統計", "N - nodes (%)": "N - 節點數 (%)", "N - nodes (absolute)": "N - 節點數 (絕對值)", "P - policy": "P - 策略值", "V - static evaluation": "V - 靜態評估", "Q - evaluation": "Q - 評估值", "U - uncertainty": "U - 不確定性", "S - search priority": "S - 搜索優先級", "M - moves left": "M - 剩餘走法", "WDL - win / draw / loss": "WDL - 勝 / 和 / 負", "Linebreak before stats": "統計前換行", "PV move numbers": "PV 走法編號", "Online API": "線上 API", "None": "無", "ChessDB.cn evals": "ChessDB.cn 評估", "Lichess results (masters)": "Lichess 結果(高手)", "Lichess results (plebs)": "Lichess 結果(普通玩家)", "Set Lichess API token": "設定 Lichess API 權杖", "Allow API after move 25": "允許第 25 步後使用 API", "Draw PV on mouseover": "滑鼠懸停顯示 PV", "Draw PV method": "PV 顯示方式", "Animate": "動畫化", "Single move": "單步", "Final position": "終局", "Pieces": "棋子", "Choose pieces folder...": "選擇棋子資料夾…", "Default": "預設", "About custom pieces": "關於自定義棋子", "Background": "背景", "Choose background image...": "選擇背景圖片…", "Book frequency arrows": "開局書頻率箭頭", "Lichess frequency arrows": "Lichess 頻率箭頭", "Sizes": "大小", "Infobox font": "信息框字體", "Move history font": "走法歷史字體", "Board": "棋盤", "Giant": "巨大", "Large": "大", "Medium": "中", "Small": "小", "Graph": "圖表", "Graph lines": "圖表線條", "I want other size options!": "我想要其他大小選項!", "Engine": "引擎", "Choose engine...": "選擇引擎…", "Choose known engine...": "選擇已知引擎…", "Weights": "權重檔案", "Lc0 WeightsFile...": "Lc0 權重檔案…", "Stockfish EvalFile...": "Stockfish 評估檔案…", "Set to ": "設定為 <自動>", "Backend": "後端", "Choose Syzygy path...": "選擇 Syzygy 路徑…", "Unset": "取消設定", "Limit - normal": "限制 - 正常", "Unlimited": "無限制", "Up slightly": "稍微增加", "Down slightly": "稍微減少", "Limit - auto-eval / play": "限制 - 自動評估/遊戲", "Limit by time instead of nodes": "以時間而非節點限制", "Threads": "執行緒", "Warning about threads": "執行緒警告", "Hash": "雜湊", "I want other hash options!": "我想要其他雜湊選項!", "MultiPV": "多 PV", "Contempt Mode": "偏見模式", "White analysis": "白方分析", "Black analysis": "黑方分析", "Contempt": "偏見值", "WDL Calibration Elo": "WDL 校準 Elo", "Use default WDL": "使用預設 WDL", "WDL Eval Objectivity": "WDL 評估客觀性", "Yes": "是", "No": "否", "Score Type": "分數類型", "Custom scripts": "自定義腳本", "How to add scripts": "如何添加腳本", "Show scripts folder": "顯示腳本資料夾", "Restart engine": "重啟引擎", "Soft engine reset": "軟重置引擎", "Play": "對弈", "Play this colour": "用此顏色對弈", "Start self-play": "開始自我對弈", "Use Polyglot book...": "使用 Polyglot 開局書…", "Use PGN book...": "使用 PGN 開局書…", "Unload book / abort load": "卸載開局書/中止載入", "Book depth limit": "開局書深度限制", "Temperature": "溫度", "Temp Decay Moves": "溫度遞減步數", "Infinite": "無限", "About play modes": "關於對弈模式", "Dev": "開發", "Toggle Developer Tools": "切換開發者工具", "Toggle Debug CSS": "切換除錯 CSS", "Permanently enable save": "永久啟用儲存", [show_config]: "顯示 config.json", [show_engineconfig]: "顯示 engines.json", [reload_engineconfig]: "重新載入 engines.json(並重啟引擎)", "Random move": "隨機走法", "Disable hardware acceleration for GUI": "禁用 GUI 硬體加速", "Spin rate": "旋轉速度", "Frenetic": "極快", "Fast": "快", "Normal": "正常", "Relaxed": "放鬆", "Lazy": "懶散", "Show engine state": "顯示引擎狀態", "List sent options": "列出已發送選項", "Show error log": "顯示錯誤日誌", "Hacks and kludges": "進階設定", "Allow arbitrary scripts": "允許任意腳本", "Accept any file size": "接受任意檔案大小", "Allow stopped analysis": "允許停止的分析", "Never hide focus buttons": "永不隱藏焦點按鈕", "Never grayout move info": "永不灰顯走法信息", "Use lowerbound / upperbound info": "使用下限/上限信息", "Suppress ucinewgame": "抑制 ucinewgame", "Log RAM state to console": "記錄 RAM 狀態至控制台", "Fire GC": "啟動垃圾回收", "Logging": "記錄", "Use logfile...": "使用日誌檔案…", "Disable logging": "禁用記錄", "Clear log when opening": "打開時清除日誌", "Use unique logfile each time": "每次使用唯一日誌檔案", "Log illegal moves": "記錄非法走法", "Log positions": "記錄位置", "Log info lines": "記錄信息行", "...including useless lines": "…包括無用行", "Language": "語言", "RESTART_REQUIRED": "需要關閉並重新起動程式" }, // SIMPLIFIED CHINESE ......................................................................... "简体中文": { "File": "文件", "About": "关于", "New game": "新棋局", "New 960 game": "新 960 棋局", "Open PGN...": "打开 PGN...", "Load FEN / PGN from clipboard": "从剪贴板加载 FEN / PGN", "Save this game...": "保存此棋局...", "Write PGN to clipboard": "将 PGN 写入剪贴板", "PGN saved statistics": "PGN 保存统计", "EV": "评估值", "Centipawns": "百分子", "N (%)": "节点数 (%)", "N (absolute)": "节点数 (绝对值)", "...out of total": "...总节点数", "Depth (A/B only)": "深度 (仅 A/B)", "Cut": "剪切", "Copy": "复制", "Paste": "粘贴", "Quit": "退出", "Tree": "树状结构", "Play engine choice": "执行引擎走法", "1st": "第一", "2nd": "第二", "3rd": "第三", "4th": "第四", "Root": "根节点", "End": "末梢节点", "Backward": "向后", "Forward": "向前", "Previous sibling": "前一兄弟节点", "Next sibling": "下一兄弟节点", "Return to main line": "返回主变化", "Promote line to main line": "将分支升为主变化", "Promote line by 1 level": "升级分支 1 级", "Delete node": "删除节点", "Delete children": "删除子节点", "Delete siblings": "删除兄弟节点", "Delete ALL other lines": "删除所有其他变化", "Show PGN games list": "显示 PGN 棋局列表", "Escape": "取消", "Analysis": "分析", "Go": "执行", "Go and lock engine": "执行并锁定引擎", "Return to locked position": "返回锁定位置", "Halt": "停止", "Auto-evaluate line": "自动评估变化", "Auto-evaluate line, backwards": "自动反向评估变化", "Show focus (searchmoves) buttons": "显示焦点(搜索走法)按钮", "Clear focus": "清除焦点", "Invert focus": "反转焦点", "Winrate POV": "胜率视角", "Current": "当前", "White": "白方", "Black": "黑方", "Centipawn POV": "百分子视角", "Win / draw / loss POV": "胜/和/负视角", "PV clicks": "PV 点击", "Do nothing": "不执行任何操作", "Go there": "移动到此处", "Add to tree": "加入树结构", "Write infobox to clipboard": "将信息框写入剪贴板", "Forget all analysis": "忘记所有分析", "Display": "显示", "Flip board": "翻转棋盘", "Arrows": "箭头", "Piece-click spotlight": "棋子点击高亮", "Always show actual move (if known)": "始终显示实际走法(如果已知)", "...with unique colour": "...使用唯一颜色", "...with outline": "...加上外框", "Arrowhead type": "箭头类型", "Winrate": "胜率", "Node %": "节点 %", "Policy": "策略值", "MultiPV rank": "多 PV 排名", "Moves Left Head": "剩余走法", "Arrow filter (Lc0)": "箭头过滤器(Lc0)", "All moves": "所有走法", "Top move": "最佳走法", "Arrow filter (others)": "箭头过滤器(其他)", "Diff < 15%": "差异 < 15%", "Diff < 10%": "差异 < 10%", "Diff < 5%": "差异 < 5%", "Infobox stats": "信息框统计", "N - nodes (%)": "N - 节点数 (%)", "N - nodes (absolute)": "N - 节点数 (绝对值)", "P - policy": "P - 策略值", "V - static evaluation": "V - 静态评估", "Q - evaluation": "Q - 评估值", "U - uncertainty": "U - 不确定性", "S - search priority": "S - 搜索优先级", "M - moves left": "M - 剩余走法", "WDL - win / draw / loss": "WDL - 胜 / 和 / 负", "Linebreak before stats": "统计前换行", "PV move numbers": "PV 走法编号", "Online API": "在线 API", "None": "无", "ChessDB.cn evals": "ChessDB.cn 评估", "Lichess results (masters)": "Lichess 结果(大师)", "Lichess results (plebs)": "Lichess 结果(普通玩家)", "Set Lichess API token": "设置 Lichess API 令牌", "Allow API after move 25": "允许第 25 步后使用 API", "Draw PV on mouseover": "鼠标悬停显示 PV", "Draw PV method": "PV 显示方式", "Animate": "动画化", "Single move": "单步", "Final position": "终局", "Pieces": "棋子", "Choose pieces folder...": "选择棋子文件夹...", "Default": "默认", "About custom pieces": "关于自定义棋子", "Background": "背景", "Choose background image...": "选择背景图片...", "Book frequency arrows": "开局书频率箭头", "Lichess frequency arrows": "Lichess 频率箭头", "Sizes": "大小", "Infobox font": "信息框字体", "Move history font": "走法历史字体", "Board": "棋盘", "Giant": "巨大", "Large": "大", "Medium": "中", "Small": "小", "Graph": "图表", "Graph lines": "图表线条", "I want other size options!": "我想要其他大小选项!", "Engine": "引擎", "Choose engine...": "选择引擎...", "Choose known engine...": "选择已知引擎...", "Weights": "权重文件", "Lc0 WeightsFile...": "Lc0 权重文件...", "Stockfish EvalFile...": "Stockfish 评估文件...", "Set to ": "设置为 <自动>", "Backend": "后端", "Choose Syzygy path...": "选择 Syzygy 路径...", "Unset": "取消设置", "Limit - normal": "限制 - 正常", "Unlimited": "无限制", "Up slightly": "稍微增加", "Down slightly": "稍微减少", "Limit - auto-eval / play": "限制 - 自动评估/游戏", "Limit by time instead of nodes": "以时间而非节点限制", "Threads": "线程", "Warning about threads": "线程警告", "Hash": "哈希", "I want other hash options!": "我想要其他哈希选项!", "MultiPV": "多 PV", "Contempt Mode": "偏见模式", "White analysis": "白方分析", "Black analysis": "黑方分析", "Contempt": "偏见值", "WDL Calibration Elo": "WDL 校准 Elo", "Use default WDL": "使用默认 WDL", "WDL Eval Objectivity": "WDL 评估客观性", "Yes": "是", "No": "否", "Score Type": "分数类型", "Custom scripts": "自定义脚本", "How to add scripts": "如何添加脚本", "Show scripts folder": "显示脚本文件夹", "Restart engine": "重启引擎", "Soft engine reset": "软重置引擎", "Play": "对弈", "Play this colour": "用此颜色对弈", "Start self-play": "开始自我对弈", "Use Polyglot book...": "使用 Polyglot 开局书...", "Use PGN book...": "使用 PGN 开局书...", "Unload book / abort load": "卸载开局书 / 中止加载", "Book depth limit": "开局书深度限制", "Temperature": "温度", "Temp Decay Moves": "温度递减步数", "Infinite": "无限", "About play modes": "关于对弈模式", "Dev": "开发", "Toggle Developer Tools": "切换开发者工具", "Toggle Debug CSS": "切换调试 CSS", "Permanently enable save": "永久启用保存", [show_config]: "显示 config.json", [show_engineconfig]: "显示 engines.json", [reload_engineconfig]: "重新加载 engines.json(并重启引擎)", "Random move": "随机走法", "Disable hardware acceleration for GUI": "禁用 GUI 硬件加速", "Spin rate": "旋转速度", "Frenetic": "极快", "Fast": "快", "Normal": "正常", "Relaxed": "放松", "Lazy": "懒散", "Show engine state": "显示引擎状态", "List sent options": "列出已发送选项", "Show error log": "显示错误日志", "Hacks and kludges": "高级设置", "Allow arbitrary scripts": "允许任意脚本", "Accept any file size": "接受任意文件大小", "Allow stopped analysis": "允许停止的分析", "Never hide focus buttons": "永不隐藏焦点按钮", "Never grayout move info": "永不灰显走法信息", "Use lowerbound / upperbound info": "使用下限/上限信息", "Suppress ucinewgame": "抑制 ucinewgame", "Log RAM state to console": "记录 RAM 状态至控制台", "Fire GC": "执行垃圾回收", "Logging": "日志记录", "Use logfile...": "使用日志文件...", "Disable logging": "禁用日志记录", "Clear log when opening": "打开时清除日志", "Use unique logfile each time": "每次使用唯一日志文件", "Log illegal moves": "记录非法走法", "Log positions": "记录位置", "Log info lines": "记录信息行", "...including useless lines": "...包括无用行", "Language": "语言", "RESTART_REQUIRED": "需要关闭并重新启动程序" }, // HINDI ...................................................................................... "हिंदी": { "File": "फ़ाइल", "About": "परिचय", "New game": "नया खेल", "New 960 game": "नया 960 खेल", "Open PGN...": "PGN खोलें...", "Load FEN / PGN from clipboard": "क्लिपबोर्ड से FEN / PGN लोड करें", "Save this game...": "यह खेल सहेजें...", "Write PGN to clipboard": "PGN को क्लिपबोर्ड में लिखें", "PGN saved statistics": "PGN सहेजे गए आंकड़े", "EV": "EV", "Centipawns": "Centipawns", "N (%)": "N (%)", "N (absolute)": "N (पूर्ण)", "...out of total": "कुल में से", "Depth (A/B only)": "Depth (A/B केवल)", "Cut": "काटें", "Copy": "कॉपी करें", "Paste": "पेस्ट करें", "Quit": "बंद करें", "Tree": "ट्री", "Play engine choice": "इंजन की पसंद खेलें", "1st": "पहला", "2nd": "दूसरा", "3rd": "तीसरा", "4th": "चौथा", "Root": "मूल", "End": "अंत", "Backward": "पीछे", "Forward": "आगे", "Previous sibling": "पिछला सिबलिंग", "Next sibling": "अगला सिबलिंग", "Return to main line": "मुख्य लाइन पर वापस जाएं", "Promote line to main line": "लाइन को मुख्य लाइन बनाएं", "Promote line by 1 level": "लाइन को 1 स्तर बढ़ाएं", "Delete node": "नोड हटाएं", "Delete children": "चिल्ड्रन हटाएं", "Delete siblings": "सिबलिंग्स हटाएं", "Delete ALL other lines": "सभी अन्य लाइनें हटाएं", "Show PGN games list": "PGN खेलों की सूची दिखाएं", "Escape": "एस्केप", "Analysis": "विश्लेषण", "Go": "शुरू करें", "Go and lock engine": "इंजन को लॉक करें और शुरू करें", "Return to locked position": "लॉक की गई स्थिति पर वापस जाएं", "Halt": "रोकें", "Auto-evaluate line": "लाइन का स्वतः मूल्यांकन करें", "Auto-evaluate line, backwards": "लाइन का पीछे से स्वतः मूल्यांकन करें", "Show focus (searchmoves) buttons": "फोकस (searchmoves) बटन दिखाएं", "Clear focus": "फोकस हटाएं", "Invert focus": "फोकस उलटें", "Winrate POV": "जीत दर POV", "Current": "वर्तमान", "White": "सफेद", "Black": "काला", "Centipawn POV": "Centipawn POV", "Win / draw / loss POV": "जीत / ड्रॉ / हार POV", "PV clicks": "PV क्लिक", "Do nothing": "कुछ न करें", "Go there": "वहां जाएं", "Add to tree": "ट्री में जोड़ें", "Write infobox to clipboard": "जानकारी बॉक्स को क्लिपबोर्ड में लिखें", "Forget all analysis": "सभी विश्लेषण भूल जाएं", "Display": "प्रदर्शन", "Flip board": "बोर्ड पलटें", "Arrows": "तीर", "Piece-click spotlight": "मोहरा-क्लिक स्पॉटलाइट", "Always show actual move (if known)": "वास्तविक चाल हमेशा दिखाएं (यदि ज्ञात हो)", "...with unique colour": "...विशिष्ट रंग के साथ", "...with outline": "...आउटलाइन के साथ", "Arrowhead type": "तीर का प्रकार", "Winrate": "जीत दर", "Node %": "Node %", "Policy": "Policy", "MultiPV rank": "MultiPV रैंक", "Moves Left Head": "शेष चालें Head", "Arrow filter (Lc0)": "तीर फ़िल्टर (Lc0)", "All moves": "सभी चालें", "Top move": "शीर्ष चाल", "Arrow filter (others)": "तीर फ़िल्टर (अन्य)", "Diff < 15%": "अंतर < 15%", "Diff < 10%": "अंतर < 10%", "Diff < 5%": "अंतर < 5%", "Infobox stats": "जानकारी बॉक्स आंकड़े", "N - nodes (%)": "N - nodes (%)", "N - nodes (absolute)": "N - nodes (पूर्ण)", "P - policy": "P - policy", "V - static evaluation": "V - static evaluation", "Q - evaluation": "Q - evaluation", "U - uncertainty": "U - uncertainty", "S - search priority": "S - search priority", "M - moves left": "M - शेष चालें", "WDL - win / draw / loss": "WDL - जीत / ड्रॉ / हार", "Linebreak before stats": "आंकड़ों से पहले लाइनब्रेक", "PV move numbers": "PV चाल संख्या", "Online API": "ऑनलाइन API", "None": "कोई नहीं", "ChessDB.cn evals": "ChessDB.cn evals", "Lichess results (masters)": "Lichess परिणाम (मास्टर्स)", "Lichess results (plebs)": "Lichess परिणाम (सामान्य)", "Set Lichess API token": "Lichess API टोकन सेट करें", "Allow API after move 25": "25वीं चाल के बाद API की अनुमति दें", "Draw PV on mouseover": "माउसओवर पर PV दिखाएं", "Draw PV method": "PV दिखाने का तरीका", "Animate": "एनिमेट करें", "Single move": "एक चाल", "Final position": "अंतिम स्थिति", "Pieces": "मोहरे", "Choose pieces folder...": "मोहरों की फ़ोल्डर चुनें...", "Default": "डिफ़ॉल्ट", "About custom pieces": "कस्टम मोहरों के बारे में", "Background": "पृष्ठभूमि", "Choose background image...": "पृष्ठभूमि छवि चुनें...", "Book frequency arrows": "बुक फ्रीक्वेंसी तीर", "Lichess frequency arrows": "Lichess फ्रीक्वेंसी तीर", "Sizes": "आकार", "Infobox font": "जानकारी बॉक्स फ़ॉन्ट", "Move history font": "चाल इतिहास फ़ॉन्ट", "Board": "बोर्ड", "Giant": "विशाल", "Large": "बड़ा", "Medium": "मध्यम", "Small": "छोटा", "Graph": "ग्राफ़", "Graph lines": "ग्राफ़ रेखाएं", "I want other size options!": "मुझे अन्य आकार विकल्प चाहिए!", "Engine": "इंजन", "Choose engine...": "इंजन चुनें...", "Choose known engine...": "ज्ञात इंजन चुनें...", "Weights": "वेट्स", "Lc0 WeightsFile...": "Lc0 WeightsFile...", "Stockfish EvalFile...": "Stockfish EvalFile...", "Set to ": " पर सेट करें", "Backend": "बैकएंड", "Choose Syzygy path...": "Syzygy पथ चुनें...", "Unset": "अनसेट करें", "Limit - normal": "सीमा - सामान्य", "Unlimited": "असीमित", "Up slightly": "थोड़ा ऊपर", "Down slightly": "थोड़ा नीचे", "Limit - auto-eval / play": "सीमा - स्वतः-मूल्यांकन / खेल", "Limit by time instead of nodes": "नोड्स के बजाय समय से सीमित करें", "Threads": "थ्रेड्स", "Warning about threads": "थ्रेड्स के बारे में चेतावनी", "Hash": "हैश", "I want other hash options!": "मुझे अन्य हैश विकल्प चाहिए!", "MultiPV": "MultiPV", "Contempt Mode": "तिरस्कार मोड", "White analysis": "सफेद विश्लेषण", "Black analysis": "काला विश्लेषण", "Contempt": "तिरस्कार", "WDL Calibration Elo": "WDL कैलिब्रेशन Elo", "Use default WDL": "डिफ़ॉल्ट WDL का उपयोग करें", "WDL Eval Objectivity": "WDL Eval वस्तुनिष्ठता", "Yes": "हाँ", "No": "नहीं", "Score Type": "स्कोर प्रकार", "Custom scripts": "कस्टम स्क्रिप्ट", "How to add scripts": "स्क्रिप्ट कैसे जोड़ें", "Show scripts folder": "स्क्रिप्ट फ़ोल्डर दिखाएं", "Restart engine": "इंजन पुनः प्रारंभ करें", "Soft engine reset": "सॉफ्ट इंजन रीसेट", "Play": "खेलें", "Play this colour": "इस रंग से खेलें", "Start self-play": "सेल्फ-प्ले शुरू करें", "Use Polyglot book...": "Polyglot बुक का उपयोग करें...", "Use PGN book...": "PGN बुक का उपयोग करें...", "Unload book / abort load": "बुक अनलोड करें / लोड रोकें", "Book depth limit": "बुक गहराई सीमा", "Temperature": "तापमान", "Temp Decay Moves": "Temp Decay Moves", "Infinite": "अनंत", "About play modes": "खेल मोड के बारे में", "Dev": "डेव", "Toggle Developer Tools": "डेवलपर टूल्स टॉगल करें", "Toggle Debug CSS": "डीबग CSS टॉगल करें", "Permanently enable save": "सहेजना स्थायी रूप से सक्षम करें", [show_config]: "config.json दिखाएं", [show_engineconfig]: "engines.json दिखाएं", [reload_engineconfig]: "engines.json पुनः लोड करें (और इंजन पुनः प्रारंभ करें)", "Random move": "रैंडम चाल", "Disable hardware acceleration for GUI": "GUI के लिए हार्डवेयर एक्सेलरेशन अक्षम करें", "Spin rate": "स्पिन दर", "Frenetic": "उन्मत्त", "Fast": "तेज़", "Normal": "सामान्य", "Relaxed": "आराम से", "Lazy": "आलसी", "Show engine state": "इंजन स्थिति दिखाएं", "List sent options": "भेजे गए विकल्प सूचीबद्ध करें", "Show error log": "त्रुटि लॉग दिखाएं", "Hacks and kludges": "हैक्स और क्लज", "Allow arbitrary scripts": "मनमाने स्क्रिप्ट की अनुमति दें", "Accept any file size": "किसी भी फ़ाइल आकार को स्वीकार करें", "Allow stopped analysis": "रुका हुआ विश्लेषण अनुमत करें", "Never hide focus buttons": "फोकस बटन कभी न छिपाएं", "Never grayout move info": "चाल की जानकारी कभी धूमिल न करें", "Use lowerbound / upperbound info": "लोअरबाउंड / अपरबाउंड जानकारी का उपयोग करें", "Suppress ucinewgame": "ucinewgame दबाएं", "Log RAM state to console": "RAM स्थिति को कंसोल में लॉग करें", "Fire GC": "GC चलाएं", "Logging": "लॉगिंग", "Use logfile...": "लॉगफ़ाइल का उपयोग करें...", "Disable logging": "लॉगिंग अक्षम करें", "Clear log when opening": "खोलते समय लॉग साफ़ करें", "Use unique logfile each time": "हर बार अलग लॉगफ़ाइल का उपयोग करें", "Log illegal moves": "अवैध चालें लॉग करें", "Log positions": "स्थितियां लॉग करें", "Log info lines": "जानकारी पंक्तियां लॉग करें", "...including useless lines": "...बेकार पंक्तियों सहित", "Language": "भाषा", "RESTART_REQUIRED": "GUI को अब पुनः प्रारंभ किया जाना चाहिए।" }, // BENGALI .................................................................................... "বাংলা": { "File": "ফাইল", "About": "সম্পর্কে", "New game": "নতুন খেলা", "New 960 game": "নতুন ৯৬০ খেলা", "Open PGN...": "PGN খুলুন...", "Load FEN / PGN from clipboard": "ক্লিপবোর্ড থেকে FEN / PGN লোড করুন", "Save this game...": "এই খেলাটি সংরক্ষণ করুন...", "Write PGN to clipboard": "ক্লিপবোর্ডে PGN লিখুন", "PGN saved statistics": "সংরক্ষিত PGN পরিসংখ্যান", "EV": "EV", "Centipawns": "সেন্টিপন", "N (%)": "N (%)", "N (absolute)": "N (পূর্ণ)", "...out of total": "মোট থেকে...", "Depth (A/B only)": "গভীরতা (শুধু A/B)", "Cut": "কাট", "Copy": "কপি", "Paste": "পেস্ট", "Quit": "প্রস্থান", "Tree": "গেমট্রি", "Play engine choice": "ইঞ্জিন পছন্দ চালান", "1st": "১ম", "2nd": "২য়", "3rd": "৩য়", "4th": "৪র্থ", "Root": "মূল", "End": "শেষ", "Backward": "পিছনে", "Forward": "সামনে", "Previous sibling": "পূর্ববর্তী শাখা", "Next sibling": "পরবর্তী শাখা", "Return to main line": "মূল লাইনে ফিরুন", "Promote line to main line": "লাইনকে মূল লাইনে উন্নীত করুন", "Promote line by 1 level": "লাইনকে ১ স্তর উন্নীত করুন", "Delete node": "নোড মুছুন", "Delete children": "চাইল্ড মুছুন", "Delete siblings": "সিবলিং মুছুন", "Delete ALL other lines": "অন্য সব লাইন মুছুন", "Show PGN games list": "PGN খেলার তালিকা দেখান", "Escape": "প্রস্থান", "Analysis": "বিশ্লেষণ", "Go": "শুরু", "Go and lock engine": "ইঞ্জিন লক করে শুরু করুন", "Return to locked position": "লক করা অবস্থানে ফিরুন", "Halt": "থামুন", "Auto-evaluate line": "স্বয়ংক্রিয় লাইন মূল্যায়ন", "Auto-evaluate line, backwards": "স্বয়ংক্রিয় লাইন মূল্যায়ন, পিছনে", "Show focus (searchmoves) buttons": "ফোকাস (সার্চমুভস) বাটন দেখান", "Clear focus": "ফোকাস মুছুন", "Invert focus": "ফোকাস উল্টান", "Winrate POV": "জয়হার দৃষ্টিকোণ", "Current": "বর্তমান", "White": "সাদা", "Black": "কালো", "Centipawn POV": "সেন্টিপন দৃষ্টিকোণ", "Win / draw / loss POV": "জয় / ড্র / হার দৃষ্টিকোণ", "PV clicks": "PV ক্লিক", "Do nothing": "কিছু করবেন না", "Go there": "সেখানে যান", "Add to tree": "ট্রিতে যোগ করুন", "Write infobox to clipboard": "তথ্যবাক্স ক্লিপবোর্ডে লিখুন", "Forget all analysis": "সব বিশ্লেষণ ভুলে যান", "Display": "প্রদর্শন করুন", "Flip board": "বোর্ড উল্টান", "Arrows": "তীর", "Piece-click spotlight": "পিস-ক্লিক স্পটলাইট", "Always show actual move (if known)": "প্রকৃত চাল দেখান (জানা থাকলে)", "...with unique colour": "...বিশেষ রঙের সাথে", "...with outline": "...আউটলাইন সহ", "Arrowhead type": "তীরের মাথার ধরন", "Winrate": "জয়হার", "Node %": "নোড %", "Policy": "পলিসি", "MultiPV rank": "MultiPV র‍্যাঙ্ক", "Moves Left Head": "বাকি চাল", "Arrow filter (Lc0)": "তীর ফিল্টার (Lc0)", "All moves": "সব চাল", "Top move": "সেরা চাল", "Arrow filter (others)": "তীর ফিল্টার (অন্যান্য)", "Diff < 15%": "পার্থক্য < ১৫%", "Diff < 10%": "পার্থক্য < ১০%", "Diff < 5%": "পার্থক্য < ৫%", "Infobox stats": "তথ্যবাক্স পরিসংখ্যান", "N - nodes (%)": "N - নোড (%)", "N - nodes (absolute)": "N - নোড (পূর্ণ)", "P - policy": "P - পলিসি", "V - static evaluation": "V - স্থির মূল্যায়ন", "Q - evaluation": "Q - মূল্যায়ন", "U - uncertainty": "U - অনিশ্চয়তা", "S - search priority": "S - অনুসন্ধান অগ্রাধিকার", "M - moves left": "M - বাকি চাল", "WDL - win / draw / loss": "WDL - জয় / ড্র / হার", "Linebreak before stats": "পরিসংখ্যানের আগে লাইনব্রেক", "PV move numbers": "PV চাল সংখ্যা", "Online API": "অনলাইন API", "None": "কোনটি নয়", "ChessDB.cn evals": "ChessDB.cn মূল্যায়ন", "Lichess results (masters)": "Lichess ফলাফল (মাস্টার্স)", "Lichess results (plebs)": "Lichess ফলাফল (সাধারণ)", "Set Lichess API token": "Lichess API টোকেন সেট করুন", "Allow API after move 25": "২৫তম চালের পর API অনুমতি দিন", "Draw PV on mouseover": "মাউস হোভারে PV আঁকুন", "Draw PV method": "PV আঁকার পদ্ধতি", "Animate": "অ্যানিমেট", "Single move": "একক চাল", "Final position": "চূড়ান্ত অবস্থান", "Pieces": "পিস", "Choose pieces folder...": "পিস ফোল্ডার বাছুন...", "Default": "ডিফল্ট", "About custom pieces": "কাস্টম পিস সম্পর্কে", "Background": "পটভূমি", "Choose background image...": "পটভূমি ছবি বাছুন...", "Book frequency arrows": "বুক ফ্রিকোয়েন্সি তীর", "Lichess frequency arrows": "Lichess ফ্রিকোয়েন্সি তীর", "Sizes": "আকার", "Infobox font": "তথ্যবাক্স ফন্ট", "Move history font": "চাল ইতিহাস ফন্ট", "Board": "বোর্ড", "Giant": "বিশাল", "Large": "বড়", "Medium": "মাঝারি", "Small": "ছোট", "Graph": "গ্রাফ", "Graph lines": "গ্রাফ লাইন", "I want other size options!": "আমি অন্য আকারের অপশন চাই!", "Engine": "ইঞ্জিন", "Choose engine...": "ইঞ্জিন বাছুন...", "Choose known engine...": "পরিচিত ইঞ্জিন বাছুন...", "Weights": "ওয়েট", "Lc0 WeightsFile...": "Lc0 ওয়েট ফাইল...", "Stockfish EvalFile...": "Stockfish মূল্যায়ন ফাইল...", "Set to ": "-তে সেট করুন", "Backend": "ব্যাকএন্ড", "Choose Syzygy path...": "Syzygy পাথ বাছুন...", "Unset": "আনসেট", "Limit - normal": "সীমা - স্বাভাবিক", "Unlimited": "অসীম", "Up slightly": "সামান্য বাড়ান", "Down slightly": "সামান্য কমান", "Limit - auto-eval / play": "সীমা - অটো-মূল্যায়ন / খেলা", "Limit by time instead of nodes": "নোডের পরিবর্তে সময় দ্বারা সীমিত করুন", "Threads": "থ্রেড", "Warning about threads": "থ্রেড সম্পর্কে সতর্কতা", "Hash": "হ্যাশ", "I want other hash options!": "আমি অন্য হ্যাশ অপশন চাই!", "MultiPV": "MultiPV", "Contempt Mode": "Contempt মোড", "White analysis": "সাদার বিশ্লেষণ", "Black analysis": "কালোর বিশ্লেষণ", "Contempt": "Contempt", "WDL Calibration Elo": "WDL ক্যালিব্রেশন Elo", "Use default WDL": "ডিফল্ট WDL ব্যবহার করুন", "WDL Eval Objectivity": "WDL মূল্যায়ন বাস্তবতা", "Yes": "হ্যাঁ", "No": "না", "Score Type": "স্কোর টাইপ", "Custom scripts": "কাস্টম স্ক্রিপ্ট", "How to add scripts": "স্ক্রিপ্ট কিভাবে যোগ করবেন", "Show scripts folder": "স্ক্রিপ্ট ফোল্ডার দেখান", "Restart engine": "ইঞ্জিন রিস্টার্ট করুন", "Soft engine reset": "সফট ইঞ্জিন রিসেট", "Play": "খেলুন", "Play this colour": "এই রং খেলুন", "Start self-play": "সেলফ-প্লে শুরু করুন", "Use Polyglot book...": "পলিগ্লট বুক ব্যবহার করুন...", "Use PGN book...": "PGN বুক ব্যবহার করুন...", "Unload book / abort load": "বুক আনলোড / লোড বাতিল করুন", "Book depth limit": "বুক ডেপথ সীমা", "Temperature": "তাপমাত্রা", "Temp Decay Moves": "টেম্প ডিকে মুভস", "Infinite": "অসীম", "About play modes": "খেলার মোড সম্পর্কে", "Dev": "ডেভ", "Toggle Developer Tools": "ডেভেলপার টুল টগল করুন", "Toggle Debug CSS": "ডিবাগ CSS টগল করুন", "Permanently enable save": "স্থায়ীভাবে সেভ সক্রিয় করুন", [show_config]: "config.json দেখান", [show_engineconfig]: "engines.json দেখান", [reload_engineconfig]: "engines.json রিলোড করুন (এবং ইঞ্জিন রিস্টার্ট করুন)", "Random move": "এলোমেলো চাল", "Disable hardware acceleration for GUI": "GUI-এর হার্ডওয়্যার অ্যাক্সিলারেশন নিষ্ক্রিয় করুন", "Spin rate": "স্পিন রেট", "Frenetic": "উন্মত্ত", "Fast": "দ্রুত", "Normal": "স্বাভাবিক", "Relaxed": "শিথিল", "Lazy": "অলস", "Show engine state": "ইঞ্জিন অবস্থা দেখান", "List sent options": "পাঠানো অপশন তালিকা", "Show error log": "ত্রুটি লগ দেখান", "Hacks and kludges": "হ্যাকস এবং ক্লাজ", "Allow arbitrary scripts": "অনির্দিষ্ট স্ক্রিপ্ট অনুমতি দিন", "Accept any file size": "যেকোনো ফাইল সাইজ গ্রহণ করুন", "Allow stopped analysis": "থামানো বিশ্লেষণ অনুমতি দিন", "Never hide focus buttons": "ফোকাস বাটন কখনও লুকাবেন না", "Never grayout move info": "চাল তথ্য কখনও ম্লান করবেন না", "Use lowerbound / upperbound info": "লোয়ারবাউন্ড / আপারবাউন্ড তথ্য ব্যবহার করুন", "Suppress ucinewgame": "ucinewgame দমন করুন", "Log RAM state to console": "কনসোলে RAM অবস্থা লগ করুন", "Fire GC": "GC চালু করুন", "Logging": "লগিং", "Use logfile...": "লগফাইল ব্যবহার করুন...", "Disable logging": "লগিং নিষ্ক্রিয় করুন", "Clear log when opening": "খোলার সময় লগ মুছুন", "Use unique logfile each time": "প্রতিবার অনন্য লগফাইল ব্যবহার করুন", "Log illegal moves": "অবৈধ চাল লগ করুন", "Log positions": "অবস্থান লগ করুন", "Log info lines": "তথ্য লাইন লগ করুন", "...including useless lines": "...অপ্রয়োজনীয় লাইন সহ", "Language": "ভাষা", "RESTART_REQUIRED": "GUI এখন পুনরায় চালু করতে হবে।" }, // FARSI ...................................................................................... "فارسی": { "File": "پرونده", "About": "درباره", "New game": "بازی جدید", "New 960 game": "بازی ۹۶۰ جدید", "Open PGN...": "باز کردن PGN...", "Load FEN / PGN from clipboard": "بارگیری FEN / PGN از کلیپ‌بورد", "Save this game...": "ذخیره این بازی...", "Write PGN to clipboard": "نوشتن PGN در کلیپ‌بورد", "PGN saved statistics": "آمار ذخیره‌شده PGN", "EV": "ارزش مورد انتظار", "Centipawns": "سنتی‌پاون", "N (%)": "تعداد (٪)", "N (absolute)": "تعداد (مطلق)", "...out of total": "از کل", "Depth (A/B only)": "عمق (فقط A/B)", "Cut": "برش", "Copy": "کپی", "Paste": "چسباندن", "Quit": "خروج", "Tree": "درخت", "Play engine choice": "انتخاب موتور", "1st": "۱ام", "2nd": "۲ام", "3rd": "۳ام", "4th": "۴ام", "Root": "ریشه", "End": "پایان", "Backward": "عقب", "Forward": "جلو", "Previous sibling": "حرکت قبلی", "Next sibling": "حرکت بعدی", "Return to main line": "بازگشت به خط اصلی", "Promote line to main line": "ارتقا به خط اصلی", "Promote line by 1 level": "ارتقا یک سطح", "Delete node": "حذف گره", "Delete children": "حذف فرزندان", "Delete siblings": "حذف هم‌سطح‌ها", "Delete ALL other lines": "حذف تمام خطوط دیگر", "Show PGN games list": "نمایش لیست بازی‌های PGN", "Escape": "خروج", "Analysis": "تحلیل", "Go": "شروع", "Go and lock engine": "شروع و قفل موتور", "Return to locked position": "بازگشت به وضعیت قفل شده", "Halt": "توقف", "Auto-evaluate line": "ارزیابی خودکار خط", "Auto-evaluate line, backwards": "ارزیابی خودکار خط، معکوس", "Show focus (searchmoves) buttons": "نمایش دکمه‌های تمرکز", "Clear focus": "پاک کردن تمرکز", "Invert focus": "معکوس کردن تمرکز", "Winrate POV": "دیدگاه نرخ برد", "Current": "فعلی", "White": "سفید", "Black": "سیاه", "Centipawn POV": "دیدگاه سنتی‌پاون", "Win / draw / loss POV": "دیدگاه برد/مساوی/باخت", "PV clicks": "کلیک‌های PV", "Do nothing": "هیچ کاری نکن", "Go there": "برو آنجا", "Add to tree": "اضافه به درخت", "Write infobox to clipboard": "نوشتن جعبه اطلاعات در کلیپ‌بورد", "Forget all analysis": "فراموش کردن همه تحلیل‌ها", "Display": "نمایش", "Flip board": "چرخاندن صفحه", "Arrows": "پیکان‌ها", "Piece-click spotlight": "نورافکن کلیک مهره", "Always show actual move (if known)": "نمایش همیشگی حرکت واقعی", "...with unique colour": "با رنگ منحصر به فرد", "...with outline": "با حاشیه", "Arrowhead type": "نوع سر پیکان", "Winrate": "نرخ برد", "Node %": "گره ٪", "Policy": "سیاست", "MultiPV rank": "رتبه MultiPV", "Moves Left Head": "حرکات باقی‌مانده", "Arrow filter (Lc0)": "فیلتر پیکان (Lc0)", "All moves": "همه حرکت‌ها", "Top move": "حرکت برتر", "Arrow filter (others)": "فیلتر پیکان (دیگران)", "Diff < 15%": "اختلاف < ۱۵٪", "Diff < 10%": "اختلاف < ۱۰٪", "Diff < 5%": "اختلاف < ۵٪", "Infobox stats": "آمار جعبه اطلاعات", "N - nodes (%)": "N - گره‌ها (٪)", "N - nodes (absolute)": "N - گره‌ها (مطلق)", "P - policy": "P - سیاست", "V - static evaluation": "V - ارزیابی استاتیک", "Q - evaluation": "Q - ارزیابی", "U - uncertainty": "U - عدم قطعیت", "S - search priority": "S - اولویت جستجو", "M - moves left": "M - حرکات باقی‌مانده", "WDL - win / draw / loss": "WDL - برد/مساوی/باخت", "Linebreak before stats": "خط جدید قبل از آمار", "PV move numbers": "شماره حرکت‌های PV", "Online API": "API آنلاین", "None": "هیچکدام", "ChessDB.cn evals": "ارزیابی‌های ChessDB.cn", "Lichess results (masters)": "نتایج Lichess (استادان)", "Lichess results (plebs)": "نتایج Lichess (عادی)", "Set Lichess API token": "تنظیم توکن API لیچس", "Allow API after move 25": "اجازه API بعد از حرکت ۲۵", "Draw PV on mouseover": "ترسیم PV هنگام حرکت موس", "Draw PV method": "روش ترسیم PV", "Animate": "متحرک", "Single move": "حرکت تکی", "Final position": "وضعیت نهایی", "Pieces": "مهره‌ها", "Choose pieces folder...": "انتخاب پوشه مهره‌ها...", "Default": "پیش‌فرض", "About custom pieces": "درباره مهره‌های سفارشی", "Background": "پس‌زمینه", "Choose background image...": "انتخاب تصویر پس‌زمینه...", "Book frequency arrows": "پیکان‌های فراوانی کتاب", "Lichess frequency arrows": "پیکان‌های فراوانی Lichess", "Sizes": "اندازه‌ها", "Infobox font": "فونت جعبه اطلاعات", "Move history font": "فونت تاریخچه حرکات", "Board": "صفحه", "Giant": "بسیار بزرگ", "Large": "بزرگ", "Medium": "متوسط", "Small": "کوچک", "Graph": "نمودار", "Graph lines": "خطوط نمودار", "I want other size options!": "گزینه‌های اندازه دیگر می‌خواهم!", "Engine": "موتور", "Choose engine...": "انتخاب موتور...", "Choose known engine...": "انتخاب موتور شناخته شده...", "Weights": "وزن‌ها", "Lc0 WeightsFile...": "فایل وزن Lc0...", "Stockfish EvalFile...": "فایل ارزیابی Stockfish...", "Set to ": "تنظیم به <خودکار>", "Backend": "پشتیبان", "Choose Syzygy path...": "انتخاب مسیر Syzygy...", "Unset": "حذف تنظیم", "Limit - normal": "محدودیت - عادی", "Unlimited": "نامحدود", "Up slightly": "کمی بالا", "Down slightly": "کمی پایین", "Limit - auto-eval / play": "محدودیت - ارزیابی/بازی خودکار", "Limit by time instead of nodes": "محدودیت زمانی به جای گره‌ها", "Threads": "نخ‌ها", "Warning about threads": "هشدار درباره نخ‌ها", "Hash": "هش", "I want other hash options!": "گزینه‌های هش دیگر می‌خواهم!", "MultiPV": "MultiPV", "Contempt Mode": "حالت تحقیر", "White analysis": "تحلیل سفید", "Black analysis": "تحلیل سیاه", "Contempt": "تحقیر", "WDL Calibration Elo": "تنظیم Elo WDL", "Use default WDL": "استفاده از WDL پیش‌فرض", "WDL Eval Objectivity": "عینیت ارزیابی WDL", "Yes": "بله", "No": "خیر", "Score Type": "نوع امتیاز", "Custom scripts": "اسکریپت‌های سفارشی", "How to add scripts": "نحوه افزودن اسکریپت‌ها", "Show scripts folder": "نمایش پوشه اسکریپت‌ها", "Restart engine": "راه‌اندازی مجدد موتور", "Soft engine reset": "بازنشانی نرم موتور", "Play": "بازی", "Play this colour": "بازی با این رنگ", "Start self-play": "شروع بازی با خود", "Use Polyglot book...": "استفاده از کتاب Polyglot...", "Use PGN book...": "استفاده از کتاب PGN...", "Unload book / abort load": "لغو بارگیری کتاب", "Book depth limit": "محدودیت عمق کتاب", "Temperature": "دما", "Temp Decay Moves": "کاهش دمای حرکات", "Infinite": "بی‌نهایت", "About play modes": "درباره حالت‌های بازی", "Dev": "توسعه", "Toggle Developer Tools": "ابزار توسعه‌دهنده", "Toggle Debug CSS": "CSS اشکال‌زدایی", "Permanently enable save": "فعال‌سازی دائمی ذخیره", [show_config]: "نمایش config.json", [show_engineconfig]: "نمایش engines.json", [reload_engineconfig]: "بارگیری مجدد engines.json (و راه‌اندازی مجدد موتور)", "Random move": "حرکت تصادفی", "Disable hardware acceleration for GUI": "غیرفعال‌سازی شتاب سخت‌افزاری برای رابط کاربری", "Spin rate": "نرخ چرخش", "Frenetic": "شتابان", "Fast": "سریع", "Normal": "عادی", "Relaxed": "آرام", "Lazy": "کند", "Show engine state": "نمایش وضعیت موتور", "List sent options": "لیست گزینه‌های ارسال شده", "Show error log": "نمایش گزارش خطا", "Hacks and kludges": "هک‌ها و وصله‌ها", "Allow arbitrary scripts": "اجازه اسکریپت‌های دلخواه", "Accept any file size": "پذیرش هر اندازه فایل", "Allow stopped analysis": "اجازه تحلیل متوقف شده", "Never hide focus buttons": "عدم مخفی کردن دکمه‌های تمرکز", "Never grayout move info": "عدم کم‌رنگ کردن اطلاعات حرکت", "Use lowerbound / upperbound info": "استفاده از اطلاعات کران پایین/بالا", "Suppress ucinewgame": "جلوگیری از ucinewgame", "Log RAM state to console": "ثبت وضعیت RAM در کنسول", "Fire GC": "اجرای GC", "Logging": "ثبت گزارش", "Use logfile...": "استفاده از فایل گزارش...", "Disable logging": "غیرفعال کردن ثبت گزارش", "Clear log when opening": "پاک کردن گزارش هنگام باز کردن", "Use unique logfile each time": "استفاده از فایل گزارش یکتا هر بار", "Log illegal moves": "ثبت حرکات غیرمجاز", "Log positions": "ثبت موقعیت‌ها", "Log info lines": "ثبت خطوط اطلاعات", "...including useless lines": "شامل خطوط بی‌فایده", "Language": "زبان", "RESTART_REQUIRED": "رابط کاربری باید مجدداً راه‌اندازی شود." }, // HEBREW ..................................................................................... "עברית": { "File": "קובץ", "About": "על אודות", "New game": "התחל משחק חדש", "New 960 game": "משחק 960 חדש", "Open PGN...": "פתח קובץ PGN...", "Load FEN / PGN from clipboard": "טען FEN / PGN מהלוח", "Save this game...": "שמור משחק זה...", "Write PGN to clipboard": "העתק PGN ללוח", "PGN saved statistics": "סטטיסטיקת PGN שמורה", "EV": "ערך צפוי", "Centipawns": "מאיות רגלי", "N (%)": "צמתים (%)", "N (absolute)": "צמתים (מוחלט)", "...out of total": "...מתוך סך הכל", "Depth (A/B only)": "עומק החיפוש", "Cut": "גזור", "Copy": "העתק", "Paste": "הדבק", "Quit": "סגור", "Tree": "עץ", "Play engine choice": "בחר מהלך מנוע", "1st": "ראשון", "2nd": "שני", "3rd": "שלישי", "4th": "רביעי", "Root": "שורש", "End": "סוף", "Backward": "אחורה", "Forward": "קדימה", "Previous sibling": "אח קודם", "Next sibling": "אח הבא", "Return to main line": "חזור למהלך הראשי", "Promote line to main line": "קדם קו לראשי", "Promote line by 1 level": "קדם קו ברמה אחת", "Delete node": "מחק צומת", "Delete children": "מחק צאצאים", "Delete siblings": "מחק אחים", "Delete ALL other lines": "מחק את כל הקווים אחרים", "Show PGN games list": "הצג רשימת משחקים", "Escape": "ביטול", "Analysis": "ניתוח", "Go": "התחל", "Go and lock engine": "התחל ונעל מנוע ניתוח", "Return to locked position": "חזור לעמדה נעולה", "Halt": "עצור", "Auto-evaluate line": "נתח קו אוטומטית", "Auto-evaluate line, backwards": "נתח קו אוטומטית, אחורה", "Show focus (searchmoves) buttons": "הצג כפתורי מיקוד", "Clear focus": "נקה מיקוד", "Invert focus": "הפוך מיקוד", "Winrate POV": "נקודת מבט סיכויי ניצחון", "Current": "נוכחי", "White": "לבן", "Black": "שחור", "Centipawn POV": "נקודת מבט מאיות רגלי", "Win / draw / loss POV": "נקודת מבט ניצחון/תיקו/הפסד", "PV clicks": "לחיצות PV", "Do nothing": "אל תעשה כלום", "Go there": "עבור לשם", "Add to tree": "הוסף לעץ", "Write infobox to clipboard": "העתק חלונית מידע ללוח", "Forget all analysis": "שכח כל הניתוחים", "Display": "תצוגה", "Flip board": "הפוך לוח", "Arrows": "חצים", "Piece-click spotlight": "הדגשת מהלכים חוקיים בלחיצה", "Always show actual move (if known)": "הצג תמיד מהלך בפועל", "...with unique colour": "...בצבע ייחודי", "...with outline": "...עם מתאר", "Arrowhead type": "סוג ראש חץ", "Winrate": "אחוזי ניצחון", "Node %": "אחוז צמתים", "Policy": "התפלגות אפריורי", "MultiPV rank": "דרוג MultiPV", "Moves Left Head": "ראש חיזוי מהלכים נותרים", "Arrow filter (Lc0)": "סינון חצים (Lc0)", "All moves": "כל המהלכים", "Top move": "המהלך המועדף", "Arrow filter (others)": "סינון חצים (אחרים)", "Diff < 15%": "הפרש הערכה < 15%", "Diff < 10%": "הפרש הערכה < 10%", "Diff < 5%": "הפרש הערכה < 5%", "Infobox stats": "חלונית מידע", "N - nodes (%)": "צמתים (%)", "N - nodes (absolute)": "צמתים (מספר כולל)", "P - policy": "הסתברויות רשת", "V - static evaluation": "הערכת עמדה ללא חיפוש", "Q - evaluation": "הערכת עמדה עם חיפוש", "U - uncertainty": "בונוס למהלכים מבטיחים לא-נחקרים", "S - search priority": "עדיפות חיפוש", "M - moves left": "מהלכים שנותרו", "WDL - win / draw / loss": "ניצחון/תיקו/הפסד", "Linebreak before stats": "שורה חדשה לפני נתונים", "PV move numbers": "מספרי מהלכים PV", "Online API": "API", "None": "ללא", "ChessDB.cn evals": "הערכות ChessDB.cn", "Lichess results (masters)": "תוצאות Lichess (אמנים)", "Lichess results (plebs)": "תוצאות Lichess (כל השחקנים)", "Set Lichess API token": "הגדרת טוקן API של Lichess", "Allow API after move 25": "אפשר API אחרי מהלך 25", "Draw PV on mouseover": "צייר PV במעבר עכבר", "Draw PV method": "שיטת ציור PV", "Animate": "אנימציה", "Single move": "מהלך בודד", "Final position": "עמדה סופית", "Pieces": "כלים", "Choose pieces folder...": "בחר תיקיית כלים...", "Default": "ברירת מחדל", "About custom pieces": "אודות כלים מותאמים", "Background": "רקע", "Choose background image...": "בחר תמונת רקע...", "Book frequency arrows": "חיצי שכיחות בספר פתיחות", "Lichess frequency arrows": "חיצי שכיחות ב-Lichess", "Sizes": "גדלים", "Infobox font": "גופן חלונית מידע", "Move history font": "גופן היסטוריית מהלכים", "Board": "לוח", "Giant": "ענק", "Large": "גדול", "Medium": "בינוני", "Small": "קטן", "Graph": "גרף", "Graph lines": "קווי גרף", "I want other size options!": "אני רוצה אפשרויות גודל אחרות!", "Engine": "מנוע ניתוח", "Choose engine...": "בחר מנוע ניתוח...", "Choose known engine...": "בחר מנוע ניתוח מוכר...", "Weights": "משקולות", "Lc0 WeightsFile...": "קובץ משקולות Lc0...", "Stockfish EvalFile...": "קובץ הערכה Stockfish...", "Set to ": "הגדר לאוטומטי", "Backend": "Backend", "Choose Syzygy path...": "בחר נתיב Syzygy...", "Unset": "בטל הגדרה", "Limit - normal": "מגבלה - רגיל", "Unlimited": "ללא הגבלה", "Up slightly": "עלה מעט", "Down slightly": "רד מעט", "Limit - auto-eval / play": "מגבלה - הערכה אוטומטית / משחק", "Limit by time instead of nodes": "הגבל לפי זמן במקום צמתים", "Threads": "תהליכונים", "Warning about threads": "אזהרה לגבי תהליכונים", "Hash": "זיכרון מטמון", "I want other hash options!": "אני רוצה אפשרויות מטמון אחרות!", "MultiPV": "ניתוח מרובה", "Contempt Mode": "מצב הטיית תיקו", "White analysis": "ניתוח לבן", "Black analysis": "ניתוח שחור", "Contempt": "הטיית תיקו", "WDL Calibration Elo": "כיול WDL Elo", "Use default WDL": "השתמש ב-WDL ברירת מחדל", "WDL Eval Objectivity": "אובייקטיביות הערכת WDL", "Yes": "כן", "No": "לא", "Score Type": "סוג ניקוד", "Custom scripts": "סקריפטים מותאמים", "How to add scripts": "איך להוסיף סקריפטים", "Show scripts folder": "הצג תיקיית סקריפטים", "Restart engine": "הפעל מחדש מנוע ניתוח", "Soft engine reset": "איפוס חלקי למנוע ניתוח", "Play": "משחק", "Play this colour": "שחק בצבע זה", "Start self-play": "התחל משחק עצמי", "Use Polyglot book...": "השתמש בספר פתיחות Polyglot...", "Use PGN book...": "השתמש בספר פתיחות PGN...", "Unload book / abort load": "בטל טעינת ספר פתיחות", "Book depth limit": "מגבלת עומק ספר פתיחות", "Temperature": "רמת אקראיות", "Temp Decay Moves": "מספר מהלכים לדעיכת אקראיות", "Infinite": "אינסוף", "About play modes": "אודות מצבי משחק", "Dev": "פיתוח", "Toggle Developer Tools": "הצג/הסתר כלי פיתוח", "Toggle Debug CSS": "הצג/הסתר CSS לדיבוג", "Permanently enable save": "אפשר שמירה לצמיתות", [show_config]: "הצג config.json", [show_engineconfig]: "הצג engines.json", [reload_engineconfig]: "טען מחדש engines.json (והפעל מנוע מחדש)", "Random move": "מהלך אקראי", "Disable hardware acceleration for GUI": "בטל האצת חומרה לממשק", "Spin rate": "תדירות עדכון", "Frenetic": "קדחתני", "Fast": "מהיר", "Normal": "רגיל", "Relaxed": "רגוע", "Lazy": "עצלן", "Show engine state": "הצג מצב מנוע ניתוח", "List sent options": "הצג אפשרויות שנשלחו", "Show error log": "הצג יומן שגיאות", "Hacks and kludges": "אפשרויות מתקדמות ומעקפים", "Allow arbitrary scripts": "אפשר סקריפטים שרירותיים", "Accept any file size": "קבל כל גודל קובץ", "Allow stopped analysis": "אפשר ניתוח שהופסק", "Never hide focus buttons": "לעולם אל תסתיר כפתורי מיקוד", "Never grayout move info": "לעולם אל תאפיר מידע מהלכים", "Use lowerbound / upperbound info": "השתמש במידע גבול תחתון/עליון", "Suppress ucinewgame": "דכא ucinewgame", "Log RAM state to console": "רשום מצב RAM למסוף", "Fire GC": "כפה איסוף זיכרון", "Logging": "רישום", "Use logfile...": "השתמש בקובץ רישום...", "Disable logging": "בטל רישום", "Clear log when opening": "נקה רישום בפתיחה", "Use unique logfile each time": "השתמש בקובץ רישום ייחודי בכל פעם", "Log illegal moves": "רשום מהלכים לא חוקיים", "Log positions": "רשום עמדות", "Log info lines": "רשום שורות מידע", "...including useless lines": "...כולל שורות חסרות תועלת", "Language": "שפה", "RESTART_REQUIRED": "יש להפעיל את התוכנה מחדש כדי להחיל את השינויים" }, // ARABIC ..................................................................................... "العربية": { "File": "ملف", "About": "حول", "New game": "لعبة جديدة", "New 960 game": "لعبة 960 جديدة", "Open PGN...": "فتح PGN...", "Load FEN / PGN from clipboard": "تحميل FEN / PGN من الحافظة", "Save this game...": "حفظ هذه اللعبة...", "Write PGN to clipboard": "نسخ PGN إلى الحافظة", "PGN saved statistics": "إحصائيات PGN المحفوظة", "EV": "EV", "Centipawns": "Centipawns", "N (%)": "N (%)", "N (absolute)": "N (مطلق)", "...out of total": "من المجموع", "Depth (A/B only)": "العمق (A/B فقط)", "Cut": "قص", "Copy": "نسخ", "Paste": "لصق", "Quit": "خروج", "Tree": "شجرة", "Play engine choice": "لعب اختيار المحرك", "1st": "الأول", "2nd": "الثاني", "3rd": "الثالث", "4th": "الرابع", "Root": "الجذر", "End": "النهاية", "Backward": "للخلف", "Forward": "للأمام", "Previous sibling": "التفرع السابق", "Next sibling": "التفرع التالي", "Return to main line": "العودة إلى الخط الرئيسي", "Promote line to main line": "ترقية الخط إلى الخط الرئيسي", "Promote line by 1 level": "ترقية الخط مستوى واحد", "Delete node": "حذف العقدة", "Delete children": "حذف الفروع", "Delete siblings": "حذف التفرعات", "Delete ALL other lines": "حذف جميع الخطوط الأخرى", "Show PGN games list": "عرض قائمة ألعاب PGN", "Escape": "خروج", "Analysis": "تحليل", "Go": "ابدأ", "Go and lock engine": "ابدأ وثبت المحرك", "Return to locked position": "العودة إلى الوضع المثبت", "Halt": "توقف", "Auto-evaluate line": "تقييم تلقائي للخط", "Auto-evaluate line, backwards": "تقييم تلقائي للخط، للخلف", "Show focus (searchmoves) buttons": "إظهار أزرار التركيز", "Clear focus": "مسح التركيز", "Invert focus": "عكس التركيز", "Winrate POV": "وجهة نظر نسبة الفوز", "Current": "الحالي", "White": "الأبيض", "Black": "الأسود", "Centipawn POV": "وجهة نظر Centipawn", "Win / draw / loss POV": "وجهة نظر الفوز/التعادل/الخسارة", "PV clicks": "نقرات PV", "Do nothing": "لا شيء", "Go there": "اذهب هناك", "Add to tree": "أضف إلى الشجرة", "Write infobox to clipboard": "نسخ صندوق المعلومات إلى الحافظة", "Forget all analysis": "نسيان كل التحليلات", "Display": "عرض", "Flip board": "قلب اللوحة", "Arrows": "الأسهم", "Piece-click spotlight": "تسليط الضوء على النقرات", "Always show actual move (if known)": "إظهار النقلة الفعلية دائماً (إن وجدت)", "...with unique colour": "...بلون مميز", "...with outline": "...مع إطار", "Arrowhead type": "نوع رأس السهم", "Winrate": "نسبة الفوز", "Node %": "العقد %", "Policy": "السياسة", "MultiPV rank": "ترتيب MultiPV", "Moves Left Head": "النقلات المتبقية", "Arrow filter (Lc0)": "فلتر الأسهم (Lc0)", "All moves": "كل النقلات", "Top move": "أفضل نقلة", "Arrow filter (others)": "فلتر الأسهم (أخرى)", "Diff < 15%": "الفرق < 15%", "Diff < 10%": "الفرق < 10%", "Diff < 5%": "الفرق < 5%", "Infobox stats": "إحصائيات صندوق المعلومات", "N - nodes (%)": "N - العقد (%)", "N - nodes (absolute)": "N - العقد (مطلق)", "P - policy": "P - السياسة", "V - static evaluation": "V - التقييم الثابت", "Q - evaluation": "Q - التقييم", "U - uncertainty": "U - عدم اليقين", "S - search priority": "S - أولوية البحث", "M - moves left": "M - النقلات المتبقية", "WDL - win / draw / loss": "WDL - فوز/تعادل/خسارة", "Linebreak before stats": "سطر جديد قبل الإحصائيات", "PV move numbers": "أرقام نقلات PV", "Online API": "Online API", "None": "لا شيء", "ChessDB.cn evals": "تقييمات ChessDB.cn", "Lichess results (masters)": "نتائج Lichess (الأساتذة)", "Lichess results (plebs)": "نتائج Lichess (العامة)", "Set Lichess API token": "تعيين رمز API الخاص بـ Lichess", "Allow API after move 25": "السماح بـ API بعد النقلة 25", "Draw PV on mouseover": "رسم PV عند تمرير الماوس", "Draw PV method": "طريقة رسم PV", "Animate": "متحرك", "Single move": "نقلة واحدة", "Final position": "الوضع النهائي", "Pieces": "القطع", "Choose pieces folder...": "اختيار مجلد القطع...", "Default": "افتراضي", "About custom pieces": "حول القطع المخصصة", "Background": "الخلفية", "Choose background image...": "اختيار صورة الخلفية...", "Book frequency arrows": "أسهم تكرار الكتاب", "Lichess frequency arrows": "أسهم تكرار Lichess", "Sizes": "الأحجام", "Infobox font": "خط صندوق المعلومات", "Move history font": "خط تاريخ النقلات", "Board": "اللوحة", "Giant": "ضخم", "Large": "كبير", "Medium": "متوسط", "Small": "صغير", "Graph": "الرسم البياني", "Graph lines": "خطوط الرسم البياني", "I want other size options!": "أريد خيارات أحجام أخرى!", "Engine": "المحرك", "Choose engine...": "اختيار المحرك...", "Choose known engine...": "اختيار محرك معروف...", "Weights": "الأوزان", "Lc0 WeightsFile...": "ملف أوزان Lc0...", "Stockfish EvalFile...": "ملف تقييم Stockfish...", "Set to ": "تعيين إلى ", "Backend": "Backend", "Choose Syzygy path...": "اختيار مسار Syzygy...", "Unset": "إلغاء التعيين", "Limit - normal": "الحد - عادي", "Unlimited": "غير محدود", "Up slightly": "زيادة قليلة", "Down slightly": "نقص قليل", "Limit - auto-eval / play": "الحد - تقييم تلقائي / لعب", "Limit by time instead of nodes": "الحد بالوقت بدلاً من العقد", "Threads": "Threads", "Warning about threads": "تحذير حول Threads", "Hash": "Hash", "I want other hash options!": "أريد خيارات hash أخرى!", "MultiPV": "MultiPV", "Contempt Mode": "وضع Contempt", "White analysis": "تحليل الأبيض", "Black analysis": "تحليل الأسود", "Contempt": "Contempt", "WDL Calibration Elo": "معايرة WDL Elo", "Use default WDL": "استخدام WDL الافتراضي", "WDL Eval Objectivity": "موضوعية تقييم WDL", "Yes": "نعم", "No": "لا", "Score Type": "نوع النتيجة", "Custom scripts": "نصوص مخصصة", "How to add scripts": "كيفية إضافة النصوص", "Show scripts folder": "عرض مجلد النصوص", "Restart engine": "إعادة تشغيل المحرك", "Soft engine reset": "إعادة تعيين المحرك", "Play": "لعب", "Play this colour": "لعب هذا اللون", "Start self-play": "بدء اللعب الذاتي", "Use Polyglot book...": "استخدام كتاب Polyglot...", "Use PGN book...": "استخدام كتاب PGN...", "Unload book / abort load": "إلغاء تحميل الكتاب", "Book depth limit": "حد عمق الكتاب", "Temperature": "Temperature", "Temp Decay Moves": "Temp Decay Moves", "Infinite": "لا نهائي", "About play modes": "حول أوضاع اللعب", "Dev": "Dev", "Toggle Developer Tools": "تبديل أدوات المطور", "Toggle Debug CSS": "تبديل Debug CSS", "Permanently enable save": "تمكين الحفظ دائماً", [show_config]: "عرض config.json", [show_engineconfig]: "عرض engines.json", [reload_engineconfig]: "إعادة تحميل engines.json (وإعادة تشغيل المحرك)", "Random move": "نقلة عشوائية", "Disable hardware acceleration for GUI": "تعطيل تسريع الأجهزة للواجهة", "Spin rate": "معدل الدوران", "Frenetic": "محموم", "Fast": "سريع", "Normal": "عادي", "Relaxed": "مريح", "Lazy": "كسول", "Show engine state": "عرض حالة المحرك", "List sent options": "قائمة الخيارات المرسلة", "Show error log": "عرض سجل الأخطاء", "Hacks and kludges": "الحلول المؤقتة", "Allow arbitrary scripts": "السماح بالنصوص العشوائية", "Accept any file size": "قبول أي حجم ملف", "Allow stopped analysis": "السماح بالتحليل المتوقف", "Never hide focus buttons": "عدم إخفاء أزرار التركيز", "Never grayout move info": "عدم تعتيم معلومات النقلة", "Use lowerbound / upperbound info": "استخدام معلومات الحد الأدنى/الأعلى", "Suppress ucinewgame": "إخفاء ucinewgame", "Log RAM state to console": "تسجيل حالة RAM في وحدة التحكم", "Fire GC": "تشغيل GC", "Logging": "التسجيل", "Use logfile...": "استخدام ملف السجل...", "Disable logging": "تعطيل التسجيل", "Clear log when opening": "مسح السجل عند الفتح", "Use unique logfile each time": "استخدام ملف سجل فريد كل مرة", "Log illegal moves": "تسجيل النقلات غير القانونية", "Log positions": "تسجيل المواقع", "Log info lines": "تسجيل خطوط المعلومات", "...including useless lines": "...بما في ذلك الخطوط غير المفيدة", "Language": "اللغة", "RESTART_REQUIRED": "يجب إعادة تشغيل الواجهة الآن" } }; // Check dictionaries for missing items... log to console in main process. function checker() { let base_dict = translations["Français"]; let base_keys = Object.keys(base_dict); for (let language of Object.keys(translations)) { if (language === "English") { continue; } let other_dict = translations[language]; let other_keys = Object.keys(other_dict); for (let key of base_keys) { if (other_dict[key] === undefined) { console.log(`${language} missing: ${key}`); } } for (let key of other_keys) { if (base_dict[key] === undefined) { console.log(`${language} has extra: ${key}`); } } } } checker(); module.exports = translations; ================================================ FILE: files/src/nibbler.css ================================================ html { height: 100%; } body { background-color: #080808; border: 0; color: #eeeeee; cursor: default; margin: 0; overflow: hidden; padding: 0; pointer-events: none; /* These must be overriden for things that need pointer / select */ user-select: none; /* These must be overriden for things that need pointer / select */ } ::-webkit-scrollbar { pointer-events: auto; background-color: #181818; } ::-webkit-scrollbar-thumb { pointer-events: auto; background-color: #444444; } #gridder { display: grid; height: 100vh; grid-template-columns: min-content 1fr; grid-template-rows: min-content min-content 1fr; grid-template-areas: "a b" "f f" "g g"; } #rightgridder { grid-area: b; display: grid; margin: 1em 0 0 0; height: 0; /* js needs to keep this equal to the boardsize */ grid-template-columns: none; grid-template-rows: min-content 1fr min-content; grid-template-areas: "c" "d" "e"; } #boardsquares { grid-area: a; margin: 1em 0 0 1em; background-size: cover; border-collapse: collapse; table-layout: fixed; z-index: 1; } #canvas { grid-area: a; margin: 1em 0 0 1em; display: block; outline-offset: 6px; z-index: 2; } #boardfriends { grid-area: a; margin: 1em 0 0 1em; border-collapse: collapse; pointer-events: auto; table-layout: fixed; z-index: 3; } .dragging-piece { cursor: grabbing !important; } #statusbox { grid-area: c; margin: 0 0 0 1em; border: none; display: block; font-family: monospace, monospace; pointer-events: auto; overflow: hidden; white-space: pre; } #infobox { grid-area: d; margin: 1em 1em 0 1em; display: block; color: #cccccc; /* only used for Lc0 stderr output at startup */ font-family: monospace, monospace; overflow-x: hidden; overflow-y: auto; padding-right: 10px; /* so the text doesn't get so near the scroll bar */ pointer-events: auto; white-space: pre-wrap; } #graph { grid-area: e; align-self: end; display: block; margin: 10px 0 0 1em; pointer-events: auto; } input[type=text]:focus { outline: 2px dashed gray; outline-offset: 4px; } #fenbox { grid-area: f; margin: 1em 1em 0 1em; background-color: #080808; border: none; caret-color: white; color: #6cccee; display: block; font-family: monospace, monospace; font-size: 100%; pointer-events: auto; user-select: auto; } #movelist { grid-area: g; margin: 1em 1em 1em 1em; display: block; color: #999999; font-family: monospace, monospace; overflow-x: hidden; overflow-y: auto; padding-right: 10px; /* so the text doesn't get so near the scroll bar */ pointer-events: auto; white-space: pre-wrap; } #promotiontable { border-collapse: collapse; display: none; pointer-events: auto; position: fixed; table-layout: fixed; z-index: 4; } #fullbox { background-color: #080808; display: none; /* better than visibility: hidden - never intercepts inputs */ font-family: monospace, monospace; font-size: 100%; height: 100%; left: 0; overflow-y: auto; pointer-events: auto; position: fixed; top: 0; width: 100%; z-index: 6; } #fullbox_content { overflow: hidden; padding: 1em; white-space: pre; } #config_item_input { background: #101010; border: 1px solid #444444; box-sizing: border-box; color: #dddddd; font-family: inherit; font-size: inherit; max-width: 100%; width: 100%; } #config_item_input:focus { border-color: #444444; outline: none; } td { background-color: transparent; background-size: contain; border: 0; margin: 0; padding: 0; } a, a:link, a:visited, a:hover, a:active { /* I think this is now only used for the "Nibbler in normal browser" message. */ color: #6cccee; } ul { list-style: none; } .pink { color: #ffaaaa; } .white { color: #eeeeee; } .gray { color: #999999; } .darkgray { color: #666666; } .red { color: #ff6666; } .yellow { color: #ffff00; } .green { color: #66ff66; } .blue { color: #6cccee; } .infoline { margin-bottom: 1em; } .enginechooser { margin-bottom: 1em; } .enginechooser:hover { color: #6cccee; } .pgnchooser:hover { background-color: #202020; } .ocm_highlight { background-color: #770000; } .hover_highlight { background-color: #222244; } span.movelist_highlight_blue { background-color: #222244; color: #6cccee; } span.movelist_highlight_yellow { background-color: #444422; color: #ffff00; } span.nobr { white-space: nowrap; /* Used for O-O and O-O-O moves */ } ================================================ FILE: files/src/nibbler.html ================================================ Nibbler
Starting up...
================================================ FILE: files/src/package.json ================================================ { "name": "Nibbler", "version": "2.5.9", "author": "Rooklift", "description": "Leela Chess Zero (Lc0) interface", "license": "GPL-3.0", "main": "main.js", "repository": { "type": "git", "url": "https://github.com/rooklift/nibbler" } } ================================================ FILE: files/src/renderer/10_globals.js ================================================ "use strict"; // HTML stuff....................................................... // // All of this may be redundant since id-havers are in the global // namespace automatically. But declaring them const has some value. const boardfriends = document.getElementById("boardfriends"); const boardsquares = document.getElementById("boardsquares"); const canvas = document.getElementById("canvas"); const fenbox = document.getElementById("fenbox"); const graph = document.getElementById("graph"); const rightgridder = document.getElementById("rightgridder"); const infobox = document.getElementById("infobox"); const movelist = document.getElementById("movelist"); const fullbox = document.getElementById("fullbox"); const fullbox_content = document.getElementById("fullbox_content"); const promotiontable = document.getElementById("promotiontable"); const statusbox = document.getElementById("statusbox"); // If require isn't available, we're in a browser: try { require("./modules/empty"); } catch (err) { statusbox.innerHTML = ` Running Nibbler in a normal browser doesn't work. For the full app, see the Releases section of the repo.

It has also been observed not to work if your path contains a % character.`; } // Requires......................................................... const background = require("./modules/background"); const child_process = require("child_process"); const clipboard = require("electron").clipboard; const config_io = require("./modules/config_io"); const custom_uci = require("./modules/custom_uci"); const engineconfig_io = require("./modules/engineconfig_io"); const fs = require("fs"); const images = require("./modules/images"); const ipcRenderer = require("electron").ipcRenderer; const messages = require("./modules/messages"); const path = require("path"); const querystring = require("querystring"); const readline = require("readline"); const stringify = require("./modules/stringify"); const translate = require("./modules/translate"); const util = require("util"); // Prior to v32, given a file object from an event (e.g. from dragging the file onto the window) // we could simply access its path, but afterwards we need to use a helper function... let webUtils = require("electron").webUtils; const get_path_for_file = (webUtils && webUtils.getPathForFile) ? webUtils.getPathForFile : file => file.path; // Globals.......................................................... const boardctx = canvas.getContext("2d"); const graphctx = graph.getContext("2d"); const decoder = new util.TextDecoder("utf8"); // https://github.com/electron/electron/issues/18733 let [load_err1, config] = config_io.load(); let [load_err2, engineconfig] = engineconfig_io.load(); translate.register_startup_language(config.language); let next_node_id = 1; let live_nodes = Object.create(null); // Replace the renderer's built-in alert().......................... let alert = (msg) => { ipcRenderer.send("alert", stringify(msg)); }; // Get the images loading........................................... if (images.validate_folder(config.override_piece_directory)) { images.load_from(config.override_piece_directory); } else { images.load_from(path.join(__dirname, "pieces")); } // Standard options, for either type of engine...................... // Note that UCI_Chess960 is handled specially by engine.js const forced_lc0_options = { // These are sent without checking if they are known by the engine, so it doesn't matter "LogLiveStats": true, // if Leela is hiding them. Nevertheless, the user can still override them in engines.json. "MoveOverheadMs": 0, "MultiPV": 500, "ScoreType": "WDL_mu", "SmartPruningFactor": 0, "UCI_ShowWDL": true, "VerboseMoveStats": true, }; const standard_lc0_options = { // These are only sent if known by the engine. "ContemptMode": "white_side_analysis", "Contempt": 0, "WDLCalibrationElo": 0, "WDLEvalObjectivity": 0, }; const forced_ab_options = {}; const standard_ab_options = { "Contempt": 0, "Move Overhead": 0, "UCI_ShowWDL": true, }; // Yeah this seemed a good idea at the time......................... const limit_options = [ 1, 2, 5, 10, 20, 50, 100, 125, 160, 200, 250, 320, 400, 500, 640, 800, 1000, 1250, 1600, 2000, 2500, 3200, 4000, 5000, 6400, 8000, 10000, 12500, 16000, 20000, 25000, 32000, 40000, 50000, 64000, 80000, 100000, 125000, 160000, 200000, 250000, 320000, 400000, 500000, 640000, 800000, 1000000, 1250000, 1600000, 2000000, 2500000, 3200000, 4000000, 5000000, 6400000, 8000000, 10000000, 12500000, 16000000, 20000000, 25000000, 32000000, 40000000, 50000000, 64000000, 80000000, 100000000, 125000000, 160000000, 200000000, 250000000, 320000000, 400000000, 500000000, 640000000, 800000000, 1000000000 ]; limit_options.sort((a, b) => a - b); ================================================ FILE: files/src/renderer/20_utils.js ================================================ "use strict"; // At some point I tried caching the results of XY() and S() // but for XY(), object lookups were slower than calculating, // and for S(), it just isn't called enough to matter. function XY(s) { // e.g. "b7" --> [1, 1] if (typeof s !== "string" || s.length !== 2) { return [-1, -1]; } s = s.toLowerCase(); let x = s.charCodeAt(0) - 97; let y = 8 - parseInt(s[1], 10); if (x < 0 || x > 7 || y < 0 || y > 7 || Number.isNaN(y)) { return [-1, -1]; } return [x, y]; } function S(x, y) { // e.g. (1, 1) --> "b7" if (typeof x !== "number" || typeof y !== "number" || x < 0 || x > 7 || y < 0 || y > 7) { return "??"; } let xs = String.fromCharCode(x + 97); let ys = String.fromCharCode((8 - y) + 48); return xs + ys; } function InfoVal(s, key) { // Given some string like "info depth 8 seldepth 22 time 469 nodes 3918 score cp 46 hashfull 13 nps 8353 tbhits 0 multipv 1 pv d2d4 g8f6" // pull the value for the key out, e.g. in this example, key "nps" returns "8353" (as a string). // // Since Lc0's info strings often have the value ending in ")", we strip that out. if (typeof s !== "string" || typeof key !== "string") { return ""; } let tokens = s.split(" ").filter(z => z !== ""); for (let i = 0; i < tokens.length - 1; i++) { if (tokens[i] === key) { if (tokens[i + 1].endsWith(")")) { return tokens[i + 1].slice(0, -1); } else { return tokens[i + 1]; } } } return ""; } function InfoValMany(s, keys) { // Optimised version of InfoVal for when many values can be pulled out of the same string. let ret = Object.create(null); let tokens = s.split(" ").filter(z => z !== ""); for (let key of keys) { let ok = false; for (let i = 0; i < tokens.length - 1; i++) { if (tokens[i] === key) { if (tokens[i + 1].endsWith(")")) { ret[key] = tokens[i + 1].slice(0, -1); } else { ret[key] = tokens[i + 1]; } ok = true; break; } } if (!ok) { ret[key] = ""; } } return ret; } function InfoPV(s) { // Pull the PV out. if (typeof s !== "string") { return []; } let tokens = s.split(" ").filter(z => z !== ""); let pv_index = null; for (let i = 0; i < tokens.length; i++) { if (tokens[i] === "pv") { pv_index = i; break; } } let ret = []; if (pv_index !== null) { for (let i = pv_index + 1; i < tokens.length; i++) { let token = tokens[i]; if (token.length < 4 || token.length > 5) { break; } let codes = [token.charCodeAt(0), token.charCodeAt(1), token.charCodeAt(2), token.charCodeAt(3)]; if (codes[0] < 97 || codes[0] > 104) break; // a - h if (codes[1] < 49 || codes[1] > 56) break; // 1 - 8 if (codes[2] < 97 || codes[2] > 104) break; if (codes[3] < 49 || codes[3] > 56) break; ret.push(token); } } return ret; } function C960_PV_Converter(pv, board) { // Change standard UCI format castling moves in the PV // into our favoured c960 format. In place. let fix_e1g1 = board.state[4][7] === "K" && !board.castling.includes("G"); let fix_e1c1 = board.state[4][7] === "K" && !board.castling.includes("C"); let fix_e8g8 = board.state[4][0] === "k" && !board.castling.includes("g"); let fix_e8c8 = board.state[4][0] === "k" && !board.castling.includes("c"); // Those are the best tests to use here (especially considering that we // seem to be allowing arbitrary / weird castling rights like GHgh). for (let i = 0; i < pv.length; i++) { let token = pv[i]; if (fix_e1g1 && token === "e1g1") { pv[i] = "e1h1"; } else if (fix_e1c1 && token === "e1c1") { pv[i] = "e1a1"; } else if (fix_e8g8 && token === "e8g8") { pv[i] = "e8h8"; } else if (fix_e8c8 && token === "e8c8") { pv[i] = "e8a8"; } let start = token.slice(0, 2); if (start === "e1") { fix_e1g1 = false; fix_e1c1 = false; } if (start === "e8") { fix_e8g8 = false; fix_e8c8 = false; } } } function InfoWDL(s) { if (typeof s !== "string") { return null; } let tokens = s.split(" ").filter(z => z !== ""); let ret = null; for (let i = 0; i < tokens.length - 3; i++) { if (tokens[i] === "wdl") { ret = tokens.slice(i + 1, i + 4); break; } } if (Array.isArray(ret) === false || ret.length !== 3) { return null; } for (let n = 0; n < 3; n++) { let tmp = parseInt(ret[n], 10); if (Number.isNaN(tmp)) { return null; } ret[n] = tmp; } return ret; } function CompareArrays(a, b) { if (Array.isArray(a) === false || Array.isArray(b) === false) { return false; } if (a.length !== b.length) { return false; } for (let n = 0; n < a.length; n++) { if (a[n] !== b[n]) { return false; } } return true; } function ArrayStartsWith(a, b) { // where b is itself an array if (Array.isArray(a) === false || Array.isArray(b) === false) { return false; } if (a.length < b.length) { return false; } for (let n = 0; n < b.length; n++) { if (a[n] !== b[n]) { return false; } } return true; } function OppositeColour(s) { if (s === "w" || s === "W") return "b"; if (s === "b" || s === "B") return "w"; return ""; } function ReplaceAll(s, search, replace) { if (!s.includes(search)) return s; // Seems to improve speed overall. return s.split(search).join(replace); } function SafeStringHTML(s) { if (typeof s !== "string") { return undefined; } s = ReplaceAll(s, `&` , `&` ); // This needs to be first of course. s = ReplaceAll(s, `<` , `<` ); s = ReplaceAll(s, `>` , `>` ); s = ReplaceAll(s, `'` , `'` ); s = ReplaceAll(s, `"` , `"` ); return s; } function UnsafeStringHTML(s) { if (typeof s !== "string") { return undefined; } s = ReplaceAll(s, `"` , `"` ); s = ReplaceAll(s, `'` , `'` ); s = ReplaceAll(s, `>` , `>` ); s = ReplaceAll(s, `<` , `<` ); s = ReplaceAll(s, `&` , `&` ); // So I guess do this last. return s; } function SafeStringPGN(s) { if (typeof s !== "string") { return undefined; } s = ReplaceAll(s, `\\` , `\\\\` ); // Must be first. s = ReplaceAll(s, `"` , `\\"` ); return s; } function UnsafeStringPGN(s) { if (typeof s !== "string") { return undefined; } s = ReplaceAll(s, `\\"` , `"` ); s = ReplaceAll(s, `\\\\` , `\\` ); // So this ought to be last. return s; } function Log(s) { // config.logfile - name of desired log file (or null) // Log.logfilename - name of currently open log file (undefined if none) // Log.stream - actual write stream if (typeof config.logfile !== "string" || config.logfile === "") { if (Log.logfilename) { console.log(`Closing ${Log.logfilename}`); Log.stream.end(); Log.stream = undefined; Log.logfilename = undefined; } return; } // So at this point, we know config.logfile is some string... if (Log.logfilename !== config.logfile) { if (Log.logfilename) { console.log(`Closing log ${Log.logfilename}`); Log.stream.end(); Log.stream = undefined; Log.logfilename = undefined; } let actual_filepath = config.logfile_timestamp ? UniqueFilepath(config.logfile) : config.logfile; // Note that this isn't saved even temporarily - as far as the rest of the logic is concerned, we are logging to config.logfile console.log(`Logging to ${actual_filepath}`); let flags = (config.clear_log) ? "w" : "a"; let stream = fs.createWriteStream(actual_filepath, {flags: flags}); // Want var "stream" available via closure for the below... stream.on("error", (err) => { console.log(err); stream.end(); if (Log.stream === stream) { Log.stream = undefined; Log.logfilename = undefined; config.logfile = null; ipcRenderer.send("ack_logfile", config.logfile); } }); Log.stream = stream; Log.logfilename = config.logfile; } Log.stream.write(s + "\n"); } function LogBoth(s) { console.log(s); Log(s); } function UniqueFilepath(filepath) { const alpha = "abcdefghijklmnopqrstuvwxyz"; let extname = path.extname(filepath); let basename = path.basename(filepath, extname); let dirname = path.dirname(filepath); let dt = new Date(); let y = dt.getFullYear().toString(); let m = (dt.getMonth() + 1).toString(); let d = dt.getDate().toString(); let h = dt.getHours().toString(); let n = dt.getMinutes().toString(); let s = dt.getSeconds().toString(); if (m.length === 1) m = "0" + m; if (d.length === 1) d = "0" + d; if (h.length === 1) h = "0" + h; if (n.length === 1) n = "0" + n; if (s.length === 1) s = "0" + s; let newbase = `${basename}-${y}-${m}-${d}-${h}${n}${s}`; for (let n = 0; n < 26; n++) { let test = path.join(dirname, newbase) + alpha[n] + extname; if (!fs.existsSync(test)) { return test; } } // If you start 27 instances of Nibbler within a second, that's your problem. return filepath; } function New2DArray(width, height, defval) { let ret = []; for (let x = 0; x < width; x++) { ret.push([]); for (let y = 0; y < height; y++) { ret[x].push(defval); } } return ret; } function CanvasCoords(x, y) { // Given the x, y coordinates on the board (a8 is 0, 0) // return an object with the canvas coordinates for // the square, and also the centre. // // x1,y1-------- // | | // | cx,cy | // | | // --------x2,y2 let css = config.square_size; let x1 = x * css; let y1 = y * css; let x2 = x1 + css; let y2 = y1 + css; if (config.flip) { [x1, x2] = [(css * 8) - x2, (css * 8) - x1]; [y1, y2] = [(css * 8) - y2, (css * 8) - y1]; } let cx = x1 + css / 2; let cy = y1 + css / 2; return {x1, y1, x2, y2, cx, cy}; } function EventPathString(event, prefix) { // Given an event with event.path like ["foo", "bar", "searchmove_e2e4", "whatever"] // return the string "e2e4", assuming the prefix matches. Else return null. if (!event || typeof prefix !== "string") { return null; } let path = event.path || (event.composedPath && event.composedPath()); if (path) { for (let item of path) { if (typeof item.id === "string") { if (item.id.startsWith(prefix)) { return item.id.slice(prefix.length); } } } } return null; } function EventPathN(event, prefix) { // As above, but returning a number, or null. let s = EventPathString(event, prefix); if (typeof s !== "string") { return null; } let n = parseInt(s, 10); if (Number.isNaN(n)) { return null; } return n; } function SwapElements(obj1, obj2) { // https://stackoverflow.com/questions/10716986/swap-2-html-elements-and-preserve-event-listeners-on-them let temp = document.createElement("div"); obj1.parentNode.insertBefore(temp, obj1); obj2.parentNode.insertBefore(obj1, obj2); temp.parentNode.insertBefore(obj2, temp); temp.parentNode.removeChild(temp); } function NString(n) { const thousand = 1000; const million = 1000000; const billion = 1000000000; if (typeof n !== "number") { return "?"; } if (n < thousand) { return n.toString(); } if (n < 100 * thousand) { return (n / thousand).toFixed(1) + "k"; } if (n < 999.5 * thousand) { return (n / thousand).toFixed(0) + "k"; } if (n < 100 * million) { return (n / million).toFixed(1) + "M"; } if (n < 999.5 * million) { return (n / million).toFixed(0) + "M"; } if (n < 100 * billion) { return (n / billion).toFixed(1) + "B"; } return (n / billion).toFixed(0) + "B"; } function DateString(dt) { let y = dt.getFullYear(); let m = dt.getMonth() + 1; let d = dt.getDate(); let parts = [ y.toString(), (m > 9 ? "" : "0") + m.toString(), (d > 9 ? "" : "0") + d.toString(), ]; return parts.join("."); } function QfromPawns(pawns) { // Note carefully: the arg is pawns not centipawns. if (typeof pawns !== "number") { return 0; } let winrate = 1 / (1 + Math.pow(10, -pawns / 4)); let q = winrate * 2 - 1; if (q > 0.998) q = 0.998; if (q < -0.998) q = -0.998; return q; } function QfromWDL(wdl) { if (Array.isArray(wdl) === false || wdl.length !== 3) { return 0; } let winrate = (wdl[0] + (wdl[1] * 0.5)) / 1000; let q = winrate * 2 - 1; if (q > 0.998) q = 0.998; if (q < -0.998) q = -0.998; return q; } function Value(q) { // Rescale Q to 0..1 range. if (typeof q !== "number") { return 0; } if (q < -1) { return 0; } if (q > 1) { return 1; } return (q + 1) / 2; } function SmoothStep(x) { if (x < 0) x = 0; if (x > 1) x = 1; return (-2 * x * x * x) + (3 * x * x); } function Sign(n) { if (n < 0) return -1; if (n > 0) return 1; return 0; } function CommaNum(n) { if (typeof n !== "number") { return JSON.stringify(n); } if (n < 1000) { return n.toString(); } let ret = ""; let n_string = n.toString(); for (let i = 0; i < n_string.length; i++) { ret += n_string[i]; if ((n_string.length - i) % 3 === 1 && n_string.length - i > 1) { ret += ","; } } return ret; } function DurationString(ms) { let hours = Math.floor(ms / 3600000); ms -= hours * 3600000; let minutes = Math.floor(ms / 60000); ms -= minutes * 60000; let seconds = Math.floor(ms / 1000); if (hours > 0) { return `${hours}h ${minutes}m`; } if (minutes > 0) { return `${minutes}m ${seconds}s`; } return `${seconds}s`; } function NumbersBetween(a, b) { // Given integers a and b, return a list of integers between the two, inclusive. let add = a < b ? 1 : -1; let ret = []; for (let x = a; x !== b; x += add) { ret.push(x); } ret.push(b); return ret; } function RandInt(min, max) { if (typeof max !== "number") { // DWIM. max = min; min = 0; } if (min >= max) { return min; } let ret = Math.floor(Math.random() * (max - min)) + min; if (ret >= max) { // Probably impossible. ret = min; } return ret; } function RandChoice(arr) { if (Array.isArray(arr) === false || arr.length === 0) { return undefined; } return arr[RandInt(0, arr.length)]; } function HighlightString(s, prefix, classname) { // Highlights the thing after the prefix if (s.startsWith(prefix) === false) { return s; } return prefix + `` + s.slice(prefix.length) + ``; } function StringIsNumeric(s) { for (let i = 0; i < s.length; i++) { let code = s.charCodeAt(i); if (code < 48 || code > 57) { return false; } } return true; } function FileExceedsGigabyte(filename, multiplier = 1) { try { let filesize = fs.statSync(filename).size; if (filesize >= 1073741824 * multiplier) { return true; } else { return false; } } catch (err) { console.log("While checking file size: ", err.toString()); return false; // Eh, who knows } } ================================================ FILE: files/src/renderer/30_point.js ================================================ "use strict"; function Point(a, b) { // Each Point is represented by a single object so that naive equality checking works, i.e. // Point(x, y) === Point(x, y) should be true. Since object comparisons in JS will be false // unless they are the same object, we do the following... // // Returns null on invalid input, therefore the caller should take care to ensure that the // value is not null before accessing .x or .y or .s! if (Point.xy_lookup === undefined) { Point.xy_lookup = New2DArray(8, 8, null); for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { let s = S(x, y); let point = Object.freeze({x, y, s}); Point.xy_lookup[x][y] = point; } } } // Point("a8") or Point(0, 0) are both valid. if (b === undefined) { [a, b] = XY(a); // Possibly [-1, -1] if invalid } let col = Point.xy_lookup[a]; if (col === undefined) return null; let ret = col[b]; if (ret === undefined) return null; return ret; } function PointsBetween(a, b) { // Given points a and b, return a list of points between the two, inclusive. if (!a && !b) return []; if (!a) return [b]; if (!b) return [a]; if (a === b) { return [a]; } let ok = false; if (a.x === b.x) { ok = true; } if (a.y === b.y) { ok = true; } if (Math.abs(a.x - b.x) === Math.abs(a.y - b.y)) { ok = true; } if (ok === false) { return [a, b]; } let stepx = Sign(b.x - a.x); let stepy = Sign(b.y - a.y); let x = a.x; let y = a.y; let ret = []; while (1) { ret.push(Point(x, y)); if (x === b.x && y === b.y) { return ret; } x += stepx; y += stepy; } } ================================================ FILE: files/src/renderer/31_sliders.js ================================================ "use strict"; // This makes an object storing "sliders" for every piece except K and k which are handled // differently. A slider is a list of vectors, which are distances from the origin. function generate_movegen_sliders() { let invert = n => n === 0 ? 0 : -n; // Flip sign without introducing -0 let rotate = xy => [invert(xy[1]), xy[0]]; // Rotate a single vector of form [x,y] let flip = xy => [invert(xy[0]), xy[1]]; // Flip a single vector, horizontally let ret = Object.create(null); // For each of B, R, N, make an initial slider and place it in a new list as item 0... ret.B = [[[1,1], [2,2], [3,3], [4,4], [5,5], [6,6], [7,7]]]; ret.R = [[[1,0], [2,0], [3,0], [4,0], [5,0], [6,0], [7,0]]]; ret.N = [[[1,2]]]; // Add 3 rotations for each... for (let n = 0; n < 3; n++) { ret.B.push(ret.B[n].map(rotate)); ret.R.push(ret.R[n].map(rotate)); ret.N.push(ret.N[n].map(rotate)); } // Add the knight mirrors (knights have 8 sliders of length 1)... ret.N = ret.N.concat(ret.N.map(slider => slider.map(flip))); // Make the queen from the rook and bishop... ret.Q = ret.B.concat(ret.R); // The black lowercase versions can point to the same objects... for (let key of Object.keys(ret)) { ret[key.toLowerCase()] = ret[key]; } // Make the pawns... ret.P = [[[0,-1], [0,-2]], [[-1,-1]], [[1,-1]]]; ret.p = [[[0,1], [0,2]], [[-1,1]], [[1,1]]]; return ret; } let movegen_sliders = generate_movegen_sliders(); ================================================ FILE: files/src/renderer/40_position.js ================================================ "use strict"; // Note that ALL CASTLING MOVES are expected to be in format KING-TO-ROOK (e.g. e1h1). // That is, only Chess960 format is allowed. // // There are awkward ramifications if we allowed one move to have two representations, // so we don't. We either convert old-format moves to new-format as soon as we receive // them, or we treat them as illegal. const position_prototype = { move: function(s) { // s is some valid UCI move like "d1f3" or "e7e8q". For the most part, this function // assumes the move is legal - all sorts of weird things can happen if this isn't so. // // As an exception, note that position.illegal() does call this to make a temp board // that can be used to test for moves that leave the king in check, so this method // must "work" for such illegal moves. if (typeof s !== "string" || s.length < 4) { console.log("position_prototype.move called with arg", s); return this; } // s = this.c960_castling_converter(s); // Too many ramifications to think about. let [x1, y1] = XY(s.slice(0, 2)); let [x2, y2] = XY(s.slice(2, 4)); if (x1 < 0 || y1 < 0 || x1 > 7 || y1 > 7 || x2 < 0 || y2 < 0 || x2 > 7 || y2 > 7) { console.log("position_prototype.move called with arg", s); return this; } if (this.state[x1][y1] === "") { console.log("position_prototype.move called with empty source, arg was", s); return this; } let ret = this.copy(); let promotion_char = s.length > 4 ? s[4].toLowerCase() : "q"; let white_flag = ret.is_white(Point(x1, y1)); let pawn_flag = ret.state[x1][y1] === "P" || ret.state[x1][y1] === "p"; let castle_flag = (ret.state[x2][y2] === "R" && white_flag) || (ret.state[x2][y2] === "r" && white_flag === false); let capture_flag = castle_flag === false && ret.state[x2][y2]; if (pawn_flag && x1 !== x2) { // Make sure capture_flag is set even for enpassant captures capture_flag = true; } // Update castling info... if (y1 === 7 && ret.state[x1][y1] === "K") { ret.__delete_white_castling(); } if (y1 === 0 && ret.state[x1][y1] === "k") { ret.__delete_black_castling(); } if (y1 === 7 && ret.state[x1][y1] === "R") { // White rook moved. let ch = String.fromCharCode(x1 + 65); ret.__delete_castling_char(ch); } if (y2 === 7 && ret.state[x2][y2] === "R") { // White rook was captured (or castled onto). let ch = String.fromCharCode(x2 + 65); ret.__delete_castling_char(ch); } if (y1 === 0 && ret.state[x1][y1] === "r") { // Black rook moved. let ch = String.fromCharCode(x1 + 97); ret.__delete_castling_char(ch); } if (y2 === 0 && ret.state[x2][y2] === "r") { // Black rook was captured (or castled onto). let ch = String.fromCharCode(x2 + 97); ret.__delete_castling_char(ch); } // Update halfmove and fullmove... if (white_flag === false) { ret.fullmove++; } if (pawn_flag || capture_flag) { ret.halfmove = 0; } else { ret.halfmove++; } // Handle the moves of castling... if (castle_flag) { let k_ch = ret.state[x1][y1]; let r_ch = ret.state[x2][y2]; if (x2 > x1) { // Kingside castling ret.state[x1][y1] = ""; ret.state[x2][y2] = ""; ret.state[6][y1] = k_ch; ret.state[5][y1] = r_ch; } else { // Queenside castling ret.state[x1][y1] = ""; ret.state[x2][y2] = ""; ret.state[2][y1] = k_ch; ret.state[3][y1] = r_ch; } } // Handle enpassant captures... if (pawn_flag && capture_flag && ret.state[x2][y2] === "") { ret.state[x2][y1] = ""; } // Set the enpassant square... only if potential capturing pawns are present. Note // there are some subtleties where the pawns could be present but the capture is // illegal. Therefore, when comparing positions for triple-rep checks, we do some // extra tests, see the compare() method. // // Note that the code below relies on Point() generating null for offboard // coordinates, and ret.piece() accepting that null. ret.enpassant = null; if (pawn_flag && y1 === 6 && y2 === 4) { // White pawn advanced 2 if (ret.piece(Point(x1 - 1, 4)) === "p" || ret.piece(Point(x1 + 1, 4)) === "p") { ret.enpassant = Point(x1, 5); } } if (pawn_flag && y1 === 1 && y2 === 3) { // Black pawn advanced 2 if (ret.piece(Point(x1 - 1, 3)) === "P" || ret.piece(Point(x1 + 1, 3)) === "P") { ret.enpassant = Point(x1, 2); } } // Actually make the move (except we already did castling)... if (castle_flag === false) { ret.state[x2][y2] = ret.state[x1][y1]; ret.state[x1][y1] = ""; } // Handle promotions... if (y2 === 0 && pawn_flag) { ret.state[x2][y2] = promotion_char.toUpperCase(); } if (y2 === 7 && pawn_flag) { ret.state[x2][y2] = promotion_char; // Always lowercase. } // Swap who the current player is... ret.active = white_flag ? "b" : "w"; return ret; }, __delete_castling_char: function(delete_char) { let new_rights = ""; for (let ch of this.castling) { if (ch !== delete_char) { new_rights += ch; } } this.castling = new_rights; }, __delete_white_castling: function() { let new_rights = ""; for (let ch of this.castling) { if ("a" <= ch && ch <= "h") { // i.e. black survives new_rights += ch; } } this.castling = new_rights; }, __delete_black_castling: function() { let new_rights = ""; for (let ch of this.castling) { if ("A" <= ch && ch <= "H") { // i.e. white survives new_rights += ch; } } this.castling = new_rights; }, illegal: function(s) { // Returns "" if the move is legal, otherwise returns the reason it isn't. if (typeof s !== "string") { return "not a string"; } // s = this.c960_castling_converter(s); // Too many ramifications to think about. let [x1, y1] = XY(s.slice(0, 2)); let [x2, y2] = XY(s.slice(2, 4)); if (x1 < 0 || y1 < 0 || x1 > 7 || y1 > 7 || x2 < 0 || y2 < 0 || x2 > 7 || y2 > 7) { return "off board"; } if (this.active === "w" && this.is_white(Point(x1, y1)) === false) { return "wrong colour source"; } if (this.active === "b" && this.is_black(Point(x1, y1)) === false) { return "wrong colour source"; } // Colours must not be the same, except for castling. // Note that king-onto-rook is the only valid castling move... if (this.same_colour(Point(x1, y1), Point(x2, y2))) { if (this.state[x1][y1] === "K" && this.state[x2][y2] === "R") { return this.illegal_castling(x1, y1, x2, y2); } else if (this.state[x1][y1] === "k" && this.state[x2][y2] === "r") { return this.illegal_castling(x1, y1, x2, y2); } else { return "source and destination have same colour"; } } if (["N", "n"].includes(this.state[x1][y1])) { if (Math.abs(x2 - x1) + Math.abs(y2 - y1) !== 3) { return "illegal knight movement"; } if (Math.abs(x2 - x1) === 0 || Math.abs(y2 - y1) === 0) { return "illegal knight movement"; } } if (["B", "b"].includes(this.state[x1][y1])) { if (Math.abs(x2 - x1) !== Math.abs(y2 - y1)) { return "illegal bishop movement"; } } if (["R", "r"].includes(this.state[x1][y1])) { if (Math.abs(x2 - x1) > 0 && Math.abs(y2 - y1) > 0) { return "illegal rook movement"; } } if (["Q", "q"].includes(this.state[x1][y1])) { if (Math.abs(x2 - x1) !== Math.abs(y2 - y1)) { if (Math.abs(x2 - x1) > 0 && Math.abs(y2 - y1) > 0) { return "illegal queen movement"; } } } // Pawns... if (["P", "p"].includes(this.state[x1][y1])) { if (Math.abs(x2 - x1) === 0) { if (this.state[x2][y2]) { return "pawn cannot capture forwards"; } } if (Math.abs(x2 - x1) > 1) { return "pawn cannot move that far sideways"; } if (Math.abs(x2 - x1) === 1) { if (this.state[x2][y2] === "") { if (this.enpassant !== Point(x2, y2)) { return "pawn cannot capture thin air"; } } if (Math.abs(y2 - y1) !== 1) { return "pawn must move 1 forward when capturing"; } } if (this.state[x1][y1] === "P") { if (y1 !== 6) { if (y2 - y1 !== -1) { return "pawn must move forwards 1"; } } else { if (y2 - y1 !== -1 && y2 - y1 !== -2) { return "pawn must move forwards 1 or 2"; } } } if (this.state[x1][y1] === "p") { if (y1 !== 1) { if (y2 - y1 !== 1) { return "pawn must move forwards 1"; } } else { if (y2 - y1 !== 1 && y2 - y1 !== 2) { return "pawn must move forwards 1 or 2"; } } } } // Kings... if (["K", "k"].includes(this.state[x1][y1])) { if (Math.abs(y2 - y1) > 1) { return "illegal king movement"; } if (Math.abs(x2 - x1) > 1) { return "illegal king movement"; } } // Check for blockers (pieces between source and dest). if (["K", "Q", "R", "B", "P", "k", "q", "r", "b", "p"].includes(this.state[x1][y1])) { if (this.los(x1, y1, x2, y2) === false) { return "movement blocked"; } } // Check promotion and string lengths... // We DO NOT tolerate missing promotion characters. if ((y1 === 1 && this.state[x1][y1] === "P") || (y1 === 6 && this.state[x1][y1] === "p")) { if (s.length !== 5) { return "bad string length"; } let promotion = s[4]; if (promotion !== "q" && promotion !== "r" && promotion !== "b" && promotion !== "n") { return "move requires a valid promotion piece"; } } else { if (s.length !== 4) { return "bad string length"; } } // Check for check... let tmp = this.move(s); if (tmp.can_capture_king()) { return "king in check"; } return ""; }, illegal_castling: function(x1, y1, x2, y2) { // We can assume a king is on [x1, y1] and a same-colour rook is on [x2, y2] if (y1 !== y2) { return "cannot castle vertically"; } let colour = this.colour(Point(x1, y1)); if (colour === "w" && y1 !== 7) { return "cannot castle off the back rank"; } if (colour === "b" && y1 !== 0) { return "cannot castle off the back rank"; } // Check for the required castling rights character... let required_ch; if (colour === "w") { required_ch = Point(x2, y2).s[0].toUpperCase(); } else { required_ch = Point(x2, y2).s[0]; } if (this.castling.includes(required_ch) === false) { return `lost the right to castle - needed ${required_ch}`; } let king_target_x; let rook_target_x; if (x1 < x2) { // Castling kingside king_target_x = 6; rook_target_x = 5; } else { // Castling queenside king_target_x = 2; rook_target_x = 3; } let king_path = NumbersBetween(x1, king_target_x); let rook_path = NumbersBetween(x2, rook_target_x); // Check for blockers and checks... for (let x of king_path) { if (this.attacked(Point(x, y1), this.active)) { return "cannot castle [out of / through / into] check"; } if (x === x1 || x === x2) { continue; // After checking for checks } if (this.state[x][y1]) { return "castling blocked for king movement"; } } for (let x of rook_path) { if (x === x1 || x === x2) { continue; } if (this.state[x][y1]) { return "castling blocked for rook movement"; } } // Check that the king doesn't end up in check anyway... // q1nnkbbr/p1pppppp/8/1P6/8/3NN3/1PPPPPPP/rR2KBBR w BHh - 0 5 let tmp = this.move(Point(x1, y1).s + Point(x2, y2).s); if (tmp.attacked(Point(king_target_x, y1), this.active)) { return "king ends in check"; } return ""; }, sequence_illegal: function(moves) { let pos = this; for (let s of moves) { let reason = pos.illegal(s); if (reason) { return `${s} - ${reason}`; } pos = pos.move(s); } return ""; }, can_capture_king: function() { // Can the side to move capture the opponent's king? Helper function for illegal() etc. // But this is slow, do not use when king location is known - just call attacked() instead. let kch = this.active === "w" ? "k" : "K"; // i.e. the INACTIVE king let opp_colour = this.active === "w" ? "b" : "w"; for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { if (this.state[x][y] === kch) { return this.attacked(Point(x, y), opp_colour); } } } return false; // King not actually present... }, king_in_check: function() { // Don't call this if the king position is already // known since this method uses an expensive find(). let kch = this.active === "w" ? "K" : "k"; let king_loc = this.find(kch)[0]; if (king_loc === undefined) { return false; } return this.attacked(king_loc, this.active); }, los: function(x1, y1, x2, y2) { // Returns false if there is no "line of sight" between the 2 points. // Check the line is straight.... if (Math.abs(x2 - x1) > 0 && Math.abs(y2 - y1) > 0) { if (Math.abs(x2 - x1) !== Math.abs(y2 - y1)) { return false; } } let step_x; let step_y; if (x1 === x2) step_x = 0; if (x1 < x2) step_x = 1; if (x1 > x2) step_x = -1; if (y1 === y2) step_y = 0; if (y1 < y2) step_y = 1; if (y1 > y2) step_y = -1; let x = x1; let y = y1; while (true) { x += step_x; y += step_y; if (x === x2 && y === y2) { return true; } if (this.state[x][y]) { return false; } } }, attacked: function(target, my_colour) { if (!my_colour) { throw "attacked(): no colour given"; } if (!target) { // Because it was null from Point(foo) perhaps. return false; } // Attacks along the lines... for (let step_x = -1; step_x <= 1; step_x++) { for (let step_y = -1; step_y <= 1; step_y++) { if (step_x === 0 && step_y === 0) continue; if (this.line_attack(target, step_x, step_y, my_colour)) { return true; } } } // Knights... for (let d of [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]]) { let x = target.x + d[0]; let y = target.y + d[1]; if (x < 0 || x > 7 || y < 0 || y > 7) continue; if (["N", "n"].includes(this.state[x][y])) { if (this.colour(Point(x, y)) === my_colour) continue; return true; } } return false; }, line_attack: function(target, step_x, step_y, my_colour) { // Is the target square under attack via the line specified by step_x and step_y (which are both -1, 0, or 1) ? if (!my_colour) { throw "line_attack(): no colour given"; } if (!target) { // Because it was null from Point(foo) perhaps. return false; } if (step_x === 0 && step_y === 0) { return false; } let x = target.x; let y = target.y; let ranged_attackers = ["Q", "q", "R", "r"]; // Ranged attackers that can go in a cardinal direction. if (step_x !== 0 && step_y !== 0) { ranged_attackers = ["Q", "q", "B", "b"]; // Ranged attackers that can go in a diagonal direction. } let iteration = 0; while (true) { iteration++; x += step_x; y += step_y; if (x < 0 || x > 7 || y < 0 || y > 7) { return false; } if (this.state[x][y] === "") { continue; } // So there's something here. Must return now. if (this.colour(Point(x, y)) === my_colour) { return false; } // We now know the piece is hostile. This allows us to mostly not care // about distinctions between "Q" and "q", "R" and "r", etc. // Is it one of the ranged attacker types? if (ranged_attackers.includes(this.state[x][y])) { return true; } // Pawns and kings are special cases (attacking iff it's the first iteration) if (iteration === 1) { if (["K", "k"].includes(this.state[x][y])) { return true; } if (Math.abs(step_x) === 1) { if (this.state[x][y] === "p" && step_y === -1) { // Black pawn in attacking position return true; } if (this.state[x][y] === "P" && step_y === 1) { // White pawn in attacking position return true; } } } return false; } }, find: function(piece, startx, starty, endx, endy) { // Find all pieces of the specified type (colour-specific). // Search range is INCLUSIVE. Result returned as a list of points. // You can call this function with just a piece to search the whole board. if (startx === undefined) startx = 0; if (starty === undefined) starty = 0; if (endx === undefined) endx = 7; if (endy === undefined) endy = 7; // Calling with out of bounds args should also work... if (startx < 0) startx = 0; if (startx > 7) startx = 7; if (starty < 0) starty = 0; if (starty > 7) starty = 7; if (endx < 0) endx = 0; if (endx > 7) endx = 7; if (endy < 0) endy = 0; if (endy > 7) endy = 7; let ret = []; for (let x = startx; x <= endx; x++) { for (let y = starty; y <= endy; y++) { if (this.state[x][y] === piece) { ret.push(Point(x, y)); } } } return ret; }, find_castling_move: function(long_flag) { // Returns a (possibly illegal) castling move (e.g. "e1h1") or "" let king_loc; if (this.active === "w") { king_loc = this.find("K", 0, 7, 7, 7)[0]; } else { king_loc = this.find("k", 0, 0, 7, 0)[0]; } if (king_loc === undefined) { return ""; } let possible_rights_chars; if (this.active === "w") { possible_rights_chars = ["A", "B", "C", "D", "E", "F", "G", "H"]; } else { possible_rights_chars = ["a", "b", "c", "d", "e", "f", "g", "h"]; } if (long_flag) { possible_rights_chars = possible_rights_chars.slice(0, king_loc.x); possible_rights_chars.reverse(); // So we propose the shortest move first, if more than 1 is allowed by the rights. } else { possible_rights_chars = possible_rights_chars.slice(king_loc.x + 1); } for (let ch of possible_rights_chars) { if (this.castling.includes(ch)) { if (this.active === "w") { return king_loc.s + ch.toLowerCase() + "1"; } else { return king_loc.s + ch + "8"; } } } return ""; }, parse_pgn: function(s) { // Returns a UCI move and an error message. // Replace fruity dash characters with proper ASCII dash "-" for (let n of [8208, 8210, 8211, 8212, 8213, 8722]) { s = ReplaceAll(s, String.fromCodePoint(n), "-"); } // If the string contains any dots it'll be something like "1.e4" or "...e4" or whatnot... let lio = s.lastIndexOf("."); if (lio !== -1) { s = s.slice(lio + 1); } // At this point, if s is actually a UCI string (which it won't be in real PGN) we can return it. // This is a hack to allow pasting of stuff from non-PGN sources I guess... if (s.length === 4 || (s.length === 5 && ["q", "r", "b", "n"].includes(s[4]))) { if (s[0] >= "a" && s[0] <= "h" && s[1] >= "1" && s[1] <= "8" && s[2] >= "a" && s[2] <= "h" && s[3] >= "1" && s[3] <= "8" ) { let tmp = this.c960_castling_converter(s); if (!this.illegal(tmp)) { return [tmp, ""]; } } } // Delete things we don't need... s = ReplaceAll(s, "x", ""); s = ReplaceAll(s, "+", ""); s = ReplaceAll(s, "#", ""); s = ReplaceAll(s, "!", ""); s = ReplaceAll(s, "?", ""); // Fix castling with zeroes... s = ReplaceAll(s, "0-0-0", "O-O-O"); s = ReplaceAll(s, "0-0", "O-O"); if (s.toUpperCase() === "O-O") { let mv = this.find_castling_move(false); if (mv && !this.illegal(mv)) { return [mv, ""]; } else { return ["", "illegal castling"]; } } if (s.toUpperCase() === "O-O-O") { let mv = this.find_castling_move(true); if (mv && !this.illegal(mv)) { return [mv, ""]; } else { return ["", "illegal castling"]; } } // Just in case, delete any "-" characters (after handling castling, of course)... s = ReplaceAll(s, "-", ""); // If an = sign is present, save promotion string, then delete it from s... let promotion = ""; if (s[s.length - 2] === "=") { promotion = s[s.length - 1].toLowerCase(); s = s.slice(0, -2); } // A lax writer might also write the promotion string without an equals sign... if (promotion === "") { if (["Q", "R", "B", "N", "q", "r", "b", "n"].includes(s[s.length - 1])) { promotion = s[s.length - 1].toLowerCase(); s = s.slice(0, -1); } } // If the piece isn't specified (with an uppercase letter) then it's a pawn move. // Let's add P to the start of the string to keep the string format consistent... if (["K", "Q", "R", "B", "N", "P"].includes(s[0]) === false) { s = "P" + s; } // Now this works... let piece = s[0]; // We care about the colour of the piece, so make black pieces lowercase... if (this.active === "b") { piece = piece.toLowerCase(); } // The last 2 characters specify the target point. We've removed all trailing // garbage that could interfere with this fact. let dest = Point(s.slice(s.length - 2, s.length)); if (!dest) { return ["", "invalid destination"]; } // Any characters between the piece and target should be disambiguators... let disambig = s.slice(1, -2); let startx = 0; let endx = 7; let starty = 0; let endy = 7; for (let c of disambig) { if (c >= "a" && c <= "h") { startx = c.charCodeAt(0) - 97; endx = startx; } if (c >= "1" && c <= "8") { starty = 7 - (c.charCodeAt(0) - 49); endy = starty; } } // If it's a pawn and hasn't been disambiguated then it is moving forwards... if (piece === "P" || piece === "p") { if (disambig.length === 0) { startx = dest.x; endx = dest.x; } } let sources = this.find(piece, startx, starty, endx, endy); if (sources.length === 0) { return ["", "piece not found"]; } let possible_moves = []; for (let source of sources) { possible_moves.push(source.s + dest.s + promotion); } let valid_moves = []; for (let move of possible_moves) { if (this.illegal(move) === "") { valid_moves.push(move); } } if (valid_moves.length === 1) { return [valid_moves[0], ""]; } if (valid_moves.length === 0) { return ["", "piece found but move illegal"]; } if (valid_moves.length > 1) { return ["", `ambiguous moves: [${valid_moves}]`]; } }, piece: function(point) { if (!point) return ""; return this.state[point.x][point.y]; }, is_white: function(point) { let piece = this.piece(point); return ["K", "Q", "R", "B", "N", "P"].includes(piece); // Can't do "KQRBNP".includes() as that catches "". }, is_black: function(point) { let piece = this.piece(point); return ["k", "q", "r", "b", "n", "p"].includes(piece); // Can't do "kqrbnp".includes() as that catches "". }, is_empty: function(point) { return this.piece(point) === ""; }, colour: function(point) { let piece = this.piece(point); if (piece === "") { return ""; } if (["K", "Q", "R", "B", "N", "P"].includes(piece)) { return "w"; } return "b"; }, same_colour: function(point1, point2) { return this.colour(point1) === this.colour(point2); }, movegen: function(one_only = false) { let moves = []; for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { let source = Point(x, y); if (this.colour(source) !== this.active) { continue; } let piece = this.state[x][y]; if (piece !== "K" && piece !== "k") { // We don't include kings because castling is troublesome. for (let slider of movegen_sliders[piece]) { // The sliders are lists where, if one move is blocked, every subsequent move in the slider is also // blocked. Note that the test is "blocked / offboard". The test is not "is illegal" - sometimes one // move will be illegal but a move further down the slider will be legal - e.g. if it blocks a check. for (let [dx, dy] of slider) { let x2 = x + dx; let y2 = y + dy; if (x2 < 0 || x2 > 7 || y2 < 0 || y2 > 7) { // No move further along the slider will be legal. break; } let dest = Point(x2, y2); let dest_colour = this.colour(dest); if (dest_colour === this.active) { // No move further along the slider will be legal. break; } let move = source.s + dest.s; if ((piece === "P" && dest.y === 0) || (piece === "p" && dest.y === 7)) { if (this.illegal(move + "q") === "") { moves.push(move + "q"); if (one_only) { return moves; } moves.push(move + "r"); moves.push(move + "b"); moves.push(move + "n"); } } else { if (this.illegal(move) === "") { moves.push(move); if (one_only) { return moves; } } } if (dest_colour !== "") { // No move further along the slider will be legal. break; } } } } else { // King moves that involve vertical direction... for (let dx of [-1, 0, 1]) { for (let dy of [-1, 1]) { let x2 = x + dx; let y2 = y + dy; if (x2 < 0 || x2 > 7 || y2 < 0 || y2 > 7) { continue; } let dest = Point(x2, y2); let move = source.s + dest.s; if (this.illegal(move) === "") { moves.push(move); if (one_only) { return moves; } } } } // Horizontal king moves (including castling moves)... for (let x2 = 0; x2 < 8; x2++) { let dest = Point(x2, y); let move = source.s + dest.s; if (this.illegal(move) === "") { moves.push(move); if (one_only) { return moves; } } } } } } return moves; }, nice_movegen: function() { return this.movegen().map(s => this.nice_string(s)); }, no_moves: function() { return this.movegen(true).length === 0; }, c960_castling_converter: function(s) { // Given some move s, convert it to the new Chess 960 castling format if needed. if (s === "e1g1" && this.state[4][7] === "K" && this.castling.includes("G") === false) return "e1h1"; if (s === "e1c1" && this.state[4][7] === "K" && this.castling.includes("C") === false) return "e1a1"; if (s === "e8g8" && this.state[4][0] === "k" && this.castling.includes("g") === false) return "e8h8"; if (s === "e8c8" && this.state[4][0] === "k" && this.castling.includes("c") === false) return "e8a8"; return s; }, nice_string: function(s) { // Given some raw (but valid) UCI move string, return a nice human-readable // string for display in the browser window. This string should never be // examined by the caller, merely displayed. // // Note that as of 1.1.6, all castling moves are expected to be king-onto-rook, // that is, Chess960 format. // s = this.c960_castling_converter(s); // Too many ramifications to think about. let source = Point(s.slice(0, 2)); let dest = Point(s.slice(2, 4)); if (!source || !dest) { return "??"; } let piece = this.piece(source); if (piece === "") { return "??"; } let check = ""; let next_board = this.move(s); let opponent_king_char = this.active === "w" ? "k" : "K"; let opponent_king_square = this.find(opponent_king_char)[0]; // Might be undefined on corrupt board... if (opponent_king_square && next_board.attacked(opponent_king_square, next_board.colour(opponent_king_square))) { if (next_board.no_moves()) { check = "#"; } else { check = "+"; } } if (["K", "k", "Q", "q", "R", "r", "B", "b", "N", "n"].includes(piece)) { if (["K", "k"].includes(piece)) { if (this.colour(dest) === this.colour(source)) { if (dest.x > source.x) { return `O-O${check}`; } else { return `O-O-O${check}`; } } } // Would the move be ambiguous? // IMPORTANT: note that the actual move will not necessarily be valid_moves[0]. let possible_sources = this.find(piece); let possible_moves = []; let valid_moves = []; for (let foo of possible_sources) { possible_moves.push(foo.s + dest.s); // e.g. "g1f3" - note we are only dealing with pieces, so no worries about promotion } for (let move of possible_moves) { if (this.illegal(move) === "") { valid_moves.push(move); } } if (valid_moves.length > 2) { // Full disambiguation. if (this.piece(dest) === "") { return piece.toUpperCase() + source.s + dest.s + check; } else { return piece.toUpperCase() + source.s + "x" + dest.s + check; } } if (valid_moves.length === 2) { // Partial disambiguation. let source1 = Point(valid_moves[0].slice(0, 2)); let source2 = Point(valid_moves[1].slice(0, 2)); let disambiguator; if (source1.x === source2.x) { disambiguator = source.s[1]; // Note source (the true source), not source1 } else { disambiguator = source.s[0]; // Note source (the true source), not source1 } if (this.piece(dest) === "") { return piece.toUpperCase() + disambiguator + dest.s + check; } else { return piece.toUpperCase() + disambiguator + "x" + dest.s + check; } } // No disambiguation. if (this.piece(dest) === "") { return piece.toUpperCase() + dest.s + check; } else { return piece.toUpperCase() + "x" + dest.s + check; } } // So it's a pawn. Pawn moves are never ambiguous. let ret; if (source.x === dest.x) { ret = dest.s; } else { ret = source.s[0] + "x" + dest.s; } if (s.length > 4) { ret += "="; ret += s[4].toUpperCase(); } ret += check; return ret; }, next_number_string: function() { if (this.active === "w") { return `${this.fullmove}.`; } else { return `${this.fullmove}...`; } }, fen: function(friendly_flag, book_flag) { // friendly_flag - for when the engine isn't the consumer. // book_flag - for when we should omit the move numbers. let s = ""; for (let y = 0; y < 8; y++) { let x = 0; let blanks = 0; while (true) { if (this.state[x][y] === "") { blanks++; } else { if (blanks > 0) { s += blanks.toString(); blanks = 0; } s += this.state[x][y]; } x++; if (x >= 8) { if (blanks > 0) { s += blanks.toString(); } if (y < 7) { s += "/"; } break; } } } let ep_string = this.enpassant ? this.enpassant.s : "-"; let castling_string = this.castling !== "" ? this.castling : "-"; // While internally we always use Chess960 format, we can return a more friendly // FEN if asked (and if the position is normal Chess). Relies on our normalchess // flag being accurate... (potential for bugs there). if (friendly_flag && this.normalchess && castling_string !== "-") { let new_castling_string = ""; if (castling_string.includes("H")) new_castling_string += "K"; if (castling_string.includes("A")) new_castling_string += "Q"; if (castling_string.includes("h")) new_castling_string += "k"; if (castling_string.includes("a")) new_castling_string += "q"; castling_string = new_castling_string; } if (book_flag) { return s + ` ${this.active} ${castling_string} ${ep_string}`; } else { return s + ` ${this.active} ${castling_string} ${ep_string} ${this.halfmove} ${this.fullmove}`; } }, insufficient_material: function() { // There are some subtleties around help-mates and also positions where // mate is forced despite there not being enough material if the pieces // were elsewhere. This code below should have no false positives... let minors = 0; for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { switch (this.state[x][y]) { case "Q": case "q": case "R": case "r": case "P": case "p": return false; case "B": case "b": case "N": case "n": minors++; if (minors >= 2) { return false; } } } } return true; }, graphic: function() { let units = []; for (let y = 0; y < 8; y++) { units.push("\n"); for (let x = 0; x < 8; x++) { units.push(this.state[x][y] === "" ? "." : this.state[x][y]); if (x < 7) { units.push(" "); } } if (y === 7) { units.push(" "); units.push(this.fen(false)); } } units.push("\n"); return units.join(""); }, has_legal_ep_capture: function() { if (!this.enpassant) { return false; } // Find the one or two pawns of the correct colour and location that could do the e.p. capture. // Check whether these moves are actually legal. If either of them is, return true. let ep = this.enpassant; let pawn_char; let source_y; if (this.active === "w") { pawn_char = "P"; source_y = ep.y + 1; } else { pawn_char = "p"; source_y = ep.y - 1; } for (let dx = -1; dx <= 1; dx += 2) { let source = Point(ep.x + dx, source_y); if (!source) { continue; } if (this.piece(source) !== pawn_char) { continue; } if (!this.illegal(source.s + ep.s)) { return true; } } return false; }, compare: function(other, strict = false) { if (this.active !== other.active) { return false; } if (this.castling !== other.castling) { return false; } for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { if (this.state[x][y] !== other.state[x][y]) { return false; } } } if (strict) { let this_real_ep = this.has_legal_ep_capture() ? this.enpassant : null; let other_real_ep = other.has_legal_ep_capture() ? other.enpassant : null; if (this_real_ep !== other_real_ep) { return false; } } else { if (this.enpassant !== other.enpassant) { return false; } } return true; }, copy: function() { return NewPosition(this.state, this.active, this.castling, this.enpassant, this.halfmove, this.fullmove, this.normalchess); }, }; function NewPosition(state = null, active = "w", castling = "", enpassant = null, halfmove = 0, fullmove = 1, normalchess = false) { let p = Object.create(position_prototype); p.state = [ ["","","","","","","",""], ["","","","","","","",""], ["","","","","","","",""], ["","","","","","","",""], ["","","","","","","",""], ["","","","","","","",""], ["","","","","","","",""], ["","","","","","","",""], ]; if (state) { for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { let piece = state[x][y]; if (piece) { p.state[x][y] = piece; } } } } p.active = active; p.castling = castling; p.enpassant = enpassant; p.halfmove = halfmove; p.fullmove = fullmove; p.normalchess = normalchess; return p; } ================================================ FILE: files/src/renderer/41_fen.js ================================================ "use strict"; function LoadFEN(fen) { if (fen.length > 200) { throw "Invalid FEN - size"; } let ret = NewPosition(); fen = ReplaceAll(fen, "\t", " "); fen = ReplaceAll(fen, "\n", " "); fen = ReplaceAll(fen, "\r", " "); let tokens = fen.split(" ").filter(z => z !== ""); if (tokens.length === 1) tokens.push("w"); if (tokens.length === 2) tokens.push("-"); if (tokens.length === 3) tokens.push("-"); if (tokens.length === 4) tokens.push("0"); if (tokens.length === 5) tokens.push("1"); if (tokens.length !== 6) { throw "Invalid FEN - token count"; } if (tokens[0].endsWith("/")) { // Some FEN writer does this tokens[0] = tokens[0].slice(0, -1); } let rows = tokens[0].split("/"); if (rows.length > 8) { throw "Invalid FEN - board row count"; } for (let y = 0; y < rows.length; y++) { let x = 0; for (let c of rows[y]) { if (x > 7) { throw "Invalid FEN - row length"; } if (["1", "2", "3", "4", "5", "6", "7", "8"].includes(c)) { x += parseInt(c, 10); continue; } if (["K", "k", "Q", "q", "R", "r", "B", "b", "N", "n", "P", "p"].includes(c)) { ret.state[x][y] = c; x++; continue; } throw "Invalid FEN - unknown piece"; } } tokens[1] = tokens[1].toLowerCase(); if (tokens[1] !== "w" && tokens[1] !== "b") { throw "Invalid FEN - active player"; } ret.active = tokens[1]; ret.halfmove = parseInt(tokens[4], 10); if (Number.isNaN(ret.halfmove)) { throw "Invalid FEN - halfmoves"; } ret.fullmove = parseInt(tokens[5], 10); if (Number.isNaN(ret.fullmove)) { throw "Invalid FEN - fullmoves"; } // Some more validity checks... let white_kings = 0; let black_kings = 0; for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { if (ret.state[x][y] === "K") white_kings++; if (ret.state[x][y] === "k") black_kings++; } } if (white_kings !== 1 || black_kings !== 1) { throw "Invalid FEN - number of kings"; } for (let x = 0; x < 8; x++) { for (let y of [0, 7]) { if (ret.state[x][y] === "P" || ret.state[x][y] === "p") { throw "Invalid FEN - pawn position"; } } } let opponent_king_char = ret.active === "w" ? "k" : "K"; let opponent_king_square = ret.find(opponent_king_char)[0]; if (ret.attacked(opponent_king_square, ret.colour(opponent_king_square))) { throw "Invalid FEN - non-mover's king in check"; } // Some hard things. Do these in the right order! ret.castling = CastlingRights(ret, tokens[2]); ret.enpassant = EnPassantSquare(ret, tokens[3]); // Requires ret.active to be correct. ret.normalchess = IsNormalChessPosition(ret); // Requires ret.castling to be correct. return ret; } function CastlingRights(board, s) { // s is the castling string from a FEN let dict = Object.create(null); // Will contain keys like "A" to "H" and "a" to "h" // WHITE let wk_location = board.find("K", 0, 7, 7, 7)[0]; // Will be undefined if not on back rank. if (wk_location) { for (let ch of s) { if (["A", "B", "C", "D", "E", "F", "G", "H"].includes(ch)) { let point = Point(ch.toLowerCase() + "1"); if (board.piece(point) === "R") { dict[ch] = true; } } if (ch === "Q") { if (board.state[0][7] === "R") { // Compatibility with regular Chess FEN. dict.A = true; } else { let left_rooks = board.find("R", 0, 7, wk_location.x, 7); for (let rook of left_rooks) { dict[rook.s[0].toUpperCase()] = true; } } } if (ch === "K") { if (board.state[7][7] === "R") { // Compatibility with regular Chess FEN. dict.H = true; } else { let right_rooks = board.find("R", wk_location.x, 7, 7, 7); for (let rook of right_rooks) { dict[rook.s[0].toUpperCase()] = true; } } } } } // BLACK let bk_location = board.find("k", 0, 0, 7, 0)[0]; if (bk_location) { for (let ch of s) { if (["a", "b", "c", "d", "e", "f", "g", "h"].includes(ch)) { let point = Point(ch + "8"); if (board.piece(point) === "r") { dict[ch] = true; } } if (ch === "q") { if (board.state[0][0] === "r") { // Compatibility with regular Chess FEN. dict.a = true; } else { let left_rooks = board.find("r", 0, 0, bk_location.x, 0); for (let rook of left_rooks) { dict[rook.s[0]] = true; } } } if (ch === "k") { if (board.state[7][0] === "r") { // Compatibility with regular Chess FEN. dict.h = true; } else { let right_rooks = board.find("r", bk_location.x, 0, 7, 0); for (let rook of right_rooks) { dict[rook.s[0]] = true; } } } } } let ret = ""; for (let ch of "ABCDEFGHabcdefgh") { if (dict[ch]) { ret += ch; } } return ret; // FIXME: check at most 1 castling possibility on left and right of each king? // At the moment we support more arbitrary castling rights, maybe that's OK. } function EnPassantSquare(board, s) { // board.active must be correct. s is the en-passant string from a FEN. // Suffers from the same subtleties as the enpassant setter in position.move(), see there for comments. let p = Point(s.toLowerCase()); if (!p) { return null; } if (board.active === "w") { if (p.y !== 2) { return null; } // Check the takeable pawn exists... if (board.piece(Point(p.x, 3)) !== "p") { return null; } // Check the capture square is actually empty... if (board.piece(Point(p.x, 2)) !== "") { return null; } // Check potential capturer exists... if (board.piece(Point(p.x - 1, 3)) !== "P" && board.piece(Point(p.x + 1, 3)) !== "P") { return null; } return p; } if (board.active === "b") { if (p.y !== 5) { return null; } // Check the takeable pawn exists... if (board.piece(Point(p.x, 4)) !== "P") { return null; } // Check the capture square is actually empty... if (board.piece(Point(p.x, 5)) !== "") { return null; } // Check potential capturer exists... if (board.piece(Point(p.x - 1, 4)) !== "p" && board.piece(Point(p.x + 1, 4)) !== "p") { return null; } return p; } return null; } function IsNormalChessPosition(board) { for (let ch of "bcdefgBCDEFG") { if (board.castling.includes(ch)) { return false; } } if (board.castling.includes("A") || board.castling.includes("H")) { if (board.state[4][7] !== "K") { return false; } } if (board.castling.includes("a") || board.castling.includes("h")) { if (board.state[4][0] !== "k") { return false; } } // So it can be considered a normal Chess position. return true; } ================================================ FILE: files/src/renderer/42_perft.js ================================================ "use strict"; /* Perft notes: The correct perft value for a position and depth is the number of leaf nodes at that depth (or equivalently, the number of legal move sequences of that length). Some important points: - Rules about "Triple Repetition" and "Insufficient Material" are ignored. - Terminal nodes (mates) at a shallower depth are not counted. - But they are counted if they are at the correct depth. In Stockfish: setoption name UCI_Chess960 value true position fen go perft 4 */ function perft(pos, depth, print_moves) { let moves = pos.movegen(); if (depth === 1) { return moves.length; } else { let count = 0; for (let mv of moves) { let val = perft(pos.move(mv), depth - 1, false); if (print_moves) { perft_print_move(pos, mv, val); } count += val; } return count; } } function perft_print_move(pos, mv, val) { let nice = pos.nice_string(mv); console.log(`${mv + (mv.length === 4 ? " " : "")} ${nice + " ".repeat(7 - nice.length)}`, val); } // ------------------------------------------------------------------------------------------------------------------- let perft_known_values = { "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1": [0, 14, 191, 2812, 43238, 674624], "1nr1nk1r/1b5B/p1p1qp2/b2pp1pP/3P2P1/P3P2N/1Pp2P2/BNR2KQR w CHch g6 0 1": [0, 28, 964, 27838, 992438, 30218648], "Qr3knr/P1bp1p1p/2pn1q2/4p3/2PP2pB/1p1N1bP1/BP2PP1P/1R3KNR w BHbh - 0 1": [0, 31, 1122, 34613, 1253934, 40393041], "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1": [0, 48, 2039, 97862, 4085603, 193690690], "r3k2r/1P1pp1P1/8/2P2P2/2p2p2/8/1p1PP1p1/R3K2R w KQkq - 0 1": [0, 43, 1286, 39109, 1134150, 33406158], }; function Perft(fen, depth) { if (!fen || !depth) throw "Need FEN and depth"; let starttime = performance.now(); let board = LoadFEN(fen); let val = perft(board, depth, true); console.log(`Total.......... ${val} (${((performance.now() - starttime) / 1000).toFixed(1)} seconds)`); if (perft_known_values[fen] && perft_known_values[fen][depth]) { if (perft_known_values[fen][depth] === val) { console.log("Known good result"); } else { console.log(`Known BAD result -- expected ${perft_known_values[fen][depth]}`); } } return val; } function PerftFileTest(filename, depth, verbose = false) { if (!filename || !depth) throw "Need filename and depth"; let starttime = performance.now(); let contents = fs.readFileSync(filename).toString(); let lines = contents.split("\n").map(z => z.trim()).filter(z => z !== ""); for (let n = 0; n < lines.length; n++) { let blobs = lines[n].split(";"); let result = perft(LoadFEN(blobs[0]), depth, false); if (lines[n].includes(result.toString())) { if (verbose) { console.log(`ok -- ${n + 1} / ${lines.length} -- ${blobs[0]}`); } } else { console.log(`FAILED -- ${n + 1} / ${lines.length} -- ${blobs[0]}`); } } console.log(`Elapsed: (${((performance.now() - starttime) / 1000).toFixed(1)} seconds)`); } ================================================ FILE: files/src/renderer/43_chess960.js ================================================ "use strict"; function c960_arrangement(n) { // Given n, generate a string like "RNBQKBNR". // AFAIK, matches the scheme of Reinhard Scharnagl. if (n < 0) { n *= -1; } n = Math.floor(n) % 960; let pieces = [".", ".", ".", ".", ".", ".", ".", "."]; // Helper function to place a piece at an "index", // but considering only empty spots. let insert = (i, piece) => { for (let n = 0; n < 8; n++) { if (pieces[n] === "." && --i < 0) { // Careful! Remember short-circuit rules etc. pieces[n] = piece; return; } } }; // Place bishops in final positions... pieces[(Math.floor(n / 4) % 4) * 2] = "B"; pieces[(n % 4) * 2 + 1] = "B"; // Place queen in one of 6 remaining spots... let qi = Math.floor(n / 16) % 6; insert(qi, "Q"); // Knights are arranged in one of 10 possible configurations // (considering only the remaining spots)... let ni1 = [0, 0, 0, 0, 1, 1, 1, 2, 2, 3][Math.floor(n / 96)]; let ni2 = [1, 2, 3, 4, 2, 3, 4, 3, 4, 4][Math.floor(n / 96)]; insert(ni2, "N"); // Must be done in this order, insert(ni1, "N"); // works because ni2 > ni1 // Place left rook, king, right rook in first available spots... insert(0, "R"); insert(0, "K"); insert(0, "R"); return pieces.join(""); } function c960_fen(n) { // Given n, produce a full FEN. let pieces = c960_arrangement(n); // The uppercase version. let s = `${pieces.toLowerCase()}/pppppppp/8/8/8/8/PPPPPPPP/${pieces}`; let castling_rights = ""; for (let i = 0; i < 8; i++) { if (pieces[i] === "R") { castling_rights += String.fromCharCode(i + 65); } } castling_rights += castling_rights.toLowerCase(); return `${s} w ${castling_rights} - 0 1`; } ================================================ FILE: files/src/renderer/50_table.js ================================================ "use strict"; // The table object stores info from the engine about a game-tree (PGN) node. function NewTable() { let table = Object.create(table_prototype); table.clear(); return table; } const table_prototype = { clear: function() { this.moveinfo = Object.create(null); // move --> info this.version = 0; // Incremented on any change this.nodes = 0; // Stat sent by engine this.nps = 0; // Stat sent by engine this.tbhits = 0; // Stat sent by engine this.time = 0; // Stat sent by engine this.limit = null; // The limit of the last search that updated this. this.terminal = null; // null = unknown, "" = not terminal, "Non-empty string" = terminal reason this.graph_y = null; // Used by grapher only, value from White's POV between 0 and 1 this.graph_y_version = 0; // Which version (above) was used to generate the graph_y value this.already_autopopulated = false; }, get_graph_y: function() { // Naphthalin's scheme: based on centipawns. if (this.graph_y_version === this.version) { return this.graph_y; } else { let info = SortedMoveInfoFromTable(this)[0]; if (info && !info.__ghost && info.__touched && (this.nodes > 1 || this.limit === 1)) { let cp = info.cp; if (info.board.active === "b") { cp *= -1; } this.graph_y = 1 / (1 + Math.pow(0.5, cp / 100)); } else { this.graph_y = null; } this.graph_y_version = this.version; return this.graph_y; } }, set_terminal_info: function(reason, ev) { // ev is ignored if reason is "" (i.e. not a terminal position) if (reason) { this.terminal = reason; this.graph_y = ev; this.graph_y_version = this.version; } else { this.terminal = ""; } }, autopopulate: function(node) { if (!node) { throw "autopopulate() requires node argument"; } if (this.already_autopopulated) { return; } if (node.destroyed) { return; } let moves = node.board.movegen(); for (let move of moves) { if (node.table.moveinfo[move] === undefined) { node.table.moveinfo[move] = NewInfo(node.board, move); } } this.already_autopopulated = true; } }; // -------------------------------------------------------------------------------------------- // The info object stores info received from the engine about a move. The actual updating of // the object takes place in info.js and the ih.receive() method there. function NewInfo(board, move) { let info = Object.create(info_prototype); info.board = board; info.move = move; info.__ghost = false; // If not false, this is temporary inferred info. info.__touched = false; // Has this ever actually been updated? info.leelaish = false; // Whether the most recent update to this info was from an engine considered Leelaish. info.pv = [move]; // Validated as a legal sequence upon reception. info.cycle = 0; // How many "go" commands Nibbler has emitted. info.subcycle = 0; // How many "blocks" of info we have seen (delineated by multipv 1 info). info.nice_pv_cache = [board.nice_string(move)]; info.clear_stats(); return info; } const info_prototype = { // I'm not sure I've been conscientious everywhere in the code about checking whether these things are // of the right type, so for that reason most are set to some neutralish value by default. // // Exceptions: m, v, wdl (and note that all of these can be set to null by info.js) clear_stats: function() { this.cp = 0; this.depth = 0; this.m = null; this.mate = 0; // 0 can be the "not present" value. this.multipv = 1; this.n = 0; this.p = 0; // Note P is received and stored as a percent, e.g. 31.76 is a reasonable P. this.q = 0; this.s = 1; // Known as Q+U before Lc0 v0.25-rc2 this.seldepth = 0; this.u = 1; this.uci_nodes = 0; // The number of nodes reported by the UCI info lines (i.e. for the whole position). this.v = null; this.vms_order = 0; // VerboseMoveStats order, 0 means not present, 1 is the worst, higher is better. this.wdl = null; // Either null or a length 3 array of ints. }, set_pv: function(pv) { this.pv = Array.from(pv); this.nice_pv_cache = null; }, nice_pv: function() { // Human readable moves. if (this.nice_pv_cache) { return Array.from(this.nice_pv_cache); } let tmp_board = this.board; if (!this.pv || this.pv.length === 0) { // Should be impossible. this.pv = [this.move]; } let ret = []; for (let move of this.pv) { // if (tmp_board.illegal(move)) break; // Should be impossible as of 1.8.4: PVs are validated upon reception, and the only other // way they can get changed is by maybe_infer_info(), which hopefully is sound. ret.push(tmp_board.nice_string(move)); tmp_board = tmp_board.move(move); } this.nice_pv_cache = ret; return Array.from(this.nice_pv_cache); }, value: function() { return Value(this.q); // Rescaled to 0..1 }, value_string: function(dp, pov) { if (!this.__touched || typeof this.q !== "number") { return "?"; } if (this.leelaish && this.n === 0) { return "?"; } let val = this.value(); if ((pov === "w" && this.board.active === "b") || (pov === "b" && this.board.active === "w")) { val = 1 - val; } return (val * 100).toFixed(dp); }, cp_string: function(pov) { if (!this.__touched || typeof this.cp !== "number") { return "?"; } if (this.leelaish && this.n === 0) { return "?"; } let cp = this.cp; if ((pov === "w" && this.board.active === "b") || (pov === "b" && this.board.active === "w")) { cp = 0 - cp; } let ret = (cp / 100).toFixed(2); if (cp > 0) { ret = "+" + ret; } return ret; }, mate_string: function(pov) { if (typeof this.mate !== "number" || this.mate === 0) { return "?"; } let mate = this.mate; if ((pov === "w" && this.board.active === "b") || (pov === "b" && this.board.active === "w")) { mate = 0 - mate; } if (mate < 0) { return `(-M${0 - mate})`; } else { return `(+M${mate})`; } }, wdl_string: function(pov) { if (Array.isArray(this.wdl) === false || this.wdl.length !== 3) { return "?"; } if ((pov === "w" && this.board.active === "b") || (pov === "b" && this.board.active === "w")) { return `${this.wdl[2]} ${this.wdl[1]} ${this.wdl[0]}`; } else { return `${this.wdl[0]} ${this.wdl[1]} ${this.wdl[2]}`; } }, stats_list: function(opts, total_nodes) { // We pass total_nodes rather than use this.uci_nodes which can be obsolete (e.g. due to searchmoves) if (this.__ghost) { return ["Inferred"]; } let ret = []; if (opts.ev) { ret.push(`EV: ${this.value_string(1, opts.ev_pov)}%`); } if (opts.cp) { ret.push(`CP: ${this.cp_string(opts.cp_pov)}`); } // N is fairly complicated... if (this.leelaish) { if (typeof this.n === "number" && total_nodes) { // i.e. total_nodes is not zero or undefined let n_string = ""; if (opts.n) { n_string += ` N: ${(100 * this.n / total_nodes).toFixed(2)}%`; } if (opts.n_abs) { if (opts.n) { n_string += ` [${NString(this.n)}]`; } else { n_string += ` N: ${NString(this.n)}`; } } if (opts.of_n) { n_string += ` of ${NString(total_nodes)}`; } if (n_string !== "") { ret.push(n_string.trim()); } } else { if (opts.n || opts.n_abs || opts.of_n) { ret.push("N: ?"); } } } // Everything else... if (!this.leelaish) { if (opts.depth) { if (typeof this.depth === "number" && this.depth > 0) { ret.push(`Depth: ${this.depth}`); } else { ret.push(`Depth: 0`); } } } if (this.leelaish) { if (opts.p) { if (typeof this.p === "number" && this.p > 0) { ret.push(`P: ${this.p}%`); } else { ret.push(`P: ?`); } } if (opts.v) { if (typeof this.v === "number") { ret.push(`V: ${this.v.toFixed(3)}`); } else { ret.push(`V: ?`); } } } if (opts.q) { if (typeof this.q === "number") { ret.push(`Q: ${this.q.toFixed(3)}`); } else { ret.push(`Q: ?`); } } if (this.leelaish) { if (opts.u) { if (typeof this.u === "number" && this.n > 0) { // Checking n is correct. ret.push(`U: ${this.u.toFixed(3)}`); } else { ret.push(`U: ?`); } } if (opts.s) { if (typeof this.s === "number" && this.n > 0) { // Checking n is correct. ret.push(`S: ${this.s.toFixed(5)}`); } else { ret.push(`S: ?`); } } if (opts.m) { if (typeof this.m === "number") { if (this.m > 0) { ret.push(`M: ${this.m.toFixed(1)}`); } else { ret.push(`M: 0`); } } else { ret.push(`M: ?`); } } } if (opts.wdl) { ret.push(`WDL: ${this.wdl_string(opts.wdl_pov)}`); } return ret; } }; ================================================ FILE: files/src/renderer/51_node.js ================================================ "use strict"; function NewNode(parent, move, board_for_root) { // move must be legal; board is only relevant for root nodes let node = Object.create(node_prototype); node.id = next_node_id++; live_nodes[node.id.toString()] = node; if (parent) { parent.children.push(node); node.parent = parent; node.move = move; node.board = parent.board.move(move); node.depth = parent.depth + 1; node.graph_length_knower = parent.graph_length_knower; // 1 object every node points to, a bit lame } else { node.parent = null; node.move = null; node.board = board_for_root; node.depth = 0; node.graph_length_knower = {val: config.graph_minimum_length}; } if (node.depth + 1 > node.graph_length_knower.val) { node.graph_length_knower.val = node.depth + 1; } node.table = NewTable(); node.searchmoves = []; node.__nice_move = null; node.destroyed = false; node.children = []; return node; } function NewRoot(board) { // Arg is a board (position) object, not a FEN if (!board) { board = LoadFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); } let root = NewNode(null, null, board); // Tags. Only root gets these. Get overwritten by the PGN loader. // Internally, these get kept as HTML-safe, PGN-unsafe. root.tags = Object.create(null); root.tags.Event = "?"; root.tags.Site = "?"; root.tags.Date = DateString(new Date()); root.tags.Round = "?"; root.tags.White = "White"; root.tags.Black = "Black"; root.tags.Result = "*"; return root; } const node_prototype = { make_move: function(s, force_new_node) { // s must be exactly a legal move, including having promotion char iff needed (e.g. e2e1q) if (!force_new_node) { for (let child of this.children) { if (child.move === s) { return child; } } } return NewNode(this, s, null); }, history: function() { let ret = []; let node = this; while (node.move) { ret.push(node.move); node = node.parent; } ret.reverse(); return ret; }, history_old_format: function() { // For engines that can't handle Chess960 format stuff. let ret = []; let node = this; while (node.move) { ret.push(node.move_old_format()); node = node.parent; } ret.reverse(); return ret; }, move_old_format: function() { let move = this.move; if (move === "e1h1" && this.parent.board.state[4][7] === "K") return "e1g1"; if (move === "e1a1" && this.parent.board.state[4][7] === "K") return "e1c1"; if (move === "e8h8" && this.parent.board.state[4][0] === "k") return "e8g8"; if (move === "e8a8" && this.parent.board.state[4][0] === "k") return "e8c8"; return move; }, node_history: function() { let ret = []; let node = this; while (node) { ret.push(node); node = node.parent; } ret.reverse(); return ret; }, all_graph_values: function() { // Call this on any node in the line will give the same result. let ret = []; let node = this.get_end(); while (node) { ret.push(node.table.get_graph_y()); node = node.parent; } ret.reverse(); return ret; }, future_history: function() { return this.get_end().history(); }, future_node_history: function() { return this.get_end().node_history(); }, get_root: function() { let node = this; while (node.parent) { node = node.parent; } return node; }, get_end: function() { let node = this; while (node.children.length > 0) { node = node.children[0]; } return node; }, return_to_main_line_helper: function() { // Returns the node that "return to main line" should go to. let ret = this; let node = this; while (node.parent) { if (node.parent.children[0] !== node) { ret = node.parent; } node = node.parent; } return ret; }, is_main_line: function() { let node = this; while (node.parent) { if (node.parent.children[0] !== node) { return false; } node = node.parent; } return true; }, is_same_line: function(other) { // This is not testing whether one is an ancestor of the other, but // rather whether the main lines of each end in the same place. // Easy case is when one is the parent of the other... if (this.parent === other) return other.children[0] === this; if (other.parent === this) return this.children[0] === other; return this.get_end() === other.get_end(); }, is_triple_rep: function() { // Are there enough ancestors since the last pawn move or capture? if (this.board.halfmove < 8) { return false; } let ancestor = this; let hits = 0; while (ancestor.parent && ancestor.parent.parent) { ancestor = ancestor.parent.parent; if (ancestor.board.compare(this.board, hits >= 1)) { // Strict mode (about e.p. capture being legal) if we just need 1 more (earlier) hit. hits++; if (hits >= 2) { return true; } } // All further ancestors are the wrong side of a pawn move or capture? if (ancestor.board.halfmove < 2) { return false; } } return false; }, nice_move: function() { if (this.__nice_move) { return this.__nice_move; } if (!this.move || !this.parent) { this.__nice_move = "??"; } else { this.__nice_move = this.parent.board.nice_string(this.move); } return this.__nice_move; }, token: function(stats_flag, force_number_flag) { // The complete token when writing the move, including number string if necessary, // which depends on position within variations etc and so cannot easily be cached. // We don't do brackets because closing brackets are complicated. if (!this.move || !this.parent) { return ""; } let need_number_string = false; if (force_number_flag) need_number_string = true; if (!this.parent.parent) need_number_string = true; if (this.parent.board.active === "w") need_number_string = true; if (this.parent.children[0] !== this) need_number_string = true; // There are some other cases where we are supposed to have numbers but the logic // escapes me right now. let s = ""; if (need_number_string) { s += this.parent.board.next_number_string() + " "; } s += this.nice_move(); if (stats_flag) { let stats = this.make_stats(); if (stats !== "") { s += " {" + stats + "}"; } } return s; }, make_stats: function() { if (!this.parent) { return ""; } let info = this.parent.table.moveinfo[this.move]; let total_nodes = this.parent.table.nodes; if (!info || info.__ghost || info.__touched === false) { return ""; } let sl = info.stats_list({ ev_pov: config.ev_pov, cp_pov: config.cp_pov, wdl_pov: config.wdl_pov, ev: config.pgn_ev, cp: config.pgn_cp, n: config.pgn_n, n_abs: config.pgn_n_abs, of_n: config.pgn_of_n, depth: config.pgn_depth, wdl: config.pgn_wdl, p: config.pgn_p, m: config.pgn_m, v: config.pgn_v, q: config.pgn_q, u: config.pgn_u, s: config.pgn_s, }, total_nodes); return sl.join(", "); // Will be "" on empty list }, end_nodes: function() { if (this.children.length === 0) { return [this]; } else { let list = []; for (let child of this.children) { list = list.concat(child.end_nodes()); } return list; } }, terminal_reason: function() { // Returns "" if not a terminal position, otherwise returns the reason. // Also updates table.graph_y if needed. if (typeof this.table.terminal === "string") { return this.table.terminal; } let board = this.board; if (board.no_moves()) { if (board.king_in_check()) { this.table.set_terminal_info("Checkmate", board.active === "w" ? 0 : 1); // The PGN writer checks for this exact string! (Lame...) } else { this.table.set_terminal_info("Stalemate", 0.5); } } else if (board.insufficient_material()) { this.table.set_terminal_info("Insufficient Material", 0.5); } else if (board.halfmove >= 100) { this.table.set_terminal_info("50 Move Rule", 0.5); } else if (this.is_triple_rep()) { this.table.set_terminal_info("Triple Repetition", 0.5); } else { this.table.set_terminal_info("", null); } return this.table.terminal; }, validate_searchmoves: function(arr) { // Returns a new array with only legal searchmoves. if (Array.isArray(arr) === false) { arr = []; } let valid_list = []; for (let move of arr) { if (this.board.illegal(move) === "") { valid_list.push(move); } } return valid_list; }, detach: function() { // Returns the node that the hub should point to, // which is the parent unless the call is a bad one. let parent = this.parent; if (!parent) return this; // Fail parent.children = parent.children.filter(child => child !== this); this.parent = null; DestroyTree(this); return parent; }, polyglot_book: function(filepath) { // Partially written by Claude, secret method for Naphthalin, saves .bin to filepath let book = []; AddTreeToBook(this, book); let polyglot_entries = []; for (let entry of book) { let [x1, y1] = XY(entry.move.slice(0, 2)); let [x2, y2] = XY(entry.move.slice(2, 4)); let promotion = entry.move.slice(4); let move_val = 0; let promotion_val = 0; if (promotion) { switch (promotion.toLowerCase()) { case "n": promotion_val = 1; break; case "b": promotion_val = 2; break; case "r": promotion_val = 3; break; case "q": promotion_val = 4; break; } } move_val |= (x2 & 0b111); // bits 0-2: to file move_val |= (((7 - y2) & 0b111) << 3); // bits 3-5: to rank move_val |= ((x1 & 0b111) << 6); // bits 6-8: from file move_val |= (((7 - y1) & 0b111) << 9); // bits 9-11: from rank move_val |= ((promotion_val & 0b111) << 12); // bits 12-14: promotion piece polyglot_entries.push((entry.key << 64n) + (BigInt(move_val) << 48n) + (1n << 32n)); } polyglot_entries.sort((a, b) => (a < b) ? -1 : (a > b) ? 1 : 0); let buffer = Buffer.alloc(polyglot_entries.length * 16); for (let i = 0; i < polyglot_entries.length; i++) { let entry = polyglot_entries[i]; let position = i * 16; for (let j = 0; j < 16; j++) { let bit_shift = BigInt(15 - j) * 8n; let byte_val = Number((entry >> bit_shift) & 0xFFn); buffer[position + j] = byte_val; } } fs.writeFileSync(filepath, buffer); }, }; // --------------------------------------------------------------------------------------------------------- // On the theory that it might help the garbage collector, we can // destroy trees when we're done with them. Whether this is helpful // in general I don't know, but we also take this opportunity to // clear nodes from the live_list. function DestroyTree(node) { if (!node || node.destroyed) { console.log("Warning: DestroyTree() called with invalid arg"); return; } __destroy_tree(node.get_root()); } function __destroy_tree(node) { // Non-recursive when possible... while (node.children.length === 1) { let child = node.children[0]; node.parent = null; node.board = null; node.children = null; node.searchmoves = null; node.table = null; node.graph_length_knower = null; node.destroyed = true; delete live_nodes[node.id.toString()]; node = child; } // Recursive when necessary... let children = node.children; node.parent = null; node.board = null; node.children = null; node.searchmoves = null; node.table = null; node.graph_length_knower = null; node.destroyed = true; delete live_nodes[node.id.toString()]; for (let child of children) { __destroy_tree(child); } } // --------------------------------------------------------------------------------------------------------- // Reset analysis and searchmove selections, recursively. function CleanTree(node) { if (!node || node.destroyed) { return; } __clean_tree(node.get_root()); } function __clean_tree(node) { // Non-recursive when possible... while (node.children.length === 1) { node.table.clear(); node.searchmoves = []; node = node.children[0]; } // Recursive when necessary... node.table.clear(); node.searchmoves = []; for (let child of node.children) { __clean_tree(child); } } // ------------------------------------------------------------------------------------------------------ // Add positions to a book, using the given tree. No sorting here, needs to be done after completion. function AddTreeToBook(node, book) { if (!book || Array.isArray(book) === false) { throw "AddTreeToBook called without valid array"; } if (!node || node.destroyed) { return book; } __add_tree_to_book(node.get_root(), book); return book; } function __add_tree_to_book(node, book) { // Non-recursive when possible... while (node.children.length === 1) { let key = KeyFromBoard(node.board); let move = node.children[0].move; book.push({ // Duplicates allowed. This is improper. key: key, move: move, weight: 1, }); node = node.children[0]; } if (node.children.length === 0) { // Do this test here, not at the start, since it can become true. return; } // Recursive when necessary... let key = KeyFromBoard(node.board); for (let child of node.children) { book.push({ // Duplicates allowed. This is improper. key: key, move: child.move, weight: 1, }); __add_tree_to_book(child, book); } } ================================================ FILE: files/src/renderer/52_sorted_moves.js ================================================ "use strict"; function SortedMoveInfo(node) { if (!node || node.destroyed) { return []; } return SortedMoveInfoFromTable(node.table); } function SortedMoveInfoFromTable(table) { // There are a lot of subtleties around sorting the moves... // // - We want to allow other engines than Lc0. // - We want to work with low MultiPV values. // - Old and stale data can be left in our cache if MultiPV is low. // - We want to work with searchmoves, which is bound to leave stale info in the table. // - We can try and track the age of the data by various means, but these are fallible. let info_list = []; let latest_cycle = 0; let latest_subcycle = 0; for (let o of Object.values(table.moveinfo)) { info_list.push(o); if (o.cycle > latest_cycle) latest_cycle = o.cycle; if (o.subcycle > latest_subcycle) latest_subcycle = o.subcycle; } // It's important that the sort be transitive. I believe it is. info_list.sort((a, b) => { const a_is_best = -1; // return -1 to sort a to the left const b_is_best = 1; // return 1 to sort a to the right // Info that hasn't been touched must be worse... if (a.__touched && !b.__touched) return a_is_best; if (!a.__touched && b.__touched) return b_is_best; // Always prefer info from the current "go" specifically. // As well as being correct generally, it also moves searchmoves to the top. if (a.cycle === latest_cycle && b.cycle !== latest_cycle) return a_is_best; if (a.cycle !== latest_cycle && b.cycle === latest_cycle) return b_is_best; // Prefer info from the current "block" of info specifically. if (a.subcycle === latest_subcycle && b.subcycle !== latest_subcycle) return a_is_best; if (a.subcycle !== latest_subcycle && b.subcycle === latest_subcycle) return b_is_best; // If one info is leelaish and the other isn't, that can only mean that the A/B // engine is the one that ran last (since Lc0 will cause all info to become // leelaish), therefore any moves the A/B engine has touched must be "better". if (!a.leelaish && b.leelaish) return a_is_best; if (a.leelaish && !b.leelaish) return b_is_best; // ----------------------------------- LEELA AND LEELA-LIKE ENGINES ----------------------------------- // if (a.leelaish && b.leelaish) { // Mate - positive good, negative bad. // Note our info struct uses 0 when not given. if (Sign(a.mate) !== Sign(b.mate)) { // negative is worst, 0 is neutral, positive is best if (a.mate > b.mate) return a_is_best; if (a.mate < b.mate) return b_is_best; } else { // lower (i.e. towards -Inf) is better regardless of who's mating if (a.mate < b.mate) return a_is_best; if (a.mate > b.mate) return b_is_best; } // Ordering by VerboseMoveStats (suggestion of Napthalin)... if (a.vms_order > b.vms_order) return a_is_best; if (a.vms_order < b.vms_order) return b_is_best; // Leela N score (node count) - higher is better (shouldn't be possible to get here now)... if (a.n > b.n) return a_is_best; if (a.n < b.n) return b_is_best; } // ---------------------------------------- ALPHA-BETA ENGINES ---------------------------------------- // if (a.leelaish === false && b.leelaish === false) { // Specifically within the latest subcycle, prefer lower multipv. I don't think this // breaks transitivity because the latest subcycle is always sorted left (see above). if (a.subcycle === latest_subcycle && b.subcycle === latest_subcycle) { if (a.multipv < b.multipv) return a_is_best; if (a.multipv > b.multipv) return b_is_best; } // Otherwise sort by depth. if (a.depth > b.depth) return a_is_best; if (a.depth < b.depth) return b_is_best; // Sort by CP if we somehow get here. if (a.cp > b.cp) return a_is_best; if (a.cp < b.cp) return b_is_best; } // Sort alphabetically... if (a.nice_pv_cache && b.nice_pv_cache) { if (a.nice_pv_cache[0] < b.nice_pv_cache[0]) return a_is_best; if (a.nice_pv_cache[0] > b.nice_pv_cache[0]) return b_is_best; } return 0; }); return info_list; } ================================================ FILE: files/src/renderer/55_winrate_graph.js ================================================ "use strict"; function NewGrapher() { let grapher = Object.create(null); grapher.dragging = false; // Used by the event handlers in start.js grapher.clear_graph = function() { let boundingrect = graph.getBoundingClientRect(); let width = window.innerWidth - boundingrect.left - 16; let height = boundingrect.bottom - boundingrect.top; // This clears the canvas... graph.width = width; graph.height = height; }; grapher.draw = function(node, force) { if (config.graph_height <= 0) { return; } this.draw_everything(node); }; grapher.draw_everything = function(node) { this.clear_graph(); let width = graph.width; // After the above. let height = graph.height; let eval_list = node.all_graph_values(); this.draw_horizontal_lines(width, height, [1/3, 2/3]); this.draw_position_line(eval_list.length, node); // We make lists of contiguous edges that can be drawn at once... let runs = this.make_runs(eval_list, width, height, node.graph_length_knower.val); // Draw our normal runs... graphctx.strokeStyle = "white"; graphctx.lineWidth = config.graph_line_width; graphctx.lineJoin = "round"; graphctx.setLineDash([]); for (let run of runs.normal_runs) { graphctx.beginPath(); graphctx.moveTo(run[0].x1, run[0].y1); for (let edge of run) { graphctx.lineTo(edge.x2, edge.y2); } graphctx.stroke(); } // Draw our dashed runs... graphctx.strokeStyle = "#999999"; graphctx.lineWidth = config.graph_line_width; graphctx.setLineDash([config.graph_line_width, config.graph_line_width]); for (let run of runs.dashed_runs) { graphctx.beginPath(); graphctx.moveTo(run[0].x1, run[0].y1); for (let edge of run) { graphctx.lineTo(edge.x2, edge.y2); } graphctx.stroke(); } }; grapher.make_runs = function(eval_list, width, height, graph_length) { // Returns an object with 2 arrays (normal_runs and dashed_runs). // Each of those is an array of arrays of contiguous edges that can be drawn at once. let all_edges = []; let last_x = null; let last_y = null; let last_n = null; // This loop creates all edges that we are going to draw, and marks each // edge as dashed or not... for (let n = 0; n < eval_list.length; n++) { let e = eval_list[n]; if (e !== null) { let x = width * n / graph_length; let y = (1 - e) * height; if (y < 1) y = 1; if (y > height - 2) y = height - 2; if (last_x !== null) { all_edges.push({ x1: last_x, y1: last_y, x2: x, y2: y, dashed: n - last_n !== 1, }); } last_x = x; last_y = y; last_n = n; } } // Now we make runs of contiguous edges that share a style... let normal_runs = []; let dashed_runs = []; let run = []; let current_meta_list = normal_runs; // Will point at normal_runs or dashed_runs. for (let edge of all_edges) { if ((edge.dashed && current_meta_list !== dashed_runs) || (!edge.dashed && current_meta_list !== normal_runs)) { if (run.length > 0) { current_meta_list.push(run); } current_meta_list = edge.dashed ? dashed_runs : normal_runs; run = []; } run.push(edge); } if (run.length > 0) { current_meta_list.push(run); } return {normal_runs, dashed_runs}; }; grapher.draw_horizontal_lines = function(width, height, y_fractions = [0.5]) { // Avoid anti-aliasing... (FIXME: we assumed graph size was even) let pixel_y_adjustment = config.graph_line_width % 2 === 0 ? 0 : -0.5; graphctx.strokeStyle = "#666666"; graphctx.lineWidth = config.graph_line_width; graphctx.setLineDash([config.graph_line_width, config.graph_line_width]); for (let y_fraction of y_fractions) { graphctx.beginPath(); graphctx.moveTo(0, height * y_fraction + pixel_y_adjustment); graphctx.lineTo(width, height * y_fraction + pixel_y_adjustment); graphctx.stroke(); } }; grapher.draw_position_line = function(eval_list_length, node) { if (eval_list_length < 2) { return; } let width = graph.width; let height = graph.height; // Avoid anti-aliasing... let pixel_x_adjustment = config.graph_line_width % 2 === 0 ? 0 : 0.5; let x = Math.floor(width * node.depth / node.graph_length_knower.val) + pixel_x_adjustment; graphctx.strokeStyle = node.is_main_line() ? "#6cccee" : "#ffff00"; graphctx.lineWidth = config.graph_line_width; graphctx.setLineDash([config.graph_line_width, config.graph_line_width]); graphctx.beginPath(); graphctx.moveTo(x, 0); graphctx.lineTo(x, height); graphctx.stroke(); }; grapher.node_from_click = function(node, event) { if (!event || config.graph_height <= 0) { return null; } let mousex = event.offsetX; if (typeof mousex !== "number") { return null; } let width = graph.width; if (typeof width !== "number" || width < 1) { return null; } let node_list = node.future_node_history(); if (node_list.length === 0) { return null; } // OK, everything is valid... let click_depth = Math.round(node.graph_length_knower.val * mousex / width); if (click_depth < 0) click_depth = 0; if (click_depth >= node_list.length) click_depth = node_list.length - 1; return node_list[click_depth]; }; return grapher; } ================================================ FILE: files/src/renderer/60_pgn_utils.js ================================================ "use strict"; function split_buffer(buf) { // Split a binary buffer into an array of binary buffers corresponding to lines. let lines = []; let push = (arr) => { if (arr.length > 0 && arr[arr.length - 1] === 13) { // Discard \r lines.push(Buffer.from(arr.slice(0, -1))); } else { lines.push(Buffer.from(arr)); } }; let a = 0; let b; if (buf.length > 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) { a = 3; // 1st slice will skip byte order mark (BOM). } for (b = 0; b < buf.length; b++) { let ch = buf[b]; if (ch === 10) { // Split on \n let line = buf.slice(a, b); push(line); a = b + 1; } } if (a !== b) { // We haven't added the last line before EOF. let line = buf.slice(a, b); push(line); } return lines; } function new_byte_pusher(size) { if (!size || size <= 0) { size = 16; } // I bet Node has something like this, but I didn't read the docs. return { storage: new Uint8Array(size), length: 0, // Both the length and also the next index to write to. push: function(c) { if (this.length >= this.storage.length) { let new_storage = new Uint8Array(this.storage.length * 2); for (let n = 0; n < this.storage.length; n++) { new_storage[n] = this.storage[n]; } this.storage = new_storage; } this.storage[this.length] = c; this.length++; }, reset: function() { this.length = 0; }, bytes: function() { return this.storage.slice(0, this.length); }, string: function() { return decoder.decode(this.bytes()); } }; } function new_pgndata(buf, indices) { // Made by the PGN file loader. Used by the hub. let ret = {buf, indices}; ret.source = "Unknown source"; ret.count = function() { return this.indices.length; }; ret.getrecord = function(n) { if (typeof n !== "number" || n < 0 || n >= this.indices.length) { return null; } return PreParsePGN(this.buf.slice(this.indices[n], this.indices[n + 1])); // if n + 1 is out-of-bounds, still works. }; ret.string = function(n) { if (typeof n !== "number" || n < 0 || n >= this.indices.length) { return ""; } return this.buf.slice(this.indices[n], this.indices[n + 1]).toString(); // For debugging. }; return ret; } // ------------------------------------------------------------------------------------------------------------------------------ function SavePGN(filename, node) { let s = make_pgn_string(node); try { fs.writeFileSync(filename, s); } catch (err) { alert(err); } } function PGNToClipboard(node) { let s = make_pgn_string(node); clipboard.writeText(s); } // ------------------------------------------------------------------------------------------------------------------------------ function make_pgn_string(node) { let root = node.get_root(); let start_fen = root.board.fen(true); if (!root.tags) { // This should be impossible. root.tags = Object.create(null); } // Let's set the Result tag if possible... let main_line_end = root.get_end(); let terminal_reason = main_line_end.terminal_reason(); if (terminal_reason === "") { // Pass - leave it unchanged since we know nothing } else if (terminal_reason === "Checkmate") { root.tags.Result = main_line_end.board.active === "w" ? "0-1" : "1-0"; } else { root.tags.Result = "1/2-1/2"; } // Convert tag object to PGN formatted strings... let tags = []; for (let t of ["Event", "Site", "Date", "Round", "White", "Black", "Result"]) { if (root.tags[t]) { let val = SafeStringPGN(UnsafeStringHTML(root.tags[t])); // Undo HTML escaping then add PGN escaping. tags.push(`[${t} "${val}"]`); } } if (start_fen !== "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") { if (root.board.normalchess === false) { tags.push(`[Variant "Chess960"]`); } tags.push(`[FEN "${start_fen}"]`); tags.push(`[SetUp "1"]`); } let movetext = make_movetext(root); let final = tags.join("\n") + "\n\n" + movetext + "\n"; return final; } function make_movetext(node) { let root = node.get_root(); let ordered_nodes = get_ordered_nodes(root); let tokens = []; for (let item of ordered_nodes) { if (item === root) continue; // As it stands, item could be a "(" or ")" string, or an actual node... if (typeof item === "string") { tokens.push(item); } else { let item_token = item.token(true); let subtokens = item_token.split(" ").filter(z => z !== ""); for (let subtoken of subtokens) { tokens.push(subtoken); } } } if (root.tags && root.tags.Result) { tokens.push(root.tags.Result); } else { tokens.push("*"); } // Now it's all about wrapping to 80 chars... let lines = []; let line = ""; for (let token of tokens) { if (line.length + token.length > 79) { if (line !== "") { lines.push(line); } line = token; } else { if (line.length > 0 && line.endsWith("(") === false && token !== ")") { line += " "; } line += token; } } if (line !== "") { lines.push(line); } return lines.join("\n"); } // The following is to order the nodes into the order they would be written // to screen or PGN. The result does contain root, which shouldn't be drawn. // // As a crude hack, the list also contains "(" and ")" elements to indicate // where brackets should be drawn. function get_ordered_nodes(node) { let list = []; __order_nodes(node, list, false); return list; } function __order_nodes(node, list, skip_self_flag) { // Write this node itself... if (!skip_self_flag) { list.push(node); } // Write descendents as long as there's no branching, // or return if we reach a node with no children. while (node.children.length === 1) { node = node.children[0]; list.push(node); } if (node.children.length === 0) { return; } // So multiple child nodes exist... let main_child = node.children[0]; list.push(main_child); for (let child of node.children.slice(1)) { list.push("("); __order_nodes(child, list, false); list.push(")"); } __order_nodes(main_child, list, true); } ================================================ FILE: files/src/renderer/61_pgn_parse.js ================================================ "use strict"; function new_pgn_record() { return { tags: Object.create(null), movebufs: [] }; } function PreParsePGN(buf) { // buf should be the buffer for a single game, only. // Partial parse of the buffer. Generates a tags object and a list of buffers, each of which is a line // in the movetext. Not so sure this approach makes sense any more, if it ever did. In particular, // there's no really great reason why the movetext needs to be split into lines at all. // // Never fails. Always returns a valid object (though possibly containing illegal movetext). let game = new_pgn_record(); let lines = split_buffer(buf); for (let rawline of lines) { if (rawline.length === 0) { continue; } if (rawline[0] === 37) { // Percent % sign is a special comment type. continue; } let tagline; if (game.movebufs.length === 0) { // If we have movetext then this can't be a tag line. if (rawline[0] === 91) { let s = decoder.decode(rawline).trim(); if (s.endsWith(`]`)) { tagline = s; } } } if (tagline) { tagline = tagline.slice(1, -1).trim(); // So now it's like: Foo "bar etc" let first_space_i = tagline.indexOf(` `); if (first_space_i === -1) { continue; } let key = tagline.slice(0, first_space_i).trim(); let value = tagline.slice(first_space_i + 1).trim(); if (value.startsWith(`"`)) value = value.slice(1); if (value.endsWith(`"`)) value = value.slice(0, -1); value = value.trim(); game.tags[key] = SafeStringHTML(UnsafeStringPGN(value)); // Undo PGN escaping then add HTML escaping. } else { game.movebufs.push(rawline); } } return game; } function LoadPGNRecord(o) { // This can throw! // Parse of the objects produced above, to generate a game tree. // Tags are placed into the root's own tags object. let startpos; if (o.tags.FEN) { // && o.tags.SetUp === "1" - but some writers don't do this. try { startpos = LoadFEN(o.tags.FEN); } catch (err) { throw err; // Rethrow - the try/catch here is just to be explicit about this case. } } else { startpos = LoadFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); } let root = NewRoot(startpos); let node = root; let inside_brace = false; // {} are comments. Braces do not nest. let callstack = []; // When a parenthesis "(" opens, we record the node to "return" to later, on the "callstack". let token = new_byte_pusher(); let finished = false; for (let rawline of o.movebufs) { if (rawline.length === 0) { continue; } if (rawline[0] === 37) { // Percent % sign is a special comment type. continue; } for (let i = 0; i < rawline.length; i++) { // Note that, when adding characters to our current token, we peek forwards // to check if it's the end of the token. Therefore, it's safe for these // special characters to fire a continue immediately. let c = rawline[i]; if (c === 123) { // The opening brace { for a comment inside_brace = true; continue; } if (inside_brace) { if (c === 125) { // The closing brace } inside_brace = false; } continue; } if (c === 40) { // The opening parenthesis ( callstack.push(node); node = node.parent; // Unplay the last move. continue; } if (c === 41) { // The closing parenthesis ) node = callstack[callstack.length - 1]; callstack = callstack.slice(0, -1); continue; } // So... token.push(c); // Is the current token complete? // We'll start a new token when we see any of the following... let peek = rawline[i + 1]; if ( peek === undefined || // end of line peek <= 32 || // whitespace peek === 40 || // ( peek === 41 || // ) peek === 46 || // . peek === 123) { // { let s = token.string().trim(); token.reset(); // For the next round. // The above conditional means "." can only appear as the first character. // Strings like "..." get decomposed to a series of "." tokens since each one terminates the token in front of it. if (s[0] === ".") { s = s.slice(1); // s is now guaranteed not to start with "." } // Parse s. if (s === "" || s === "+" || s.startsWith("$") || StringIsNumeric(s)) { // Useless token. continue; } if (s === "1/2-1/2" || s === "1-0" || s === "0-1" || s === "*") { finished = true; break; } // Probably an actual move... let [move, error] = node.board.parse_pgn(s); if (error) { // If the problem specifically is one of Kd4, Ke4, Kd5, Ke5, it's probably just a DGT board thing // due to the kings being moved to indicate the result. if (s.includes("Kd4") || s.includes("Ke4") || s.includes("Kd5") || s.includes("Ke5") || s.includes("Kxd4") || s.includes("Kxe4") || s.includes("Kxd5") || s.includes("Kxe5")) { finished = true; break; } else { DestroyTree(root); throw `"${s}" -- ${error}`; } } node = node.make_move(move, true); } } if (finished) { break; } } // Save all tags into the root. if (!root.tags) { root.tags = Object.create(null); } for (let key of Object.keys(o.tags)) { root.tags[key] = o.tags[key]; } return root; } ================================================ FILE: files/src/renderer/63_polyglot.js ================================================ "use strict"; // http://hgm.nubati.net/book_format.html // Note on bitwise operations on BigInt values: everything is treated as infinite-length twos-compliment, // which means negatives will never be accidentally introduced. (For normal Numbers, bitwise operations // coerce to 32-bit signed.) const PolyglotPieceXorVals = [ // the trailing n here means BigInt 0x9d39247e33776d41n, 0x2af7398005aaa5c7n, 0x44db015024623547n, 0x9c15f73e62a76ae2n, 0x75834465489c0c89n, 0x3290ac3a203001bfn, 0x0fbbad1f61042279n, 0xe83a908ff2fb60can, 0x0d7e765d58755c10n, 0x1a083822ceafe02dn, 0x9605d5f0e25ec3b0n, 0xd021ff5cd13a2ed5n, 0x40bdf15d4a672e32n, 0x011355146fd56395n, 0x5db4832046f3d9e5n, 0x239f8b2d7ff719ccn, 0x05d1a1ae85b49aa1n, 0x679f848f6e8fc971n, 0x7449bbff801fed0bn, 0x7d11cdb1c3b7adf0n, 0x82c7709e781eb7ccn, 0xf3218f1c9510786cn, 0x331478f3af51bbe6n, 0x4bb38de5e7219443n, 0xaa649c6ebcfd50fcn, 0x8dbd98a352afd40bn, 0x87d2074b81d79217n, 0x19f3c751d3e92ae1n, 0xb4ab30f062b19abfn, 0x7b0500ac42047ac4n, 0xc9452ca81a09d85dn, 0x24aa6c514da27500n, 0x4c9f34427501b447n, 0x14a68fd73c910841n, 0xa71b9b83461cbd93n, 0x03488b95b0f1850fn, 0x637b2b34ff93c040n, 0x09d1bc9a3dd90a94n, 0x3575668334a1dd3bn, 0x735e2b97a4c45a23n, 0x18727070f1bd400bn, 0x1fcbacd259bf02e7n, 0xd310a7c2ce9b6555n, 0xbf983fe0fe5d8244n, 0x9f74d14f7454a824n, 0x51ebdc4ab9ba3035n, 0x5c82c505db9ab0fan, 0xfcf7fe8a3430b241n, 0x3253a729b9ba3dden, 0x8c74c368081b3075n, 0xb9bc6c87167c33e7n, 0x7ef48f2b83024e20n, 0x11d505d4c351bd7fn, 0x6568fca92c76a243n, 0x4de0b0f40f32a7b8n, 0x96d693460cc37e5dn, 0x42e240cb63689f2fn, 0x6d2bdcdae2919661n, 0x42880b0236e4d951n, 0x5f0f4a5898171bb6n, 0x39f890f579f92f88n, 0x93c5b5f47356388bn, 0x63dc359d8d231b78n, 0xec16ca8aea98ad76n, 0x5355f900c2a82dc7n, 0x07fb9f855a997142n, 0x5093417aa8a7ed5en, 0x7bcbc38da25a7f3cn, 0x19fc8a768cf4b6d4n, 0x637a7780decfc0d9n, 0x8249a47aee0e41f7n, 0x79ad695501e7d1e8n, 0x14acbaf4777d5776n, 0xf145b6beccdea195n, 0xdabf2ac8201752fcn, 0x24c3c94df9c8d3f6n, 0xbb6e2924f03912ean, 0x0ce26c0b95c980d9n, 0xa49cd132bfbf7cc4n, 0xe99d662af4243939n, 0x27e6ad7891165c3fn, 0x8535f040b9744ff1n, 0x54b3f4fa5f40d873n, 0x72b12c32127fed2bn, 0xee954d3c7b411f47n, 0x9a85ac909a24eaa1n, 0x70ac4cd9f04f21f5n, 0xf9b89d3e99a075c2n, 0x87b3e2b2b5c907b1n, 0xa366e5b8c54f48b8n, 0xae4a9346cc3f7cf2n, 0x1920c04d47267bbdn, 0x87bf02c6b49e2ae9n, 0x092237ac237f3859n, 0xff07f64ef8ed14d0n, 0x8de8dca9f03cc54en, 0x9c1633264db49c89n, 0xb3f22c3d0b0b38edn, 0x390e5fb44d01144bn, 0x5bfea5b4712768e9n, 0x1e1032911fa78984n, 0x9a74acb964e78cb3n, 0x4f80f7a035dafb04n, 0x6304d09a0b3738c4n, 0x2171e64683023a08n, 0x5b9b63eb9ceff80cn, 0x506aacf489889342n, 0x1881afc9a3a701d6n, 0x6503080440750644n, 0xdfd395339cdbf4a7n, 0xef927dbcf00c20f2n, 0x7b32f7d1e03680ecn, 0xb9fd7620e7316243n, 0x05a7e8a57db91b77n, 0xb5889c6e15630a75n, 0x4a750a09ce9573f7n, 0xcf464cec899a2f8an, 0xf538639ce705b824n, 0x3c79a0ff5580ef7fn, 0xede6c87f8477609dn, 0x799e81f05bc93f31n, 0x86536b8cf3428a8cn, 0x97d7374c60087b73n, 0xa246637cff328532n, 0x043fcae60cc0eba0n, 0x920e449535dd359en, 0x70eb093b15b290ccn, 0x73a1921916591cbdn, 0x56436c9fe1a1aa8dn, 0xefac4b70633b8f81n, 0xbb215798d45df7afn, 0x45f20042f24f1768n, 0x930f80f4e8eb7462n, 0xff6712ffcfd75ea1n, 0xae623fd67468aa70n, 0xdd2c5bc84bc8d8fcn, 0x7eed120d54cf2dd9n, 0x22fe545401165f1cn, 0xc91800e98fb99929n, 0x808bd68e6ac10365n, 0xdec468145b7605f6n, 0x1bede3a3aef53302n, 0x43539603d6c55602n, 0xaa969b5c691ccb7an, 0xa87832d392efee56n, 0x65942c7b3c7e11aen, 0xded2d633cad004f6n, 0x21f08570f420e565n, 0xb415938d7da94e3cn, 0x91b859e59ecb6350n, 0x10cff333e0ed804an, 0x28aed140be0bb7ddn, 0xc5cc1d89724fa456n, 0x5648f680f11a2741n, 0x2d255069f0b7dab3n, 0x9bc5a38ef729abd4n, 0xef2f054308f6a2bcn, 0xaf2042f5cc5c2858n, 0x480412bab7f5be2an, 0xaef3af4a563dfe43n, 0x19afe59ae451497fn, 0x52593803dff1e840n, 0xf4f076e65f2ce6f0n, 0x11379625747d5af3n, 0xbce5d2248682c115n, 0x9da4243de836994fn, 0x066f70b33fe09017n, 0x4dc4de189b671a1cn, 0x51039ab7712457c3n, 0xc07a3f80c31fb4b4n, 0xb46ee9c5e64a6e7cn, 0xb3819a42abe61c87n, 0x21a007933a522a20n, 0x2df16f761598aa4fn, 0x763c4a1371b368fdn, 0xf793c46702e086a0n, 0xd7288e012aeb8d31n, 0xde336a2a4bc1c44bn, 0x0bf692b38d079f23n, 0x2c604a7a177326b3n, 0x4850e73e03eb6064n, 0xcfc447f1e53c8e1bn, 0xb05ca3f564268d99n, 0x9ae182c8bc9474e8n, 0xa4fc4bd4fc5558can, 0xe755178d58fc4e76n, 0x69b97db1a4c03dfen, 0xf9b5b7c4acc67c96n, 0xfc6a82d64b8655fbn, 0x9c684cb6c4d24417n, 0x8ec97d2917456ed0n, 0x6703df9d2924e97en, 0xc547f57e42a7444en, 0x78e37644e7cad29en, 0xfe9a44e9362f05fan, 0x08bd35cc38336615n, 0x9315e5eb3a129acen, 0x94061b871e04df75n, 0xdf1d9f9d784ba010n, 0x3bba57b68871b59dn, 0xd2b7adeeded1f73fn, 0xf7a255d83bc373f8n, 0xd7f4f2448c0ceb81n, 0xd95be88cd210ffa7n, 0x336f52f8ff4728e7n, 0xa74049dac312ac71n, 0xa2f61bb6e437fdb5n, 0x4f2a5cb07f6a35b3n, 0x87d380bda5bf7859n, 0x16b9f7e06c453a21n, 0x7ba2484c8a0fd54en, 0xf3a678cad9a2e38cn, 0x39b0bf7dde437ba2n, 0xfcaf55c1bf8a4424n, 0x18fcf680573fa594n, 0x4c0563b89f495ac3n, 0x40e087931a00930dn, 0x8cffa9412eb642c1n, 0x68ca39053261169fn, 0x7a1ee967d27579e2n, 0x9d1d60e5076f5b6fn, 0x3810e399b6f65ba2n, 0x32095b6d4ab5f9b1n, 0x35cab62109dd038an, 0xa90b24499fcfafb1n, 0x77a225a07cc2c6bdn, 0x513e5e634c70e331n, 0x4361c0ca3f692f12n, 0xd941aca44b20a45bn, 0x528f7c8602c5807bn, 0x52ab92beb9613989n, 0x9d1dfa2efc557f73n, 0x722ff175f572c348n, 0x1d1260a51107fe97n, 0x7a249a57ec0c9ba2n, 0x04208fe9e8f7f2d6n, 0x5a110c6058b920a0n, 0x0cd9a497658a5698n, 0x56fd23c8f9715a4cn, 0x284c847b9d887aaen, 0x04feabfbbdb619cbn, 0x742e1e651c60ba83n, 0x9a9632e65904ad3cn, 0x881b82a13b51b9e2n, 0x506e6744cd974924n, 0xb0183db56ffc6a79n, 0x0ed9b915c66ed37en, 0x5e11e86d5873d484n, 0xf678647e3519ac6en, 0x1b85d488d0f20cc5n, 0xdab9fe6525d89021n, 0x0d151d86adb73615n, 0xa865a54edcc0f019n, 0x93c42566aef98ffbn, 0x99e7afeabe000731n, 0x48cbff086ddf285an, 0x7f9b6af1ebf78bafn, 0x58627e1a149bba21n, 0x2cd16e2abd791e33n, 0xd363eff5f0977996n, 0x0ce2a38c344a6eedn, 0x1a804aadb9cfa741n, 0x907f30421d78c5den, 0x501f65edb3034d07n, 0x37624ae5a48fa6e9n, 0x957baf61700cff4en, 0x3a6c27934e31188an, 0xd49503536abca345n, 0x088e049589c432e0n, 0xf943aee7febf21b8n, 0x6c3b8e3e336139d3n, 0x364f6ffa464ee52en, 0xd60f6dcedc314222n, 0x56963b0dca418fc0n, 0x16f50edf91e513afn, 0xef1955914b609f93n, 0x565601c0364e3228n, 0xecb53939887e8175n, 0xbac7a9a18531294bn, 0xb344c470397bba52n, 0x65d34954daf3cebdn, 0xb4b81b3fa97511e2n, 0xb422061193d6f6a7n, 0x071582401c38434dn, 0x7a13f18bbedc4ff5n, 0xbc4097b116c524d2n, 0x59b97885e2f2ea28n, 0x99170a5dc3115544n, 0x6f423357e7c6a9f9n, 0x325928ee6e6f8794n, 0xd0e4366228b03343n, 0x565c31f7de89ea27n, 0x30f5611484119414n, 0xd873db391292ed4fn, 0x7bd94e1d8e17debcn, 0xc7d9f16864a76e94n, 0x947ae053ee56e63cn, 0xc8c93882f9475f5fn, 0x3a9bf55ba91f81can, 0xd9a11fbb3d9808e4n, 0x0fd22063edc29fcan, 0xb3f256d8aca0b0b9n, 0xb03031a8b4516e84n, 0x35dd37d5871448afn, 0xe9f6082b05542e4en, 0xebfafa33d7254b59n, 0x9255abb50d532280n, 0xb9ab4ce57f2d34f3n, 0x693501d628297551n, 0xc62c58f97dd949bfn, 0xcd454f8f19c5126an, 0xbbe83f4ecc2bdecbn, 0xdc842b7e2819e230n, 0xba89142e007503b8n, 0xa3bc941d0a5061cbn, 0xe9f6760e32cd8021n, 0x09c7e552bc76492fn, 0x852f54934da55cc9n, 0x8107fccf064fcf56n, 0x098954d51fff6580n, 0x23b70edb1955c4bfn, 0xc330de426430f69dn, 0x4715ed43e8a45c0an, 0xa8d7e4dab780a08dn, 0x0572b974f03ce0bbn, 0xb57d2e985e1419c7n, 0xe8d9ecbe2cf3d73fn, 0x2fe4b17170e59750n, 0x11317ba87905e790n, 0x7fbf21ec8a1f45ecn, 0x1725cabfcb045b00n, 0x964e915cd5e2b207n, 0x3e2b8bcbf016d66dn, 0xbe7444e39328a0acn, 0xf85b2b4fbcde44b7n, 0x49353fea39ba63b1n, 0x1dd01aafcd53486an, 0x1fca8a92fd719f85n, 0xfc7c95d827357afan, 0x18a6a990c8b35ebdn, 0xcccb7005c6b9c28dn, 0x3bdbb92c43b17f26n, 0xaa70b5b4f89695a2n, 0xe94c39a54a98307fn, 0xb7a0b174cff6f36en, 0xd4dba84729af48adn, 0x2e18bc1ad9704a68n, 0x2de0966daf2f8b1cn, 0xb9c11d5b1e43a07en, 0x64972d68dee33360n, 0x94628d38d0c20584n, 0xdbc0d2b6ab90a559n, 0xd2733c4335c6a72fn, 0x7e75d99d94a70f4dn, 0x6ced1983376fa72bn, 0x97fcaacbf030bc24n, 0x7b77497b32503b12n, 0x8547eddfb81ccb94n, 0x79999cdff70902cbn, 0xcffe1939438e9b24n, 0x829626e3892d95d7n, 0x92fae24291f2b3f1n, 0x63e22c147b9c3403n, 0xc678b6d860284a1cn, 0x5873888850659ae7n, 0x0981dcd296a8736dn, 0x9f65789a6509a440n, 0x9ff38fed72e9052fn, 0xe479ee5b9930578cn, 0xe7f28ecd2d49eecdn, 0x56c074a581ea17fen, 0x5544f7d774b14aefn, 0x7b3f0195fc6f290fn, 0x12153635b2c0cf57n, 0x7f5126dbba5e0ca7n, 0x7a76956c3eafb413n, 0x3d5774a11d31ab39n, 0x8a1b083821f40cb4n, 0x7b4a38e32537df62n, 0x950113646d1d6e03n, 0x4da8979a0041e8a9n, 0x3bc36e078f7515d7n, 0x5d0a12f27ad310d1n, 0x7f9d1a2e1ebe1327n, 0xda3a361b1c5157b1n, 0xdcdd7d20903d0c25n, 0x36833336d068f707n, 0xce68341f79893389n, 0xab9090168dd05f34n, 0x43954b3252dc25e5n, 0xb438c2b67f98e5e9n, 0x10dcd78e3851a492n, 0xdbc27ab5447822bfn, 0x9b3cdb65f82ca382n, 0xb67b7896167b4c84n, 0xbfced1b0048eac50n, 0xa9119b60369ffebdn, 0x1fff7ac80904bf45n, 0xac12fb171817eee7n, 0xaf08da9177dda93dn, 0x1b0cab936e65c744n, 0xb559eb1d04e5e932n, 0xc37b45b3f8d6f2ban, 0xc3a9dc228caac9e9n, 0xf3b8b6675a6507ffn, 0x9fc477de4ed681dan, 0x67378d8eccef96cbn, 0x6dd856d94d259236n, 0xa319ce15b0b4db31n, 0x073973751f12dd5en, 0x8a8e849eb32781a5n, 0xe1925c71285279f5n, 0x74c04bf1790c0efen, 0x4dda48153c94938an, 0x9d266d6a1cc0542cn, 0x7440fb816508c4fen, 0x13328503df48229fn, 0xd6bf7baee43cac40n, 0x4838d65f6ef6748fn, 0x1e152328f3318dean, 0x8f8419a348f296bfn, 0x72c8834a5957b511n, 0xd7a023a73260b45cn, 0x94ebc8abcfb56daen, 0x9fc10d0f989993e0n, 0xde68a2355b93cae6n, 0xa44cfe79ae538bben, 0x9d1d84fcce371425n, 0x51d2b1ab2ddfb636n, 0x2fd7e4b9e72cd38cn, 0x65ca5b96b7552210n, 0xdd69a0d8ab3b546dn, 0x604d51b25fbf70e2n, 0x73aa8a564fb7ac9en, 0x1a8c1e992b941148n, 0xaac40a2703d9bea0n, 0x764dbeae7fa4f3a6n, 0x1e99b96e70a9be8bn, 0x2c5e9deb57ef4743n, 0x3a938fee32d29981n, 0x26e6db8ffdf5adfen, 0x469356c504ec9f9dn, 0xc8763c5b08d1908cn, 0x3f6c6af859d80055n, 0x7f7cc39420a3a545n, 0x9bfb227ebdf4c5cen, 0x89039d79d6fc5c5cn, 0x8fe88b57305e2ab6n, 0xa09e8c8c35ab96den, 0xfa7e393983325753n, 0xd6b6d0ecc617c699n, 0xdfea21ea9e7557e3n, 0xb67c1fa481680af8n, 0xca1e3785a9e724e5n, 0x1cfc8bed0d681639n, 0xd18d8549d140caean, 0x4ed0fe7e9dc91335n, 0xe4dbf0634473f5d2n, 0x1761f93a44d5aefen, 0x53898e4c3910da55n, 0x734de8181f6ec39an, 0x2680b122baa28d97n, 0x298af231c85bafabn, 0x7983eed3740847d5n, 0x66c1a2a1a60cd889n, 0x9e17e49642a3e4c1n, 0xedb454e7badc0805n, 0x50b704cab602c329n, 0x4cc317fb9cddd023n, 0x66b4835d9eafea22n, 0x219b97e26ffc81bdn, 0x261e4e4c0a333a9dn, 0x1fe2cca76517db90n, 0xd7504dfa8816edbbn, 0xb9571fa04dc089c8n, 0x1ddc0325259b27den, 0xcf3f4688801eb9aan, 0xf4f5d05c10cab243n, 0x38b6525c21a42b0en, 0x36f60e2ba4fa6800n, 0xeb3593803173e0cen, 0x9c4cd6257c5a3603n, 0xaf0c317d32adaa8an, 0x258e5a80c7204c4bn, 0x8b889d624d44885dn, 0xf4d14597e660f855n, 0xd4347f66ec8941c3n, 0xe699ed85b0dfb40dn, 0x2472f6207c2d0484n, 0xc2a1e7b5b459aeb5n, 0xab4f6451cc1d45ecn, 0x63767572ae3d6174n, 0xa59e0bd101731a28n, 0x116d0016cb948f09n, 0x2cf9c8ca052f6e9fn, 0x0b090a7560a968e3n, 0xabeeddb2dde06ff1n, 0x58efc10b06a2068dn, 0xc6e57a78fbd986e0n, 0x2eab8ca63ce802d7n, 0x14a195640116f336n, 0x7c0828dd624ec390n, 0xd74bbe77e6116ac7n, 0x804456af10f5fb53n, 0xebe9ea2adf4321c7n, 0x03219a39ee587a30n, 0x49787fef17af9924n, 0xa1e9300cd8520548n, 0x5b45e522e4b1b4efn, 0xb49c3b3995091a36n, 0xd4490ad526f14431n, 0x12a8f216af9418c2n, 0x001f837cc7350524n, 0x1877b51e57a764d5n, 0xa2853b80f17f58een, 0x993e1de72d36d310n, 0xb3598080ce64a656n, 0x252f59cf0d9f04bbn, 0xd23c8e176d113600n, 0x1bda0492e7e4586en, 0x21e0bd5026c619bfn, 0x3b097adaf088f94en, 0x8d14dedb30be846en, 0xf95cffa23af5f6f4n, 0x3871700761b3f743n, 0xca672b91e9e4fa16n, 0x64c8e531bff53b55n, 0x241260ed4ad1e87dn, 0x106c09b972d2e822n, 0x7fba195410e5ca30n, 0x7884d9bc6cb569d8n, 0x0647dfedcd894a29n, 0x63573ff03e224774n, 0x4fc8e9560f91b123n, 0x1db956e450275779n, 0xb8d91274b9e9d4fbn, 0xa2ebee47e2fbfce1n, 0xd9f1f30ccd97fb09n, 0xefed53d75fd64e6bn, 0x2e6d02c36017f67fn, 0xa9aa4d20db084e9bn, 0xb64be8d8b25396c1n, 0x70cb6af7c2d5bcf0n, 0x98f076a4f7a2322en, 0xbf84470805e69b5fn, 0x94c3251f06f90cf3n, 0x3e003e616a6591e9n, 0xb925a6cd0421aff3n, 0x61bdd1307c66e300n, 0xbf8d5108e27e0d48n, 0x240ab57a8b888b20n, 0xfc87614baf287e07n, 0xef02cdd06ffdb432n, 0xa1082c0466df6c0an, 0x8215e577001332c8n, 0xd39bb9c3a48db6cfn, 0x2738259634305c14n, 0x61cf4f94c97df93dn, 0x1b6baca2ae4e125bn, 0x758f450c88572e0bn, 0x959f587d507a8359n, 0xb063e962e045f54dn, 0x60e8ed72c0dff5d1n, 0x7b64978555326f9fn, 0xfd080d236da814ban, 0x8c90fd9b083f4558n, 0x106f72fe81e2c590n, 0x7976033a39f7d952n, 0xa4ec0132764ca04bn, 0x733ea705fae4fa77n, 0xb4d8f77bc3e56167n, 0x9e21f4f903b33fd9n, 0x9d765e419fb69f6dn, 0xd30c088ba61ea5efn, 0x5d94337fbfaf7f5bn, 0x1a4e4822eb4d7a59n, 0x6ffe73e81b637fb3n, 0xddf957bc36d8b9can, 0x64d0e29eea8838b3n, 0x08dd9bdfd96b9f63n, 0x087e79e5a57d1d13n, 0xe328e230e3e2b3fbn, 0x1c2559e30f0946ben, 0x720bf5f26f4d2eaan, 0xb0774d261cc609dbn, 0x443f64ec5a371195n, 0x4112cf68649a260en, 0xd813f2fab7f5c5can, 0x660d3257380841een, 0x59ac2c7873f910a3n, 0xe846963877671a17n, 0x93b633abfa3469f8n, 0xc0c0f5a60ef4cdcfn, 0xcaf21ecd4377b28cn, 0x57277707199b8175n, 0x506c11b9d90e8b1dn, 0xd83cc2687a19255fn, 0x4a29c6465a314cd1n, 0xed2df21216235097n, 0xb5635c95ff7296e2n, 0x22af003ab672e811n, 0x52e762596bf68235n, 0x9aeba33ac6ecc6b0n, 0x944f6de09134dfb6n, 0x6c47bec883a7de39n, 0x6ad047c430a12104n, 0xa5b1cfdba0ab4067n, 0x7c45d833aff07862n, 0x5092ef950a16da0bn, 0x9338e69c052b8e7bn, 0x455a4b4cfe30e3f5n, 0x6b02e63195ad0cf8n, 0x6b17b224bad6bf27n, 0xd1e0ccd25bb9c169n, 0xde0c89a556b9ae70n, 0x50065e535a213cf6n, 0x9c1169fa2777b874n, 0x78edefd694af1eedn, 0x6dc93d9526a50e68n, 0xee97f453f06791edn, 0x32ab0edb696703d3n, 0x3a6853c7e70757a7n, 0x31865ced6120f37dn, 0x67fef95d92607890n, 0x1f2b1d1f15f6dc9cn, 0xb69e38a8965c6b65n, 0xaa9119ff184cccf4n, 0xf43c732873f24c13n, 0xfb4a3d794a9a80d2n, 0x3550c2321fd6109cn, 0x371f77e76bb8417en, 0x6bfa9aae5ec05779n, 0xcd04f3ff001a4778n, 0xe3273522064480can, 0x9f91508bffcfc14an, 0x049a7f41061a9e60n, 0xfcb6be43a9f2fe9bn, 0x08de8a1c7797da9bn, 0x8f9887e6078735a1n, 0xb5b4071dbfc73a66n, 0x230e343dfba08d33n, 0x43ed7f5a0fae657dn, 0x3a88a0fbbcb05c63n, 0x21874b8b4d2dbc4fn, 0x1bdea12e35f6a8c9n, 0x53c065c6c8e63528n, 0xe34a1d250e7a8d6bn, 0xd6b04d3b7651dd7en, 0x5e90277e7cb39e2dn, 0x2c046f22062dc67dn, 0xb10bb459132d0a26n, 0x3fa9ddfb67e2f199n, 0x0e09b88e1914f7afn, 0x10e8b35af3eeab37n, 0x9eedeca8e272b933n, 0xd4c718bc4ae8ae5fn, 0x81536d601170fc20n, 0x91b534f885818a06n, 0xec8177f83f900978n, 0x190e714fada5156en, 0xb592bf39b0364963n, 0x89c350c893ae7dc1n, 0xac042e70f8b383f2n, 0xb49b52e587a1ee60n, 0xfb152fe3ff26da89n, 0x3e666e6f69ae2c15n, 0x3b544ebe544c19f9n, 0xe805a1e290cf2456n, 0x24b33c9d7ed25117n, 0xe74733427b72f0c1n, 0x0a804d18b7097475n, 0x57e3306d881edb4fn, 0x4ae7d6a36eb5dbcbn, 0x2d8d5432157064c8n, 0xd1e649de1e7f268bn, 0x8a328a1cedfe552cn, 0x07a3aec79624c7dan, 0x84547ddc3e203c94n, 0x990a98fd5071d263n, 0x1a4ff12616eefc89n, 0xf6f7fd1431714200n, 0x30c05b1ba332f41cn, 0x8d2636b81555a786n, 0x46c9feb55d120902n, 0xccec0a73b49c9921n, 0x4e9d2827355fc492n, 0x19ebb029435dcb0fn, 0x4659d2b743848a2cn, 0x963ef2c96b33be31n, 0x74f85198b05a2e7dn, 0x5a0f544dd2b1fb18n, 0x03727073c2e134b1n, 0xc7f6aa2de59aea61n, 0x352787baa0d7c22fn, 0x9853eab63b5e0b35n, 0xabbdcdd7ed5c0860n, 0xcf05daf5ac8d77b0n, 0x49cad48cebf4a71en, 0x7a4c10ec2158c4a6n, 0xd9e92aa246bf719en, 0x13ae978d09fe5557n, 0x730499af921549ffn, 0x4e4b705b92903ba4n, 0xff577222c14f0a3an, 0x55b6344cf97aafaen, 0xb862225b055b6960n, 0xcac09afbddd2cdb4n, 0xdaf8e9829fe96b5fn, 0xb5fdfc5d3132c498n, 0x310cb380db6f7503n, 0xe87fbb46217a360en, 0x2102ae466ebb1148n, 0xf8549e1a3aa5e00dn, 0x07a69afdcc42261an, 0xc4c118bfe78feaaen, 0xf9f4892ed96bd438n, 0x1af3dbe25d8f45dan, 0xf5b4b0b0d2deeeb4n, 0x962aceefa82e1c84n, 0x046e3ecaaf453ce9n, 0xf05d129681949a4cn, 0x964781ce734b3c84n, 0x9c2ed44081ce5fbdn, 0x522e23f3925e319en, 0x177e00f9fc32f791n, 0x2bc60a63a6f3b3f2n, 0x222bbfae61725606n, 0x486289ddcc3d6780n, 0x7dc7785b8efdfc80n, 0x8af38731c02ba980n, 0x1fab64ea29a2ddf7n, 0xe4d9429322cd065an, 0x9da058c67844f20cn, 0x24c0e332b70019b0n, 0x233003b5a6cfe6adn, 0xd586bd01c5c217f6n, 0x5e5637885f29bc2bn, 0x7eba726d8c94094bn, 0x0a56a5f0bfe39272n, 0xd79476a84ee20d06n, 0x9e4c1269baa4bf37n, 0x17efee45b0dee640n, 0x1d95b0a5fcf90bc6n, 0x93cbe0b699c2585dn, 0x65fa4f227a2b6d79n, 0xd5f9e858292504d5n, 0xc2b5a03f71471a6fn, 0x59300222b4561e00n, 0xce2f8642ca0712dcn, 0x7ca9723fbb2e8988n, 0x2785338347f2ba08n, 0xc61bb3a141e50e8cn, 0x150f361dab9dec26n, 0x9f6a419d382595f4n, 0x64a53dc924fe7ac9n, 0x142de49fff7a7c3dn, 0x0c335248857fa9e7n, 0x0a9c32d5eae45305n, 0xe6c42178c4bbb92en, 0x71f1ce2490d20b07n, 0xf1bcc3d275afe51an, 0xe728e8c83c334074n, 0x96fbf83a12884624n, 0x81a1549fd6573da5n, 0x5fa7867caf35e149n, 0x56986e2ef3ed091bn, 0x917f1dd5f8886c61n, 0xd20d8c88c8ffe65fn, ]; const PolyglotCastleXorVals = [ 0x31d71dce64b2c310n, 0xf165b587df898190n, 0xa57e6339dd2cf3a0n, 0x1ef6e6dbb1961ec9n, ]; const PolyglotEnPassantXorVals = [ 0x70cc73d90bc26e24n, 0xe21a6b35df0c3ad7n, 0x003a93d8b2806962n, 0x1c99ded33cb890a1n, 0xcf3145de0add4289n, 0xd0e4427a5514fb72n, 0x77c621cc9fb3a483n, 0x67a34dac4356550bn, ]; const PolyglotActiveXorVal = 0xf8d626aaaf278509n; // ------------------------------------------------------------------------------------------------------------------------ const PolyglotMoveLookup = []; // Lookup table for book blob bytes 8-9. Most of the moves are impossible, but meh. for (let n = 0; n < 65536; n++) { let to_file = (n >> 0) & 0x07; let to_row = (n >> 3) & 0x07; let from_file = (n >> 6) & 0x07; let from_row = (n >> 9) & 0x07; let promval = (n >> 12) & 0x07; let source = Point(from_file, 7 - from_row); let dest = Point(to_file, 7 - to_row); let promch = ["", "n", "b", "r", "q", "", "", ""][promval]; PolyglotMoveLookup.push(source.s + dest.s + promch); } // ------------------------------------------------------------------------------------------------------------------------ function KeyFromBoard(board) { if (!board) return ""; let key = 0n; // Note to anyone reading this trying to make their own Polyglot routines: // My board (0,0) is a8, not a1. Otherwise, you'd use y and not (7 - y) in the index calc. for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { if (!board.state[x][y]) { continue; } let piecekind = "pPnNbBrRqQkK".indexOf(board.state[x][y]); if (piecekind === -1) { continue; } let index = (64 * piecekind) + (8 * (7 - y)) + x; // I mean here. key ^= PolyglotPieceXorVals[index]; } } if (board.castling.includes("H")) key ^= PolyglotCastleXorVals[0]; if (board.castling.includes("A")) key ^= PolyglotCastleXorVals[1]; if (board.castling.includes("h")) key ^= PolyglotCastleXorVals[2]; if (board.castling.includes("a")) key ^= PolyglotCastleXorVals[3]; // Happily, the format's idea of when an en passant square should be included is identical to mine... // "If the opponent has performed a double pawn push and there is now a pawn next to it belonging to the player to move." if (board.enpassant) { key ^= PolyglotEnPassantXorVals[board.enpassant.x]; } if (board.active === "w") { key ^= PolyglotActiveXorVal; } return key; } function ParsePolyglotBlob(buf, off) { // Args are Buffer + offset. if (buf instanceof Buffer === false) { throw "ParsePolyglotBlob() bad book"; } if (off < 0 || off > buf.length - 16) { throw "ParsePolyglotBlob() bad offset"; } // Bytes 0-7 represent the key as a big-endian number. let hi = (buf[off++] * 16777216) + (buf[off++] * 65536) + (buf[off++] * 256) + buf[off++]; let lo = (buf[off++] * 16777216) + (buf[off++] * 65536) + (buf[off++] * 256) + buf[off++]; let key = (BigInt(hi) << 32n) + BigInt(lo); // Bytes 8-9 represent the move as a big-endian bitfield, uh... let move = PolyglotMoveLookup[(buf[off++] * 256) + buf[off++]]; // Bytes 10-11 represent the quality as a big-endian number. let weight = (buf[off++] * 256) + buf[off++]; return {key, move, weight}; } function SortAndDeclutterPGNBook(book) { // book must be an array of objects of form {key, move, weight} if (book instanceof Buffer) { throw "Cannot call SortAndDeclutterPGNBook() on a Buffer."; } if (Array.isArray(book) === false) { throw "SortAndDeclutterPGNBook() bad arg"; } if (book.length === 0) { return; } book.sort((a, b) => { // Sort by key AND move to make deduplication possible. if (a.key < b.key) return -1; if (a.key > b.key) return 1; if (a.move < b.move) return -1; if (a.move > b.move) return 1; return 0; }); // Now we deduplicate the book in place... (algorithm relies on length >= 1) let i = 0; // Slow index let j = 1; // Fast index while (true) { if (j >= book.length) { book.length = i + 1; break; } if (book[i].key === book[j].key && book[i].move === book[j].move) { book[i].weight++; j++; } else { book[i + 1] = book[j]; i++; j++; } } } function BookAtLogicalIndex(book, i) { if (book instanceof Buffer) { return ParsePolyglotBlob(book, i * 16); } else { return book[i]; } } function BookLogicalLength(book) { if (book instanceof Buffer) { return Math.floor(book.length / 16); } else if (Array.isArray(book)) { return book.length; } else { return 0; } } function BookProbe(key, book) { // book is either the raw buffer, or an array of objects of form {key, move, weight} let logical_length = BookLogicalLength(book); // returns 0 on most bad args if (!key || logical_length === 0) { return []; } let hit; let cur; let mid; let lowerbound = 0; let upperbound = logical_length - 1; while (true) { if (lowerbound > upperbound) { console.log("BookProbe(): lowerbound > upperbound"); break; } else { mid = Math.floor((upperbound + lowerbound) / 2); // If upper and lower are neighbours, mid is the left one. cur = BookAtLogicalIndex(book, mid); if (cur.key === key) { hit = cur; break; } if (lowerbound === upperbound) { break; } if (cur.key < key) { lowerbound = mid + 1; // +1 is used here so the neighbours case does change lower. } else { upperbound = mid; // In the neighbours case, upper becomes equal to lower. Can't do -1 or it would go to the left of lower. } continue; } } if (hit === undefined) { return []; } let ret = [hit]; let left = mid; let right = mid; while (left > 0) { cur = BookAtLogicalIndex(book, --left); if (cur.key === key) { ret.unshift(cur); } else { break; } } while (right < logical_length - 1) { cur = BookAtLogicalIndex(book, ++right); if (cur.key === key) { ret.push(cur); } else { break; } } return ret; } function BookSortedTest(book) { // Returns true if a cursory inspection suggests the book is sorted. let logical_length = BookLogicalLength(book); if (logical_length === 0) { return true; } let indices = []; for (let n = 0; n < 100; n++) { indices.push(RandInt(0, logical_length)); } indices.sort((a, b) => { return a - b; }); let check = 0n; for (let index of indices) { let object = BookAtLogicalIndex(book, index); if (object.key < check) { return false; } check = object.key; } return true; } // For debugging........................................................................................................... function HubProbe() { let objects = BookProbe(KeyFromBoard(hub.tree.node.board), hub.book); let ret = []; for (let o of objects) { ret.push({ hex: BigIntToHex(o.key), key: o.key, move: o.move, weight: o.weight, }); } return ret; } function BigIntToHex(big) { let s = big.toString(16); while (s.length < 16) s = "0" + s; return s; } function BookStressTest() { // Given a randomly chosen singleton (position with 1 move), // does BookProbe() actually find it? if (!hub.book) return "Need hub to have a book!"; let trials = 0; let successes = 0; let logical_length = BookLogicalLength(hub.book); for (let n = 0; n < 100000; n++) { let i = RandInt(1, logical_length - 1); // So that's actually a value between 1 and logical_length - 2, inclusive. let left_o = BookAtLogicalIndex(hub.book, i - 1); let mid_o = BookAtLogicalIndex(hub.book, i); let right_o = BookAtLogicalIndex(hub.book, i + 1); if (left_o.key === mid_o.key || right_o.key === mid_o.key) { continue; } trials++; let proberesults = BookProbe(mid_o.key, hub.book); if (proberesults.length === 1) { successes++; } else { console.log("Missed:", mid_o.key); } } return `${trials} trials, ${successes} successes, ${trials - successes} failures.`; } ================================================ FILE: files/src/renderer/65_loaders.js ================================================ "use strict"; // Non-blocking loader objects. // // Implementation rule: The callback property is non-null iff it's still possible that the load will succeed. // If callback === null this implies that shutdown() has already been called at least once. // // Also, every loader starts itself via setTimeout so that the caller can finish whatever it was doing first. // This prevents some weird inconsistency with order-of-events (whether it matters I don't know). // ------------------------------------------------------------------------------------------------------------------------------ function NewFastPGNLoader(foo, callback) { // foo is allowed to be filepath or Buffer if (typeof foo !== "string" && foo instanceof Buffer === false) { throw "NewFastPGNLoader() bad call"; } let loader = Object.create(null); loader.type = "pgn"; loader.starttime = performance.now(); loader.callback = callback; loader.msg = "Loading PGN..."; loader.buf = null; loader.indices = []; loader.off = 0; loader.phase = 1; loader.search = Buffer.from("\n\n["); loader.fix = 2; // Where the [ char will be loader.shutdown = function() { this.callback = null; this.msg = ""; this.buf = null; this.indices = null; }; loader.load = function(foo) { if (this.callback) { if (foo instanceof Buffer) { this.buf = foo; this.continue(); } else { fs.readFile(foo, (err, data) => { if (this.callback) { // Must test again, because this is later. if (err) { let cb = this.callback; cb(err, null); this.shutdown(); } else { this.buf = data; this.continue(); } } }); } } }; loader.continue = function() { if (!this.callback) { return; } if (this.indices.length === 0 && this.buf.length > 0) { this.indices.push(0); } let continuetime = performance.now(); while (true) { let index = this.buf.indexOf(this.search, this.off); if (index === -1) { if (this.phase === 1) { this.phase = 2; this.search = Buffer.from("\n\r\n["); this.fix = 3; this.off = 0; continue; } else { break; } } this.indices.push(index + this.fix); this.off = index + 1; if (this.indices.length % 100 === 0) { if (performance.now() - continuetime > 10) { this.msg = `Loading PGN... ${this.indices.length} games`; setTimeout(() => {this.continue();}, 10); return; } } } // Once, after the while loop is broken... this.indices.sort((a, b) => a - b); let ret = new_pgndata(this.buf, this.indices); let cb = this.callback; cb(null, ret); this.shutdown(); }; setTimeout(() => {loader.load(foo);}, 0); return loader; } // ------------------------------------------------------------------------------------------------------------------------------ function NewPolyglotBookLoader(filename, callback) { let loader = Object.create(null); loader.type = "book"; loader.starttime = performance.now(); loader.callback = callback; loader.msg = "Loading book..."; loader.shutdown = function() { this.callback = null; this.msg = ""; }; loader.load = function(filename) { if (this.callback) { fs.readFile(filename, (err, data) => { if (this.callback) { // Must test again, because this is later. if (err) { let cb = this.callback; cb(err, null); this.shutdown(); } else { let cb = this.callback; cb(null, data); this.shutdown(); } } }); } }; setTimeout(() => {loader.load(filename);}, 0); return loader; } // ------------------------------------------------------------------------------------------------------------------------------ function NewPGNBookLoader(filename, callback) { let loader = Object.create(null); loader.type = "book"; loader.starttime = performance.now(); loader.callback = callback; loader.msg = "Loading book..."; loader.buf = null; loader.book = []; loader.pgndata = null; loader.fastloader = null; loader.n = 0; loader.shutdown = function() { this.callback = null; this.msg = ""; this.buf = null; this.book = null; this.pgndata = null; if (this.fastloader) { this.fastloader.shutdown(); this.fastloader = null; } }; loader.load = function(filename) { if (this.callback) { this.fastloader = NewFastPGNLoader(filename, (err, pgndata) => { if (this.callback) { // Must test again, because this is later. if (err) { let cb = this.callback; cb(err, null); this.shutdown(); } else { this.pgndata = pgndata; this.continue(); } } }); } }; loader.continue = function() { if (!this.callback) { return; } let continuetime = performance.now(); let count = this.pgndata.count(); while (true) { if (this.n >= count) { break; } let o = this.pgndata.getrecord(this.n++); try { let root = LoadPGNRecord(o); // Note that this calls DestroyTree() itself if it must throw. this.book = AddTreeToBook(root, this.book); DestroyTree(root); } catch (err) { // } if (performance.now() - continuetime > 10) { this.msg = `Loading book... ${(100 * (this.n / count)).toFixed(0)}%`; setTimeout(() => {this.continue();}, 10); return; } } // Once, after the while loop is broken... SortAndDeclutterPGNBook(this.book); let ret = this.book; // Just in case I ever replace the direct cb() with a setTimeout (shutdown would cause this.book to be null). let cb = this.callback; cb(null, ret); this.shutdown(); }; setTimeout(() => {loader.load(filename);}, 0); return loader; } ================================================ FILE: files/src/renderer/71_tree_handler.js ================================================ "use strict"; // The point is that updating the node should trigger an immediate redraw. The caller doesn't need // to care about redrawing. Ideally, this object should be able to make good decisions about how // to best redraw. function NewTreeHandler() { let handler = Object.create(null); Object.assign(handler, tree_manipulation_props); Object.assign(handler, tree_draw_props); handler.root = NewRoot(); handler.node = handler.root; handler.node.table.autopopulate(handler.node); return handler; } let tree_manipulation_props = { // Since we use Object.assign(), it's bad form to have any deep objects in the props. tree_version: 0, // Increment every time the tree structure changes. root: null, node: null, // Where relevant, return values of the methods are whether this.node changed - // i.e. whether the hub has to call position_changed() replace_tree: function(root) { DestroyTree(this.root); this.root = root; this.node = root; this.node.table.autopopulate(this.node); this.tree_version++; this.dom_from_scratch(); return true; }, set_node: function(node) { // Note that we may call dom_easy_highlight_change() so don't // rely on this to draw any nodes that never got drawn. if (!node || node === this.node || node.destroyed) { return false; } let original_node = this.node; this.node = node; if (original_node.is_same_line(this.node)) { // This test is super-fast if one node is a parent of the other this.dom_easy_highlight_change(); } else { this.dom_from_scratch(); } return true; }, prev: function() { return this.set_node(this.node.parent); // OK if undefined }, next: function() { return this.set_node(this.node.children[0]); // OK if undefined }, goto_root: function() { return this.set_node(this.root); }, goto_end: function() { return this.set_node(this.node.get_end()); }, previous_sibling: function() { if (!this.node.parent || this.node.parent.children.length < 2) { return false; } if (this.node.parent.children[0] === this.node) { return this.set_node(this.node.parent.children[this.node.parent.children.length - 1]); } for (let i = this.node.parent.children.length - 1; i > 0; i--) { if (this.node.parent.children[i] === this.node) { return this.set_node(this.node.parent.children[i - 1]); } } return false; // Can't get here. }, next_sibling: function() { if (!this.node.parent || this.node.parent.children.length < 2) { return false; } if (this.node.parent.children[this.node.parent.children.length - 1] === this.node) { return this.set_node(this.node.parent.children[0]); } for (let i = 0; i < this.node.parent.children.length - 1; i++) { if (this.node.parent.children[i] === this.node) { return this.set_node(this.node.parent.children[i + 1]); } } return false; // Can't get here. }, return_to_main_line: function() { let node = this.node.return_to_main_line_helper(); if (this.node === node) { return false; } this.node = node; this.dom_from_scratch(); return true; }, delete_node: function() { if (!this.node.parent) { this.delete_children(); return false; } let parent = this.node.parent; this.node.detach(); this.node = parent; this.tree_version++; this.dom_from_scratch(); return true; }, make_move: function(s) { // s must be exactly a legal move, including having promotion char iff needed (e.g. e2e1q) let next_node_id__initial = next_node_id; this.node = this.node.make_move(s); if (next_node_id !== next_node_id__initial) { // NewNode() was called this.tree_version++; } this.dom_from_scratch(); // Could potentially call something else here. return true; }, make_move_sequence: function(moves, set_this_node = true) { if (Array.isArray(moves) === false || moves.length === 0) { return false; } let next_node_id__initial = next_node_id; let node = this.node; for (let s of moves) { node = node.make_move(s); // Calling the node's make_move() method, not handler's } if (set_this_node) { this.node = node; } if (next_node_id !== next_node_id__initial) { // NewNode() was called this.tree_version++; } this.dom_from_scratch(); return true; }, add_move_sequence: function(moves) { return this.make_move_sequence(moves, false); }, // ------------------------------------------------------------------------------------------------------------- // The following methods don't ever change this.node - so the caller has no action to take. No return value. promote_to_main_line: function() { let node = this.node; let changed = false; while (node.parent) { if (node.parent.children[0] !== node) { for (let n = 1; n < node.parent.children.length; n++) { if (node.parent.children[n] === node) { node.parent.children[n] = node.parent.children[0]; node.parent.children[0] = node; changed = true; break; } } } node = node.parent; } if (changed) { this.tree_version++; this.dom_from_scratch(); } }, promote: function() { let node = this.node; let changed = false; while (node.parent) { if (node.parent.children[0] !== node) { for (let n = 1; n < node.parent.children.length; n++) { if (node.parent.children[n] === node) { let swapper = node.parent.children[n - 1]; node.parent.children[n - 1] = node; node.parent.children[n] = swapper; changed = true; break; } } break; // 1 tree change only } node = node.parent; } if (changed) { this.tree_version++; this.dom_from_scratch(); } }, delete_other_lines: function() { this.promote_to_main_line(); let changed = false; let node = this.root; while (node.children.length > 0) { for (let child of node.children.slice(1)) { child.detach(); changed = true; } node = node.children[0]; } if (changed) { this.tree_version++; this.dom_from_scratch(); // This may be the 2nd draw since promote_to_main_line() may have drawn. Bah. } }, delete_children: function() { if (this.node.children.length > 0) { for (let child of this.node.children) { child.detach(); } this.tree_version++; this.dom_from_scratch(); } }, delete_siblings: function() { let changed = false; if (this.node.parent) { for (let sibling of this.node.parent.children) { if (sibling !== this.node) { sibling.detach(); changed = true; } } } if (changed) { this.tree_version++; this.dom_from_scratch(); } }, // ------------------------------------------------------------------------------------------------------------- handle_click: function(event) { let n = EventPathN(event, "node_"); if (typeof n !== "number") { return false; } let node = live_nodes[n.toString()]; if (!node || node.destroyed) { // Probably the check for .destroyed is unnecessary. return false; } return this.set_node(node); }, }; ================================================ FILE: files/src/renderer/72_tree_draw.js ================================================ "use strict"; let tree_draw_props = { // Since we use Object.assign(), it's bad form to have any deep objects in the props. ordered_nodes_cache: null, ordered_nodes_cache_version: -1, dom_easy_highlight_change: function() { // When the previously highlighted node and the newly highlighted node are on the same line, // with the same end-of-line, meaning no gray / white changes are needed. let dom_highlight = this.get_movelist_highlight(); let highlight_class; if (dom_highlight && dom_highlight.classList.contains("movelist_highlight_yellow")) { highlight_class = "movelist_highlight_yellow"; } else { highlight_class = "movelist_highlight_blue"; } if (dom_highlight) { dom_highlight.classList.remove("movelist_highlight_blue"); dom_highlight.classList.remove("movelist_highlight_yellow"); } let dom_node = document.getElementById(`node_${this.node.id}`); if (dom_node) { dom_node.classList.add(highlight_class); } this.fix_scrollbar_position(); }, dom_from_scratch: function() { // Some prep-work (we need to undo all this at the end)... let line_end = this.node.get_end(); let foo = line_end; while (foo) { foo.current_line = true; // These nodes will be coloured white, others gray foo = foo.parent; } let main_line_end = this.root.get_end(); main_line_end.main_line_end = true; // Begin... if (this.ordered_nodes_cache_version !== this.tree_version) { this.ordered_nodes_cache = get_ordered_nodes(this.root); this.ordered_nodes_cache_version = this.tree_version; } let pseudoelements = []; // Objects containing opening span string `` and text string for (let item of this.ordered_nodes_cache) { if (item === this.root) { continue; } // As a crude hack, the item can be a bracket string. // Deal with that first... if (typeof item === "string") { pseudoelements.push({ opener: "", text: item, closer: "" }); continue; } // So item is a real node... let node = item; let classes = []; if (node === this.node) { if (node.is_main_line()) { classes.push("movelist_highlight_blue"); } else { classes.push("movelist_highlight_yellow"); } } if (node.current_line) { classes.push("white"); // Otherwise, inherits gray colour from movelist CSS } pseudoelements.push({ opener: ``, text: node.token(), closer: `` }); } let all_spans = []; for (let n = 0; n < pseudoelements.length; n++) { let p = pseudoelements[n]; let nextp = pseudoelements[n + 1]; // Possibly undefined if (!nextp || (p.text !== "(" && nextp.text !== ")")) { p.text += " "; } all_spans.push(`${p.opener}${p.text}${p.closer}`); } movelist.innerHTML = all_spans.join(""); // Undo the damage to our tree from the start... foo = line_end; while(foo) { delete foo.current_line; foo = foo.parent; } delete main_line_end.main_line_end; // And finally... this.fix_scrollbar_position(); }, // Helpers... get_movelist_highlight: function() { let elements = document.getElementsByClassName("movelist_highlight_blue"); if (elements && elements.length > 0) { return elements[0]; } elements = document.getElementsByClassName("movelist_highlight_yellow"); if (elements && elements.length > 0) { return elements[0]; } return null; }, fix_scrollbar_position: function() { let highlight = this.get_movelist_highlight(); if (highlight) { let top = highlight.offsetTop - movelist.offsetTop; if (top < movelist.scrollTop) { movelist.scrollTop = top; } let bottom = top + highlight.offsetHeight; if (bottom > movelist.scrollTop + movelist.offsetHeight) { movelist.scrollTop = bottom - movelist.offsetHeight; } } else { movelist.scrollTop = 0; } }, }; ================================================ FILE: files/src/renderer/75_looker.js ================================================ "use strict"; // Rate limit strategy - thanks to Sopel: // // .running holds the item in-flight. // .pending holds a single item to send after. // // Note: Don't store the retrieved info in the node.table, because the logic // there is already a bit convoluted with __touched, __ghost and whatnot (sadly). // // Note: format of entries in the DB is {type: "foo", moves: {}} // where moves is a map of string --> object function NewLooker() { let looker = Object.create(null); looker.running = null; looker.pending = null; looker.all_dbs = Object.create(null); looker.bans = Object.create(null); // db --> time of last rate-limit Object.assign(looker, looker_props); return looker; } let looker_props = { clear_queue: function() { this.running = null; this.pending = null; }, add_to_queue: function(board) { if (!config.looker_api || !board.normalchess) { return; } if (!config.look_past_25 && board.fullmove > 25) { return; } // Is there a reason the test for whether we've already looked up this // position isn't done here, but is done later at query_api()? I forget. let query = { // Since queries are objects, different queries can always be told apart. board: board, db_name: config.looker_api }; if (!this.running) { this.send_query(query); } else { this.pending = query; } }, send_query: function(query) { this.running = query; // It is ESSENTIAL that every call to send_query() eventually generates a call to query_complete() // so that the item gets removed from the queue. While we don't really need to use promises, doing // it as follows lets me just have a single place where query_complete() is called. I guess. this.query_api(query).catch(error => { console.log("Query failed:", error); }).finally(() => { this.query_complete(query); }); }, query_complete: function(query) { if (this.running !== query) { // Possible if clear_queue() was called. return; } let next_query = this.pending; this.running = null; this.pending = null; if (next_query) { this.send_query(next_query); } }, get_db: function(db_name) { // Creates it if needed. if (typeof db_name !== "string") { return null; } if (!this.all_dbs[db_name]) { this.all_dbs[db_name] = Object.create(null); } return this.all_dbs[db_name]; }, new_entry: function(db_name, board) { // Creates a new (empty) entry in the database (to be populated elsewhere) and returns it. let entry = { type: db_name, moves: {}, }; let db = this.get_db(db_name); db[board.fen()] = entry; return entry; }, lookup: function(db_name, board) { // Return the full entry for a position. When repeatedly called with the same params, this should // return the same object (unless it changes of course). Returns null if not available. let db = this.get_db(db_name); if (db) { // Remember get_db() can return null. let ret = db[board.fen()]; if (ret) { return ret; } } return null; // I guess we tend to like null over undefined. (Bad habit?) }, set_ban: function(db_name) { this.bans[db_name] = performance.now(); }, query_api(query) { // Returns a promise, which is solely used by the caller to attach some cleanup catch/finally() if (this.lookup(query.db_name, query.board)) { // We already have a result for this board. return Promise.resolve(); // Consider this case a satisfactory result. } if (this.bans[query.db_name]) { if (performance.now() - this.bans[query.db_name] < 60000) { // No requests within 1 minute of the ban. return Promise.resolve(); // Consider this case a satisfactory result. } } let friendly_fen = query.board.fen(true); let fen_for_web = ReplaceAll(friendly_fen, " ", "%20"); let url; if (query.db_name === "chessdbcn") { url = `https://www.chessdb.cn/cdb.php?action=queryall&json=1&board=${fen_for_web}`; } else if (query.db_name === "lichess_masters") { url = `https://explorer.lichess.org/masters?topGames=0&fen=${fen_for_web}`; } else if (query.db_name === "lichess_plebs") { url = `https://explorer.lichess.org/lichess?variant=standard&topGames=0&recentGames=0&fen=${fen_for_web}`; } else { return Promise.reject(new Error("Bad db_name")); } let fetch_options = { headers: {"User-Agent": "Nibbler"} }; if ((query.db_name === "lichess_masters" || query.db_name === "lichess_plebs") && config.lichess_token) { fetch_options.headers["Authorization"] = `Bearer ${config.lichess_token}`; } return fetch(url, fetch_options).then(response => { if (response.status === 429) { // rate limit hit this.set_ban(query.db_name); hub.set_special_message("429 Too Many Requests", "red", 5000); // relies on hub being in script/global scope, which it is throw new Error("rate limited"); } if (!response.ok) { // ok means status in range 200-299 throw new Error("response.ok was false"); } return response.json(); }).then(raw_object => { this.handle_response_object(query, raw_object); }); }, handle_response_object: function(query, raw_object) { let board = query.board; let o = this.new_entry(query.db_name, board); // If the raw_object is invalid, now's the time to return - after the empty object // has been stored in the database, so we don't do this lookup again. if (typeof raw_object !== "object" || raw_object === null || Array.isArray(raw_object.moves) === false) { return; // This can happen e.g. if the position is checkmate. } // Our Lichess moves need to know the total number of games so they can return valid stats. // While the total is available as raw_object.white + raw_object.black + raw_object.draws, // it's probably better to sum up the items that we're given. let lichess_position_total = 0; if (query.db_name === "lichess_masters" || query.db_name === "lichess_plebs") { for (let raw_item of raw_object.moves) { lichess_position_total += raw_item.white + raw_item.black + raw_item.draws; } } // Now add moves to the entry... for (let raw_item of raw_object.moves) { let move = raw_item.uci; move = board.c960_castling_converter(move); if (query.db_name === "chessdbcn") { o.moves[move] = new_chessdbcn_move(board, raw_item); } else if (query.db_name === "lichess_masters" || query.db_name === "lichess_plebs") { o.moves[move] = new_lichess_move(board, raw_item, lichess_position_total); } } // Note that even if we get no info, we still leave the empty object o in the database, // and this allows us to know that we've done this search already. }, }; // Below are some functions which use the info a server sends about a single move to create our // own object containing just what we need (and with a prototype containing some useful methods). function new_chessdbcn_move(board, raw_item) { // The object with info about a single move in a chessdbcn object. let ret = Object.create(chessdbcn_move_props); ret.active = board.active; ret.score = raw_item.score / 100; return ret; } let chessdbcn_move_props = { text: function(pov) { // pov can be null for current let score = this.score; if ((pov === "w" && this.active === "b") || (pov === "b" && this.active === "w")) { score = 0 - this.score; } let s = score.toFixed(2); if (s !== "0.00" && s[0] !== "-") { s = "+" + s; } return `API: ${s}`; }, sort_score: function() { return this.score; }, }; function new_lichess_move(board, raw_item, position_total) { // The object with info about a single move in a lichess object. let ret = Object.create(lichess_move_props); ret.active = board.active; ret.white = raw_item.white; ret.black = raw_item.black; ret.draws = raw_item.draws; ret.total = raw_item.white + raw_item.draws + raw_item.black; ret.position_total = position_total; return ret; } let lichess_move_props = { text: function(pov) { // pov can be null for current let actual_pov = pov ? pov : this.active; let wins = actual_pov === "w" ? this.white : this.black; let ev = (wins + (this.draws / 2)) / this.total; let win_string = (ev * 100).toFixed(1); let weight_string = (100 * this.total / this.position_total).toFixed(0); return `API win: ${win_string}% freq: ${weight_string}% [${NString(this.total)}]`; }, sort_score: function() { return this.total; }, }; ================================================ FILE: files/src/renderer/80_info.js ================================================ "use strict"; function NewInfoHandler() { let ih = Object.create(null); Object.assign(ih, info_misc_props); Object.assign(ih, info_receiver_props); Object.assign(ih, arrow_props); Object.assign(ih, infobox_props); // Array of possible one-click moves. Updated by draw_arrows(). Used elsewhere. ih.one_click_moves = New2DArray(8, 8, null); // Clickable elements in the infobox. Updated by draw_infobox(). Used elsewhere. ih.info_clickers = []; ih.info_clickers_node_id = null; // Infobox stuff, used solely to skip redraws... ih.last_drawn_node_id = null; ih.last_drawn_version = null; ih.last_drawn_highlight = null; ih.last_drawn_highlight_class = null; ih.last_drawn_length = 0; ih.last_drawn_searchmoves = []; ih.last_drawn_allow_inactive_focus = null; ih.last_drawn_lookup_object = null; // Info about engine cycles. These aren't reset even when the engine resets. ih.engine_cycle = 0; // Count of "go" commands emitted. Since Engine can change, can't store this in Engine objects ih.engine_subcycle = 0; // Count of how many times we have seen "multipv 1" - each time it's a new "block" of info ih.ever_updated_a_table = false; // Info about the current engine... // Note that, when the engine is restarted, hub must call reset_engine_info() to fix these. A bit lame. ih.engine_start_time = performance.now(); ih.engine_sent_info = false; ih.engine_sent_q = false; ih.engine_sent_errors = false; ih.error_time = 0; ih.error_log = ""; ih.next_vms_order_int = 1; return ih; } let info_misc_props = { reset_engine_info: function() { this.engine_start_time = performance.now(); this.engine_sent_info = false; this.engine_sent_q = false; this.engine_sent_errors = false; this.error_time = 0; this.error_log = ""; this.next_vms_order_int = 1; }, displaying_error_log: function() { // Recent error... if (this.engine_sent_errors && performance.now() - this.error_time < 10000) { return true; } // Engine hasn't yet sent info, and was recently started... if (!this.engine_sent_info) { if (performance.now() - this.engine_start_time < 5000) { return true; } } // We have never updated a table (meaning we never received useful info from an engine) // and we aren't displaying API info... if (!this.ever_updated_a_table && !config.looker_api) { return true; } return false; }, }; let info_receiver_props = { err_receive: function(s) { if (typeof s !== "string") { return; } if (this.error_log.length > 50000) { return; } let s_low = s.toLowerCase(); if (s_low.includes("warning") || s_low.includes("error") || s_low.includes("unknown") || s_low.includes("failed") || s_low.includes("exception")) { this.engine_sent_errors = true; this.error_log += `${s}
`; this.error_time = performance.now(); } else { this.error_log += `${s}
`; } }, receive: function(engine, search, s) { let node = search.node; if (typeof s !== "string" || !node || node.destroyed) { return; } let board = node.board; if (s.startsWith("info") && s.includes(" pv ") && ((!s.includes("lowerbound") && !s.includes("upperbound")) || config.accept_bounds)) { if (config.log_info_lines) Log("< " + s); // info depth 8 seldepth 31 time 3029 nodes 23672 score cp 27 wdl 384 326 290 nps 7843 tbhits 0 multipv 1 // pv d2d4 g8f6 c2c4 e7e6 g1f3 d7d5 b1c3 f8b4 c1g5 d5c4 e2e4 c7c5 f1c4 h7h6 g5f6 d8f6 e1h1 c5d4 e4e5 f6d8 c3e4 let infovals = InfoValMany(s, ["pv", "cp", "mate", "multipv", "nodes", "nps", "time", "depth", "seldepth", "tbhits"]); let tmp; let move_info; let move = infovals["pv"]; move = board.c960_castling_converter(move); if (node.table.moveinfo[move] && !node.table.moveinfo[move].__ghost) { // We already have move info for this move. move_info = node.table.moveinfo[move]; } else { // We don't. if (board.illegal(move)) { if (config.log_illegal_moves) { Log(`INVALID / ILLEGAL MOVE RECEIVED: ${move}`); } return; } move_info = NewInfo(board, move); node.table.moveinfo[move] = move_info; } let move_cycle_pre_update = move_info.cycle; let move_depth_pre_update = move_info.depth; // --------------------------------------------------------------------------------------------------------------------- if (!engine.leelaish) { move_info.clear_stats(); // The stats we get this way are all that the engine has, so clear everything. } move_info.leelaish = engine.leelaish; this.engine_sent_info = true; // After the move legality check; i.e. we want REAL info this.ever_updated_a_table = true; node.table.version++; node.table.limit = search.limit; move_info.cycle = this.engine_cycle; move_info.__touched = true; // --------------------------------------------------------------------------------------------------------------------- let did_set_q_from_mate = false; tmp = parseInt(infovals["cp"], 10); if (Number.isNaN(tmp) === false) { move_info.cp = tmp; if (this.engine_sent_q === false) { move_info.q = QfromPawns(tmp / 100); // Potentially overwritten later by the better QfromWDL() } move_info.mate = 0; // Engines will send one of cp or mate, so mate gets reset when receiving cp } tmp = parseInt(infovals["mate"], 10); if (Number.isNaN(tmp) === false) { move_info.mate = tmp; if (tmp !== 0) { move_info.q = tmp > 0 ? 1 : -1; move_info.cp = tmp > 0 ? 32000 : -32000; did_set_q_from_mate = true; } } tmp = parseInt(infovals["multipv"], 10); if (Number.isNaN(tmp) === false) { move_info.multipv = tmp; if (tmp === 1) { this.engine_subcycle++; } } else { this.engine_subcycle++; } move_info.subcycle = this.engine_subcycle; tmp = parseInt(infovals["nodes"], 10); if (Number.isNaN(tmp) === false) { move_info.uci_nodes = tmp; node.table.nodes = tmp; } tmp = parseInt(infovals["nps"], 10); if (Number.isNaN(tmp) === false) { node.table.nps = tmp; // Note this is stored in the node.table, not the move_info } tmp = parseInt(infovals["time"], 10); if (Number.isNaN(tmp) === false) { node.table.time = tmp; // Note this is stored in the node.table, not the move_info } tmp = parseInt(infovals["tbhits"], 10); if (Number.isNaN(tmp) === false) { node.table.tbhits = tmp; // Note this is stored in the node.table, not the move_info } tmp = parseInt(infovals["depth"], 10); if (Number.isNaN(tmp) === false) { move_info.depth = tmp; } tmp = parseInt(infovals["seldepth"], 10); if (Number.isNaN(tmp) === false) { move_info.seldepth = tmp; } move_info.wdl = InfoWDL(s); if (this.engine_sent_q === false && !did_set_q_from_mate && Array.isArray(move_info.wdl)) { move_info.q = QfromWDL(move_info.wdl); } // If the engine isn't respecting Chess960 castling format, the PV // may contain old-fashioned castling moves... let new_pv = InfoPV(s); C960_PV_Converter(new_pv, board); if (CompareArrays(new_pv, move_info.pv) === false) { if (!board.sequence_illegal(new_pv)) { if (move_cycle_pre_update === move_info.cycle && ArrayStartsWith(move_info.pv, new_pv) && move_depth_pre_update >= move_info.depth - 1 ) { // Skip the update. This partially mitigates Stockfish sending unresolved PVs. // We don't skip the update if the old PV is too old - issue noticed by Nagisa. } else { move_info.set_pv(new_pv); } } else { move_info.set_pv([move]); } } } else if (s.startsWith("info string") && !s.includes("NNUE evaluation")) { if (config.log_info_lines) Log("< " + s); // info string d2d4 (293 ) N: 12005 (+169) (P: 22.38%) (WL: 0.09480) (D: 0.326) // (M: 7.4) (Q: 0.09480) (U: 0.01211) (Q+U: 0.10691) (V: 0.0898) // Ceres has been known to send these in Euro decimal format e.g. Q: 0,094 // We'll have to replace all commas... s = ReplaceAll(s, ",", "."); let infovals = InfoValMany(s, ["string", "N:", "(D:", "(U:", "(Q+U:", "(S:", "(P:", "(Q:", "(V:", "(M:"]); let tmp; let move_info; let move = infovals["string"]; if (move === "node") { // Mostly ignore these lines, but... this.next_vms_order_int = 1; // ...use them to note that the VerboseMoveStats have completed. A bit sketchy? tmp = parseInt(infovals["N:"], 10); if (Number.isNaN(tmp) === false) { node.table.nodes = tmp; // ...and use this line to ensure a valid nodes count for the table. (Mostly helps with Ceres.) } return; } move = board.c960_castling_converter(move); if (node.table.moveinfo[move] && !node.table.moveinfo[move].__ghost) { // We already have move info for this move. move_info = node.table.moveinfo[move]; } else { // We don't. if (board.illegal(move)) { if (config.log_illegal_moves) { Log(`INVALID / ILLEGAL MOVE RECEIVED: ${move}`); } return; } move_info = NewInfo(board, move); node.table.moveinfo[move] = move_info; } // --------------------------------------------------------------------------------------------------------------------- engine.leelaish = true; // Note this isn't the main way engine.leelaish gets set (because reasons) move_info.leelaish = true; this.engine_sent_info = true; // After the move legality check; i.e. we want REAL info this.ever_updated_a_table = true; node.table.version++; node.table.limit = search.limit; // move_info.cycle = this.engine_cycle; // No... we get VMS lines even when excluded by searchmoves. // move_info.subcycle = this.engine_subcycle; move_info.__touched = true; // --------------------------------------------------------------------------------------------------------------------- move_info.vms_order = this.next_vms_order_int++; tmp = parseInt(infovals["N:"], 10); if (Number.isNaN(tmp) === false) { move_info.n = tmp; } tmp = parseFloat(infovals["(U:"]); if (Number.isNaN(tmp) === false) { move_info.u = tmp; } tmp = parseFloat(infovals["(Q+U:"]); // Q+U, old name for S if (Number.isNaN(tmp) === false) { move_info.s = tmp; } tmp = parseFloat(infovals["(S:"]); if (Number.isNaN(tmp) === false) { move_info.s = tmp; } tmp = parseFloat(infovals["(P:"]); // P, parseFloat will ignore the trailing % if (Number.isNaN(tmp) === false) { move_info.p = tmp; } tmp = parseFloat(infovals["(Q:"]); if (Number.isNaN(tmp) === false) { this.engine_sent_q = true; move_info.q = tmp; } tmp = parseFloat(infovals["(V:"]); if (Number.isNaN(tmp) === false) { move_info.v = tmp; } else { move_info.v = null; // V sometimes is -.----- (we used to not do anything here, preserving any old (but confusing) value) } tmp = parseFloat(infovals["(M:"]); if (Number.isNaN(tmp) === false) { move_info.m = tmp; } else { move_info.m = null; // M sometimes is -.----- (we used to not do anything here, preserving any old (but confusing) value) } } else if (s.startsWith("info") && s.includes(" pv ") && (s.includes("lowerbound") || s.includes("upperbound"))) { if (config.log_info_lines) Log("< " + s); let infovals = InfoValMany(s, ["pv", "multipv"]); let tmp; let move_info; let move = infovals["pv"]; move = board.c960_castling_converter(move); if (node.table.moveinfo[move] && !node.table.moveinfo[move].__ghost) { // We already have move info for this move. move_info = node.table.moveinfo[move]; } if (move_info) { tmp = parseInt(infovals["multipv"], 10); if (Number.isNaN(tmp) === false) { move_info.multipv = tmp; move_info.subcycle = this.engine_subcycle; } } } else { if (config.log_info_lines && config.log_useless_info) Log("< " + s); } }, }; ================================================ FILE: files/src/renderer/81_arrows.js ================================================ "use strict"; let arrow_props = { draw_arrows: function(node, specific_source, show_move) { // If not nullish, specific_source is a Point() and show_move is a string // Function is responsible for updating the one_click_moves array. for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { this.one_click_moves[x][y] = null; } } if (!config.arrows_enabled || !node || node.destroyed) { return; } let full_list = SortedMoveInfo(node); if (full_list.length === 0) { // Keep this test early so we can assume full_list[0] exists later. return; } let best_info = full_list[0]; // Note that, since we may filter the list, it might not contain best_info later. let info_list = []; let arrows = []; let heads = []; let mode; let show_move_was_forced = false; // Will become true if the show_move is only in the list because of the show_move arg let show_move_head = null; if (specific_source) { mode = "specific"; } else if (full_list[0].__ghost) { mode = "ghost"; } else if (full_list[0].__touched === false) { mode = "untouched"; } else if (full_list[0].leelaish === false) { mode = "ab"; } else { mode = "normal"; } switch (mode) { case "normal": info_list = full_list; break; case "ab": for (let info of full_list) { if (info.__touched && info.subcycle >= full_list[0].subcycle) { info_list.push(info); } else if (info.move === show_move) { info_list.push(info); show_move_was_forced = true; } } break; case "ghost": for (let info of full_list) { if (info.__ghost) { info_list.push(info); } else if (info.move === show_move) { info_list.push(info); show_move_was_forced = true; } } break; case "untouched": for (let info of full_list) { if (info.move === show_move) { info_list.push(info); show_move_was_forced = true; } } break; case "specific": for (let info of full_list) { if (info.move.slice(0, 2) === specific_source.s) { info_list.push(info); } } break; } // ------------------------------------------------------------------------------------------------------------ for (let i = 0; i < info_list.length; i++) { let loss = 0; if (typeof best_info.q === "number" && typeof info_list[i].q === "number") { loss = best_info.value() - info_list[i].value(); } let ok = true; // Filter for normal (Leelaish) mode... if (mode === "normal") { if (config.arrow_filter_type === "top") { if (i !== 0) { ok = false; } } if (config.arrow_filter_type === "N") { if (typeof info_list[i].n !== "number" || info_list[i].n === 0) { ok = false; } else { let n_fraction = info_list[i].n / node.table.nodes; if (n_fraction < config.arrow_filter_value) { ok = false; } } } // Moves proven to lose... if (typeof info_list[i].u === "number" && info_list[i].u === 0 && info_list[i].value() === 0) { if (config.arrow_filter_type !== "all") { ok = false; } } // If the show_move would be filtered out, note that fact... if (!ok && info_list[i].move === show_move) { show_move_was_forced = true; } } // Filter for ab mode... // Note that we don't set show_move_was_forced for ab mode. // If it wasn't already set, then we have good info for this move. if (mode === "ab") { if (loss >= config.ab_filter_threshold) { ok = false; } } // Go ahead, if the various tests don't filter the move out... if (ok || i === 0 || info_list[i].move === show_move) { let [x1, y1] = XY(info_list[i].move.slice(0, 2)); let [x2, y2] = XY(info_list[i].move.slice(2, 4)); let colour; if (info_list[i].move === show_move && config.next_move_unique_colour) { colour = config.actual_move_colour; } else if (info_list[i].move === show_move && show_move_was_forced) { colour = config.terrible_colour; } else if (info_list[i].__touched === false) { colour = config.terrible_colour; } else if (info_list[i] === best_info) { colour = config.best_colour; } else if (loss < config.bad_move_threshold) { colour = config.good_colour; } else if (loss < config.terrible_move_threshold) { colour = config.bad_colour; } else { colour = config.terrible_colour; } let x_head_adjustment = 0; // Adjust head of arrow for castling moves... let normal_castling_flag = false; if (node.board && node.board.colour(Point(x1, y1)) === node.board.colour(Point(x2, y2))) { // So the move is a castling move (reminder: as of 1.1.6 castling format is king-onto-rook). if (node.board.normalchess) { normal_castling_flag = true; // ...and we are playing normal Chess (not 960). } if (x2 > x1) { x_head_adjustment = normal_castling_flag ? -1 : -0.5; } else { x_head_adjustment = normal_castling_flag ? 2 : 0.5; } } arrows.push({ colour: colour, x1: x1, y1: y1, x2: x2 + x_head_adjustment, y2: y2, info: info_list[i] }); // If there is no one_click_move set for the target square, then set it // and also set an arrowhead to be drawn later. if (normal_castling_flag) { if (!this.one_click_moves[x2 + x_head_adjustment][y2]) { heads.push({ colour: colour, x2: x2 + x_head_adjustment, y2: y2, info: info_list[i] }); this.one_click_moves[x2 + x_head_adjustment][y2] = info_list[i].move; if (info_list[i].move === show_move) { show_move_head = heads[heads.length - 1]; } } } else { if (!this.one_click_moves[x2][y2]) { heads.push({ colour: colour, x2: x2 + x_head_adjustment, y2: y2, info: info_list[i] }); this.one_click_moves[x2][y2] = info_list[i].move; if (info_list[i].move === show_move) { show_move_head = heads[heads.length - 1]; } } } } } // It looks best if the longest arrows are drawn underneath. Manhattan distance is good enough. // For the sake of displaying the best pawn promotion (of the 4 possible), sort ties are broken // by node counts, with lower drawn first. [Eh, what about Stockfish? Meh, it doesn't affect // the heads, merely the colour of the lines, so it's not a huge problem I think.] arrows.sort((a, b) => { if (Math.abs(a.x2 - a.x1) + Math.abs(a.y2 - a.y1) < Math.abs(b.x2 - b.x1) + Math.abs(b.y2 - b.y1)) { return 1; } if (Math.abs(a.x2 - a.x1) + Math.abs(a.y2 - a.y1) > Math.abs(b.x2 - b.x1) + Math.abs(b.y2 - b.y1)) { return -1; } if (a.info.n < b.info.n) { return -1; } if (a.info.n > b.info.n) { return 1; } return 0; }); boardctx.lineWidth = config.arrow_width; boardctx.textAlign = "center"; boardctx.textBaseline = "middle"; boardctx.font = config.board_font; for (let o of arrows) { let cc1 = CanvasCoords(o.x1, o.y1); let cc2 = CanvasCoords(o.x2, o.y2); if (o.info.move === show_move && config.next_move_outline) { // Draw the outline at the layer just below the actual arrow. boardctx.strokeStyle = "black"; boardctx.fillStyle = "black"; boardctx.lineWidth = config.arrow_width + 4; boardctx.beginPath(); boardctx.moveTo(cc1.cx, cc1.cy); boardctx.lineTo(cc2.cx, cc2.cy); boardctx.stroke(); boardctx.lineWidth = config.arrow_width; if (show_move_head) { // This is the best layer to draw the head outline. boardctx.beginPath(); boardctx.arc(cc2.cx, cc2.cy, config.arrowhead_radius + 2, 0, 2 * Math.PI); boardctx.fill(); } } boardctx.strokeStyle = o.colour; boardctx.fillStyle = o.colour; boardctx.beginPath(); boardctx.moveTo(cc1.cx, cc1.cy); boardctx.lineTo(cc2.cx, cc2.cy); boardctx.stroke(); } for (let o of heads) { let cc2 = CanvasCoords(o.x2, o.y2); boardctx.fillStyle = o.colour; boardctx.beginPath(); boardctx.arc(cc2.cx, cc2.cy, config.arrowhead_radius, 0, 2 * Math.PI); boardctx.fill(); boardctx.fillStyle = "black"; let s = "?"; switch (config.arrowhead_type) { case 0: s = o.info.value_string(0, config.ev_pov); if (s === "100" && o.info.q < 1.0) { s = "99"; // Don't round up to 100. } break; case 1: if (node.table.nodes > 0) { s = (100 * o.info.n / node.table.nodes).toFixed(0); } break; case 2: if (o.info.p > 0) { s = o.info.p.toFixed(0); } break; case 3: s = o.info.multipv; break; case 4: if (typeof o.info.m === "number") { s = o.info.m.toFixed(0); } break; default: s = "!"; break; } if (o.info.__touched === false) { s = "?"; } if (show_move_was_forced && o.info.move === show_move) { s = "?"; } boardctx.fillText(s, cc2.cx, cc2.cy + 1); } draw_arrows_last_mode = mode; // For debugging only. }, // ---------------------------------------------------------------------------------------------------------- // We have a special function for the book explorer mode. Explorer mode is very nicely isolated from the rest // of the app. The info_list here is just a list of objects each containing only "move" and "weight" - where // the weights have been normalised to the 0-1 scale and the list has been sorted. // // Note that info_list here MUST NOT BE MODIFIED. draw_explorer_arrows: function(node, info_list, specific_source) { // If not nullish, specific_source is a Point() for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { this.one_click_moves[x][y] = null; } } if (!node || node.destroyed) { return; } let arrows = []; let heads = []; for (let i = 0; i < info_list.length; i++) { if (specific_source && specific_source.s !== info_list[i].move.slice(0, 2)) { continue; } let [x1, y1] = XY(info_list[i].move.slice(0, 2)); let [x2, y2] = XY(info_list[i].move.slice(2, 4)); let colour = i === 0 ? config.best_colour : config.good_colour; let x_head_adjustment = 0; // Adjust head of arrow for castling moves... let normal_castling_flag = false; if (node.board && node.board.colour(Point(x1, y1)) === node.board.colour(Point(x2, y2))) { if (node.board.normalchess) { normal_castling_flag = true; // ...and we are playing normal Chess (not 960). } if (x2 > x1) { x_head_adjustment = normal_castling_flag ? -1 : -0.5; } else { x_head_adjustment = normal_castling_flag ? 2 : 0.5; } } arrows.push({ colour: colour, x1: x1, y1: y1, x2: x2 + x_head_adjustment, y2: y2, info: info_list[i] }); // If there is no one_click_move set for the target square, then set it // and also set an arrowhead to be drawn later. if (normal_castling_flag) { if (!this.one_click_moves[x2 + x_head_adjustment][y2]) { heads.push({ colour: colour, x2: x2 + x_head_adjustment, y2: y2, info: info_list[i] }); this.one_click_moves[x2 + x_head_adjustment][y2] = info_list[i].move; } } else { if (!this.one_click_moves[x2][y2]) { heads.push({ colour: colour, x2: x2 + x_head_adjustment, y2: y2, info: info_list[i] }); this.one_click_moves[x2][y2] = info_list[i].move; } } } arrows.sort((a, b) => { if (Math.abs(a.x2 - a.x1) + Math.abs(a.y2 - a.y1) < Math.abs(b.x2 - b.x1) + Math.abs(b.y2 - b.y1)) { return 1; } if (Math.abs(a.x2 - a.x1) + Math.abs(a.y2 - a.y1) > Math.abs(b.x2 - b.x1) + Math.abs(b.y2 - b.y1)) { return -1; } return 0; }); boardctx.lineWidth = config.arrow_width; boardctx.textAlign = "center"; boardctx.textBaseline = "middle"; boardctx.font = config.board_font; for (let o of arrows) { let cc1 = CanvasCoords(o.x1, o.y1); let cc2 = CanvasCoords(o.x2, o.y2); boardctx.strokeStyle = o.colour; boardctx.fillStyle = o.colour; boardctx.beginPath(); boardctx.moveTo(cc1.cx, cc1.cy); boardctx.lineTo(cc2.cx, cc2.cy); boardctx.stroke(); } for (let o of heads) { let cc2 = CanvasCoords(o.x2, o.y2); boardctx.fillStyle = o.colour; boardctx.beginPath(); boardctx.arc(cc2.cx, cc2.cy, config.arrowhead_radius, 0, 2 * Math.PI); boardctx.fill(); boardctx.fillStyle = "black"; let s = "?"; if (typeof o.info.weight === "number") { s = (100 * o.info.weight).toFixed(0); } boardctx.fillText(s, cc2.cx, cc2.cy + 1); } } }; // For debugging... let draw_arrows_last_mode = null; ================================================ FILE: files/src/renderer/82_infobox.js ================================================ "use strict"; let infobox_props = { draw_infobox: function(node, mouse_point, active_square, active_colour, hoverdraw_div, allow_inactive_focus, lookup_object) { let searchmoves = node.searchmoves; if (this.displaying_error_log()) { infobox.innerHTML = this.error_log; this.last_drawn_version = null; return; } if (!node || node.destroyed) { return; } let info_list; if (node.terminal_reason()) { info_list = []; } else { info_list = SortedMoveInfo(node); } // A lookup_object should always have type (string) and moves (object). let ltype = lookup_object ? lookup_object.type : null; let lookup_moves = lookup_object ? lookup_object.moves : null; // If we are using an online API, and the list has some "untouched" info, we // may be able to sort them using the API info. if (ltype === "chessdbcn" || ltype === "lichess_masters" || ltype === "lichess_plebs") { let touched_list = []; let untouched_list = []; for (let info of info_list) { if (info.__touched) { touched_list.push(info); } else { untouched_list.push(info); } } const a_is_best = -1; const b_is_best = 1; untouched_list.sort((a, b) => { if (lookup_moves[a.move] && !lookup_moves[b.move]) return a_is_best; if (!lookup_moves[a.move] && lookup_moves[b.move]) return b_is_best; if (!lookup_moves[a.move] && !lookup_moves[b.move]) return 0; return lookup_moves[b.move].sort_score() - lookup_moves[a.move].sort_score(); }); info_list = touched_list.concat(untouched_list); } let best_subcycle = info_list.length > 0 ? info_list[0].subcycle : 0; if (best_subcycle === 0) { // Because all info was autopopulated best_subcycle = -1; // Causes all info to be gray } if (typeof config.max_info_lines === "number" && config.max_info_lines > 0) { // Hidden option, request of rwbc info_list = info_list.slice(0, config.max_info_lines); } // We might be highlighting some div... let highlight_move = null; let highlight_class = null; // We'll highlight it if it's a valid OCM *and* clicking there now would make it happen... if (mouse_point && this.one_click_moves[mouse_point.x][mouse_point.y]) { if (!active_square || this.one_click_moves[mouse_point.x][mouse_point.y].slice(0, 2) === active_square.s) { highlight_move = this.one_click_moves[mouse_point.x][mouse_point.y]; highlight_class = "ocm_highlight"; } } if (typeof hoverdraw_div === "number" && hoverdraw_div >= 0 && hoverdraw_div < info_list.length) { highlight_move = info_list[hoverdraw_div].move; highlight_class = "hover_highlight"; } // We cannot skip the draw if... let no_skip_reasons = []; if (node.id !== this.last_drawn_node_id) no_skip_reasons.push("node"); if (node.table.version !== this.last_drawn_version) no_skip_reasons.push("table version"); if (highlight_move !== this.last_drawn_highlight_move) no_skip_reasons.push("highlight move"); if (highlight_class !== this.last_drawn_highlight_class) no_skip_reasons.push("highlight class"); if (info_list.length !== this.last_drawn_length) no_skip_reasons.push("info list length"); if (allow_inactive_focus !== this.last_drawn_allow_inactive_focus) no_skip_reasons.push("allow inactive focus"); if (CompareArrays(searchmoves, this.last_drawn_searchmoves) === false) no_skip_reasons.push("searchmoves"); if (lookup_object !== this.last_drawn_lookup_object) no_skip_reasons.push("lookup object"); draw_infobox_no_skip_reasons = no_skip_reasons.join(", "); // For debugging only. if (no_skip_reasons.length === 0) { draw_infobox_total_skips++; return; } this.last_drawn_node_id = node.id; this.last_drawn_version = node.table.version; this.last_drawn_highlight_move = highlight_move; this.last_drawn_highlight_class = highlight_class; this.last_drawn_length = info_list.length; this.last_drawn_allow_inactive_focus = allow_inactive_focus; this.last_drawn_searchmoves = Array.from(searchmoves); this.last_drawn_lookup_object = lookup_object; this.info_clickers = []; this.info_clickers_node_id = node.id; let substrings = []; let clicker_index = 0; let div_index = 0; for (let info of info_list) { // The div containing the PV etc... let divclass = "infoline"; if (info.subcycle !== best_subcycle && !config.never_grayout_infolines) { divclass += " " + "gray"; } if (info.move === highlight_move) { divclass += " " + highlight_class; } substrings.push(`
`); // The "focus" button... if (config.searchmoves_buttons) { if (searchmoves.includes(info.move)) { substrings.push(`${config.focus_on_text} `); } else { if (allow_inactive_focus) { substrings.push(`${config.focus_off_text} `); } } } // The value... let value_string = "?"; if (config.show_cp) { if (typeof info.mate === "number" && info.mate !== 0) { value_string = info.mate_string(config.cp_pov); } else { value_string = info.cp_string(config.cp_pov); } } else { value_string = info.value_string(1, config.ev_pov); if (value_string !== "?") { value_string += "%"; } } if (info.subcycle === best_subcycle || config.never_grayout_infolines) { substrings.push(`${value_string} `); } else { substrings.push(`${value_string} `); } // The PV... let colour = active_colour; let movenum = node.board.fullmove; // Only matters for config.infobox_pv_move_numbers let nice_pv = info.nice_pv(); for (let i = 0; i < nice_pv.length; i++) { let spanclass = ""; if (info.subcycle === best_subcycle || config.never_grayout_infolines) { spanclass = colour === "w" ? "white" : "pink"; } if (nice_pv[i].includes("O-O")) { spanclass += (spanclass.length > 0) ? " nobr" : "nobr"; } let numstring = ""; if (config.infobox_pv_move_numbers) { if (colour === "w") { numstring = `${movenum}. `; } else if (colour === "b" && i === 0) { numstring = `${movenum}... `; } } substrings.push(`${numstring}${nice_pv[i]} `); this.info_clickers.push({ move: info.pv[i], is_start: i === 0, is_end: i === nice_pv.length - 1, }); colour = OppositeColour(colour); if (colour === "w") { movenum++; } } // The extra stats... let extra_stat_strings = []; if (info.__touched) { let stats_list = info.stats_list( { n: config.show_n, n_abs: config.show_n_abs, depth: config.show_depth, wdl: config.show_wdl, wdl_pov: config.wdl_pov, p: config.show_p, m: config.show_m, v: config.show_v, q: config.show_q, u: config.show_u, s: config.show_s, }, node.table.nodes); extra_stat_strings = extra_stat_strings.concat(stats_list); } if (config.looker_api) { let api_string = "API: ?"; if (ltype && lookup_moves) { let pov = null; if (ltype === "chessdbcn") { pov = config.cp_pov; } else if (ltype === "lichess_masters" || ltype === "lichess_plebs") { pov = config.ev_pov; } let o = lookup_moves[info.move]; if (typeof o === "object" && o !== null) { api_string = o.text(pov); } } extra_stat_strings.push(api_string); } if (extra_stat_strings.length > 0) { if (config.infobox_stats_newline) { substrings.push("
"); } substrings.push(`(${extra_stat_strings.join(', ')})`); } // Close the whole div... substrings.push("
"); } infobox.innerHTML = substrings.join(""); }, must_draw_infobox: function() { this.last_drawn_version = null; }, clickers_are_valid_for_node: function(node) { if (!node || !this.info_clickers_node_id) { return false; } return node.id === this.info_clickers_node_id; }, moves_from_click_n: function(n, desired_length = null) { if (typeof n !== "number" || Number.isNaN(n)) { return []; } if (!this.info_clickers || n < 0 || n >= this.info_clickers.length) { return []; } let move_list = []; // Work backwards until we get to the start of the line... for (let i = n; i >= 0; i--) { let object = this.info_clickers[i]; move_list.push(object.move); if (object.is_start) { break; } } move_list.reverse(); // If a PV length is specified, either truncate or extend as needed... if (typeof desired_length === "number") { if (move_list.length > desired_length) { move_list = move_list.slice(0, desired_length); } else if (move_list.length < desired_length) { for (let i = n + 1; i < this.info_clickers.length; i++) { let object = this.info_clickers[i]; if (object.is_start) { break; } move_list.push(object.move); // Note the different order of stataments compared to the above. if (move_list.length >= desired_length) { break; } } } } return move_list; }, }; // For debugging... let draw_infobox_total_skips = 0; let draw_infobox_no_skip_reasons = ""; ================================================ FILE: files/src/renderer/83_statusbox.js ================================================ "use strict"; function NewStatusHandler() { let sh = Object.create(null); sh.special_message = null; sh.special_message_class = "yellow"; sh.special_message_timeout = performance.now(); sh.set_special_message = function(s, css_class, duration) { if (!css_class) css_class = "yellow"; if (!duration) duration = 3000; this.special_message = s; this.special_message_class = css_class; this.special_message_timeout = performance.now() + duration; }; sh.draw_statusbox = function(node, engine, analysing_other, loading_message, book_is_loaded) { if (loading_message) { statusbox.innerHTML = `${loading_message} (abort?)`; } else if (config.show_engine_state) { let cl; let status; if (engine.search_running.node && engine.search_running === engine.search_desired) { cl = "green"; status = "running"; } else if (engine.search_running !== engine.search_desired) { cl = "yellow"; status = "desync"; } else { cl = "yellow"; status = "stopped"; } statusbox.innerHTML = `${status}, ` + `${config.behaviour}, ` + `${engine.last_send}`; } else if (!engine.ever_received_uciok) { statusbox.innerHTML = `Awaiting uciok from engine`; } else if (!engine.ever_received_readyok) { statusbox.innerHTML = `Awaiting readyok from engine`; } else if (this.special_message && performance.now() < this.special_message_timeout) { statusbox.innerHTML = `${this.special_message}`; } else if (engine.unresolved_stop_time && performance.now() - engine.unresolved_stop_time > 500) { statusbox.innerHTML = `${messages.desync}`; } else if (analysing_other) { statusbox.innerHTML = `Locked to ${analysing_other} (return?)`; } else if (node.terminal_reason()) { statusbox.innerHTML = `${node.terminal_reason()}`; } else if (!node || node.destroyed) { statusbox.innerHTML = `draw_statusbox - !node || node.destroyed`; } else { let status_string = ""; if (config.behaviour === "halt" && !engine.search_running.node) { status_string += `HALTED (go?) `; } else if (config.behaviour === "halt" && engine.search_running.node) { status_string += `HALTING... `; } else if (config.behaviour === "analysis_locked") { status_string += `Locked! `; } else if (config.behaviour === "play_white" && node.board.active !== "w") { status_string += `YOUR MOVE `; } else if (config.behaviour === "play_black" && node.board.active !== "b") { status_string += `YOUR MOVE `; } else if (config.behaviour === "self_play") { status_string += `Self-play! `; } else if (config.behaviour === "auto_analysis") { status_string += `Auto-eval! `; } else if (config.behaviour === "back_analysis") { status_string += `Back-eval! `; } else if (config.behaviour === "analysis_free") { if (hub.engine.sent_options.contempt !== undefined && hub.engine.sent_options.contempt !== "0") { status_string += `Contempt active! `; } else { status_string += `ANALYSIS (halt?) `; } } if (config.book_explorer) { let warn = book_is_loaded ? "" : " (No book loaded)"; status_string += `Book frequency arrows only!${warn}`; } else if (config.lichess_explorer) { let warn = (config.looker_api === "lichess_masters" || config.looker_api === "lichess_plebs") ? "" : " (API not selected)"; status_string += `Lichess frequency arrows only!${warn}`; } else { status_string += `${NString(node.table.nodes)} ${node.table.nodes === 1 ? "node" : "nodes"}`; status_string += `, ${DurationString(node.table.time)} (N/s: ${NString(node.table.nps)})`; if (engineconfig[engine.filepath].options["SyzygyPath"] || node.table.tbhits > 0) { status_string += `, ${NString(node.table.tbhits)} ${node.table.tbhits === 1 ? "tbhit" : "tbhits"}`; } status_string += ``; if (!engine.search_running.node && engine.search_completed.node === node) { let stoppedtext = ""; if (config.behaviour !== "halt") { stoppedtext = ` (stopped)`; } /* // The following doesn't make sense if a time limit rather than a move limit is in force. if (typeof engineconfig[engine.filepath].search_nodes === "number" && engineconfig[engine.filepath].search_nodes > 0) { if (node.table.nodes >= engineconfig[engine.filepath].search_nodes) { stoppedtext = ` (limit met)`; } } */ status_string += stoppedtext; } } statusbox.innerHTML = status_string; } }; return sh; } ================================================ FILE: files/src/renderer/90_engine.js ================================================ "use strict"; /* We are in one of these states (currently implicit in the logic): 1. Inactive 2. Running a search 3. Changing the search 4. Ending the search (1) Inactive................................................................................ A "bestmove" should not arrive. If the user wants to start a search, we send it and enter state 2. (2) Running a search........................................................................ A "bestmove" might arrive, in which case the search ends and we go into state 1. The "bestmove" line must be passed to hub.receive_bestmove(). Alternatively, the user may demand a search with new parameters, in which case we send "stop" and enter state 3. Or the user may halt, in which case we send "stop" and enter state 4. (3) Changing the search..................................................................... A "stop" has been sent and we are waiting for a "bestmove" response. When it arrives, we can send the new search and go back to state 2. The "bestmove" line itself can be discarded since it is not relevant to the desired search. In state 3, if the user changes the desired search, we simply replace the old desired search (which never started) with the new desired search (which may be the null search, in which case we have entered state 4). (4) Ending the search....................................................................... Just like state 3, except the desired search is the null search. When a "bestmove" arrives, we go to state 1. */ const GUI_WANTS_TO_KNOW = ["Backend", "EvalFile", "WeightsFile", "SyzygyPath", "Threads", "Hash", "MultiPV", "ContemptMode", "Contempt", "WDLCalibrationElo", "WDLEvalObjectivity", "ScoreType", "Temperature", "TempDecayMoves"]; let NoSearch = Object.freeze({ node: null, limit: null, limit_by_time: false, searchmoves: Object.freeze([]) }); function SearchParams(node = null, limit = null, limit_by_time = false, searchmoves = null) { if (!node) return NoSearch; let validated; if (Array.isArray(searchmoves)) { validated = node.validate_searchmoves(searchmoves); // returns a new array } else { validated = []; } Object.freeze(validated); // under no circumstances refactor this to freeze the original searchmoves return Object.freeze({ node: node, limit: limit, limit_by_time: limit_by_time, searchmoves: validated }); } function NewEngine(hub) { let eng = Object.create(null); eng.hub = hub; eng.exe = null; eng.scanner = null; eng.err_scanner = null; eng.filepath = ""; // Used to decide what entry in engineconfig to use. Start as "", which has defaults for the dummy engine. eng.last_send = null; eng.unresolved_stop_time = null; eng.ever_received_uciok = false; eng.ever_received_readyok = false; eng.have_quit = false; eng.suppress_cycle_info = null; // Stupid hack to allow "forget all analysis" to work; info lines from this cycle are ignored. eng.known_options = Object.create(null); // Keys are always lowercase. eng.sent_options = Object.create(null); // Keys are always lowercase. Values are always strings. eng.setoption_queue = []; eng.warn_send_fail = true; eng.leelaish = false; // Most likely set by hub upon an "id name" line, though can also be set by info_handler. eng.search_running = NoSearch; // The search actually being run right now. eng.search_desired = NoSearch; // The search we want Leela to be running. Often the same object as above. eng.search_completed = NoSearch; // Whatever object search_running was when the last "bestmove" came. // ------------------------------------------------------------------------------------------- eng.send = function(msg, force) { // Importantly, setoption messages are normally held back until the engine is not running. msg = msg.trim(); if (msg.startsWith("setoption")) { if (this.search_running.node && !force) { this.setoption_queue.push(msg); return; } let lower = msg.toLowerCase(); let i1 = lower.indexOf(" name "); let i2 = lower.indexOf(" value "); if (i1 !== -1 && i2 !== -1 && i2 > i1) { let key = lower.slice(i1 + 6, i2).trim(); // Keys are always lowercase. let val = msg.slice(i2 + 7).trim(); if (key.length > 0) { this.sent_options[key] = val; this.send_ack_setoption(key); } } } // Do this test here so the sent_options / ack stuff happens even when there is no engine // loaded, this helps our menu check marks to be correct. if (!this.exe) { return; } // Send the message... try { this.exe.stdin.write(msg); this.exe.stdin.write("\n"); Log("--> " + msg); this.last_send = msg; } catch (err) { Log("(failed) --> " + msg); if (this.last_send !== null && this.warn_send_fail) { alert(messages.send_fail); this.warn_send_fail = false; } } }; eng.send_desired = function() { if (this.search_running.node) { throw "send_desired() called but search was running"; } let node = this.search_desired.node; if (!node || node.destroyed || node.terminal_reason()) { this.search_running = NoSearch; this.search_desired = NoSearch; return; } let root_fen = node.get_root().board.fen(!this.in_960_mode()); let setup = `fen ${root_fen}`; if (!this.in_960_mode() && setup === "fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") { setup = "startpos"; // May as well send this format if we're not in 960 mode. } let moves; if (!this.in_960_mode()) { moves = node.history_old_format(); } else { moves = node.history(); } if (moves.length === 0) { this.send(`position ${setup}`); } else { this.send(`position ${setup} moves ${moves.join(" ")}`); } if (config.log_positions) { Log(node.board.graphic()); } let s; let n = this.search_desired.limit; if (!n) { s = "go infinite"; } else if (this.search_desired.limit_by_time) { s = `go movetime ${n}`; } else { s = `go nodes ${n}`; } if (config.searchmoves_buttons && this.search_desired.searchmoves.length > 0) { s += " searchmoves"; for (let move of this.search_desired.searchmoves) { s += " " + move; } } this.send(s); this.search_running = this.search_desired; this.suppress_cycle_info = null; this.hub.info_handler.engine_cycle++; this.hub.info_handler.engine_subcycle++; }; eng.set_search_desired = function(node, limit, limit_by_time, searchmoves) { if (!this.ever_received_uciok || !this.ever_received_readyok) { console.log("set_search_desired() aborted - too early"); return; } let params = SearchParams(node, limit, limit_by_time, searchmoves); // It is correct to check these against the *desired* search // (which may or may not be the one currently running). if (this.search_desired.node === params.node) { if (this.search_desired.limit === params.limit) { if (this.search_desired.limit_by_time === params.limit_by_time) { if (CompareArrays(this.search_desired.searchmoves, params.searchmoves)) { return; } } } } this.search_desired = params; // If a search is running, stop it... we will send the new position (if applicable) after receiving bestmove. // If no search is running, start the new search immediately. if (this.search_running.node) { this.send("stop"); if (!this.unresolved_stop_time) { this.unresolved_stop_time = performance.now(); } } else { if (this.search_desired.node) { this.send_desired(); } } }; eng.send_queued_setoptions = function() { for (let msg of this.setoption_queue) { this.send(msg, true); // Use the force flag in case we haven't set search_running to its correct value. } this.setoption_queue = []; }; eng.send_ucinewgame = function() { // Engine should be halted before calling this. if (!this.ever_received_uciok || !this.ever_received_readyok) { console.log("send_ucinewgame() aborted - too early"); return; // This is OK. When we actually get these, hub will send ucinewgame. } this.send("ucinewgame"); }; eng.handle_bestmove_line = function(line) { this.search_completed = this.search_running; this.search_running = NoSearch; this.unresolved_stop_time = null; // If this.search_desired === this.search_running then the search that just completed is // the most recent one requested by the hub; we have nothing to replace it with. // // Note that, in certain cases (e.g. a halt followed instantly by a resume) search_desired // and search_running will have identical properties but be different objects; in that case // it is correct to send the desired object as a new search. let no_new_search = this.search_desired === this.search_completed || !this.search_desired.node; let report_bestmove = this.search_desired === this.search_completed && this.search_completed.node; if (no_new_search) { this.search_desired = NoSearch; if (report_bestmove) { Log("< " + line); this.send_queued_setoptions(); // After logging the incoming. this.hub.receive_bestmove(line, this.search_completed.node); // May trigger a new search, so do it last. } else { Log("(ignore halted) < " + line); this.send_queued_setoptions(); // After logging the incoming. } } else { Log("(ignore old) < " + line); this.send_queued_setoptions(); // After logging the incoming. this.send_desired(); } }; eng.handle_info_line = function(line) { if (line.startsWith("info string ERROR")) { // Stockfish sends these. Log("< " + line); this.hub.info_handler.err_receive(line.slice(12)); return; } if (!this.search_running.node) { if (config.log_info_lines) Log("(ignore !node) < " + line); return; } if (this.search_running.node.destroyed) { if (config.log_info_lines) Log("(ignore destroyed) < " + line); return; } // Stockfish has a nasty habit of sending super short PVs when you stop its search. // To get around that, we ignore info from SF if it comes during transition. if (!this.leelaish && this.search_desired.node !== this.search_running.node) { if (config.log_info_lines) Log("(ignore A/B late) < " + line); return; } // Hub can set a cycle to be suppressed (e.g. for the sake of making "forget all analysis" work). // This feels a bit sketchy, but will be OK as long as the next "go" is guaranteed to increment the cycle number. if (this.suppress_cycle_info === this.hub.info_handler.engine_cycle) { if (config.log_info_lines) Log("(ignore suppressed) < " + line); return; } this.hub.info_handler.receive(this, this.search_running, line); // Responsible for logging lines that get this far. }; eng.setoption = function(name, value) { let s = `setoption name ${name} value ${value}`; this.send(s); return s; // Just so the caller can pop s up as a message if it wants. }; eng.pressbutton = function(name) { let s = `setoption name ${name}`; this.send(s); return s; // Just so the caller can pop s up as a message if it wants. }; eng.send_ack_setoption = function(name) { let key = name.toLowerCase(); // Keys are always stored in lowercase. let val = typeof this.sent_options[key] === "string" ? this.sent_options[key] : ""; // Values are strings, if present let o = {key, val}; ipcRenderer.send("ack_setoption", o); return o; }; eng.in_960_mode = function() { return this.sent_options["uci_chess960"] === "true"; // The string "true" since these values are always strings. }; eng.known = function(s) { return this.known_options[s.toLowerCase()] !== undefined; }; eng.send_ack_engine = function() { ipcRenderer.send("ack_engine", this.filepath); }; eng.setup = function(filepath, args) { // Returns true on success, false otherwise. Log(""); Log(`Launching ${filepath}`); if (args.length > 0) Log(`Args: ${JSON.stringify(args)}`); Log(""); try { if (path.basename(filepath).toLowerCase().includes("lc0")) { // Stupid hack to make Lc0 show all its options. if (args.includes("--show-hidden") === false) { args = ["--show-hidden"].concat(args); } } this.exe = child_process.spawn(filepath, args, {cwd: path.dirname(filepath)}); } catch (err) { console.log(`engine.setup() failed: ${err.toString()}`); return false; } this.filepath = filepath; this.send_ack_engine(); // After this.filepath is set. // Main process wants to keep track of what these things are set to (for menu checks). // These will all ack the value "" to main.js since no value has been set yet... this.sent_options = Object.create(null); // Blank anything we "sent" up till now. for (let key of GUI_WANTS_TO_KNOW) { this.send_ack_setoption(key); } this.exe.once("error", (err) => { alert(err); }); this.scanner = readline.createInterface({ input: this.exe.stdout, output: undefined, terminal: false }); this.err_scanner = readline.createInterface({ input: this.exe.stderr, output: undefined, terminal: false }); this.err_scanner.on("line", (line) => { if (this.have_quit) return; Log(". " + line); this.hub.err_receive(SafeStringHTML(line)); }); this.scanner.on("line", (line) => { if (this.have_quit) return; if (line.startsWith("bestmove")) { this.handle_bestmove_line(line); // Will do logging, possibly adding a reason for rejection. } else if (line.startsWith("info")) { this.handle_info_line(line); // Will do logging, possibly adding a reason for rejection. } else { Log("< " + line); if (line.startsWith("option")) { let a = line.indexOf(" name "); let b = line.indexOf(" type "); if (a !== -1 && b != -1) { let optname = line.slice(a + 6, b).trim().toLowerCase(); this.known_options[optname] = line.slice(b + 1); if (optname === "uci_chess960") { // As a special thing, always set UCI_Chess960 where possible. this.setoption("UCI_Chess960", true); // (Why is this not just done in globals.js? I forget...) } } } if (line.startsWith("uciok")) { this.ever_received_uciok = true; } if (line.startsWith("readyok")) { this.ever_received_readyok = true; } this.hub.receive_misc(SafeStringHTML(line)); } }); return true; }; eng.shutdown = function() { // Note: Don't reuse the engine object. this.have_quit = true; this.send("quit"); if (this.exe) { setTimeout(() => { this.exe.kill(); }, 2000); } }; return eng; } ================================================ FILE: files/src/renderer/95_hub.js ================================================ "use strict"; function NewHub() { let hub = Object.create(null); hub.engine = NewEngine(hub); // Just a dummy object with no exe. Fixed by start.js later. hub.tree = NewTreeHandler(); hub.grapher = NewGrapher(); hub.looker = NewLooker(); hub.info_handler = NewInfoHandler(); hub.status_handler = NewStatusHandler(); // Various state we have to keep track of... hub.loaders = []; // The loaders can have shutdown() called on them to stop ASAP. hub.book = null; // Either a Polyglot buffer, or an array of {key, move, weight}. hub.pgndata = null; // Object representing the loaded PGN file. hub.engine_choices = []; // Made by show_fast_engine_chooser() when needed. hub.fullbox_config_item = null; // Name of config item currently being edited in fullbox. hub.fullbox_web_link = null; // Web link which can be clicked on in the config editor. hub.pgn_choices_start = 0; // Where we are in the PGN Chooser screen. hub.friendly_draws = New2DArray(8, 8, null); // What pieces are drawn in boardfriends. Used to skip redraws. hub.enemy_draws = New2DArray(8, 8, null); // What pieces are drawn in boardsquares. Used to skip redraws. hub.dirty_squares = New2DArray(8, 8, null); // What squares have some coloured background. hub.active_square = null; // Clicked square, shown in blue. hub.hoverdraw_div = -1; // Which div is hovered; used by draw_infobox(). hub.hoverdraw_depth = 0; // How deep in the hover PV we are. hub.tick = 0; // How many draw loops we've been through. Used to animate hoverdraw. hub.position_change_time = performance.now(); // Time of the last position change. Used for cooldown on hoverdraw. hub.node_to_clean = hub.tree.node; // The next node to be cleaned up (done when exiting it). hub.leela_lock_node = null; // Non-null only when in "analysis_locked" mode. hub.looker.add_to_queue(hub.tree.node.board); // Maybe make initial call to API such as ChessDN.cn... Object.assign(hub, hub_props); return hub; } let hub_props = { // --------------------------------------------------------------------------------------------------------------------- // Core methods wrt our main state... behave: function(reason) { // reason should be "position" or "behaviour" // Called when position changes. // Called when behaviour changes. // // Each branch should do one of the following: // // Call __go() to start a new search // Call __halt() to ensure the engine isn't running // Nothing, iff the correct search is already running if (reason !== "position" && reason !== "behaviour") { throw "behave(): bad call"; } switch (config.behaviour) { case "halt": this.__halt(); break; case "analysis_free": // Note that the 2nd part of the condition is needed because changing behaviour can change what node_limit() // returns, therefore we might already be running a search for the right node but with the wrong limit. // THIS IS TRUE THROUGHOUT THIS FUNCTION. if (this.engine.search_desired.node !== this.tree.node || this.engine.search_desired.limit !== this.node_limit()) { this.__go(this.tree.node); } break; case "auto_analysis": case "back_analysis": if (this.tree.node.terminal_reason()) { this.continue_auto_analysis(); // This can get a bit recursive, do we care? } else if (this.engine.search_desired.node !== this.tree.node || this.engine.search_desired.limit !== this.node_limit()) { this.__go(this.tree.node); } break; case "analysis_locked": // Moving shouldn't trigger anything, except that re-entering the correct node changes behaviour to halt // iff the search is completed. if (reason === "position") { if (this.tree.node === this.leela_lock_node) { if (!this.engine.search_desired.node) { this.set_behaviour_direct("halt"); } } } else { if (this.engine.search_desired.node !== this.leela_lock_node || this.engine.search_desired.limit !== this.node_limit()) { this.__go(this.leela_lock_node); } } break; case "self_play": case "play_white": case "play_black": if ((config.behaviour === "self_play") || (config.behaviour === "play_white" && this.tree.node.board.active === "w") || (config.behaviour === "play_black" && this.tree.node.board.active === "b")) { if (this.maybe_setup_book_move()) { this.__halt(); break; } if (this.engine.search_desired.node !== this.tree.node || this.engine.search_desired.limit !== this.node_limit()) { this.__go(this.tree.node); } } else { // Play single colour mode, wrong colour. this.__halt(); } break; } }, position_changed: function(new_game_flag, avoid_confusion) { // Called right after this.tree.node is changed, meaning we are now drawing a different position. this.escape(); drag_handler.cancel_drag(); this.hoverdraw_div = -1; this.position_change_time = performance.now(); fenbox.value = this.tree.node.board.fen(true); if (new_game_flag) { this.node_to_clean = null; this.leela_lock_node = null; this.set_behaviour("halt"); // Will cause "stop" to be sent. if (!config.suppress_ucinewgame) { this.engine.send_ucinewgame(); // Must happen after "stop" is sent. } this.send_title(); if (this.engine.ever_received_uciok && !this.engine.in_960_mode() && this.tree.node.board.normalchess === false) { alert(messages.c960_warning); } } if (this.tree.node.table.already_autopopulated === false) { this.tree.node.table.autopopulate(this.tree.node); } // When entering a position, clear its searchmoves, unless it's the analysis_locked node. if (this.leela_lock_node !== this.tree.node) { this.tree.node.searchmoves = []; } // Caller can tell us the change would cause user confusion for some modes... if (avoid_confusion) { if (["play_white", "play_black", "self_play", "auto_analysis", "back_analysis"].includes(config.behaviour)) { this.set_behaviour("halt"); } } this.maybe_infer_info(); // Before node_exit_cleanup() so that previous ghost info is available when moving forwards. this.behave("position"); this.draw(); this.node_exit_cleanup(); // This feels like the right time to do this. this.node_to_clean = this.tree.node; this.looker.add_to_queue(this.tree.node.board); }, set_behaviour: function(s) { if (!this.engine.ever_received_uciok || !this.engine.ever_received_readyok) { s = "halt"; } // Don't do anything if behaviour is already correct. But // "halt" always triggers a behave() call for safety reasons. if (s === config.behaviour) { switch (s) { case "halt": break; // i.e. do NOT immediately return case "analysis_locked": if (this.leela_lock_node !== this.tree.node) { break; // i.e. do NOT immediately return } return; case "analysis_free": if (!this.engine.search_desired.node) { break; // i.e. do NOT immediately return } return; default: return; } } this.set_behaviour_direct(s); this.behave("behaviour"); }, set_behaviour_direct: function(s) { this.leela_lock_node = (s === "analysis_locked") ? this.tree.node : null; config.behaviour = s; }, toggle_go: function() { if (["analysis_free", "self_play", "auto_analysis", "back_analysis"].includes(config.behaviour)) { this.set_behaviour("halt"); } else if (config.behaviour === "halt") { this.set_behaviour("analysis_free"); } }, play_this_colour: function() { if (this.tree.node.board.active === "w") { this.set_behaviour("play_white"); } else { this.set_behaviour("play_black"); } }, handle_search_params_change: function() { // If there's already a search desired, we can just let __go() figure out what the new parameters should be. // If they match what is already desired then set_search_desired() will ignore the call. if (this.engine.search_desired.node) { this.__go(this.engine.search_desired.node); } // If there's no search desired, changing params probably shouldn't start one. As of 1.8.3, when a search // completes due to hitting the (normal) node limit, behaviour gets changed back to "halt" in one way or // another (unless config.allow_stopped_analysis is set). }, continue_auto_analysis: function() { let ok; if (config.behaviour === "auto_analysis") { ok = this.tree.next(); } else if (config.behaviour === "back_analysis") { ok = this.tree.prev(); } if (ok) { this.position_changed(false, false); } else { this.set_behaviour("halt"); } }, maybe_setup_book_move: function() { if (!this.book || this.tree.node.terminal_reason()) { return false; } if (typeof config.book_depth === "number" && this.tree.node.depth >= config.book_depth * 2) { return false; } let move; let objects = BookProbe(KeyFromBoard(this.tree.node.board), this.book); let total_weight = 0; if (Array.isArray(objects)) { for (let o of objects) { total_weight += o.weight; } } if (total_weight <= 0) { return false; } let rng = RandInt(0, total_weight); let weight_seen = 0; for (let o of objects) { // The order doesn't matter at all when you think about it. No need to sort. weight_seen += o.weight; if (rng < weight_seen) { move = o.move; break; } } if (!move) { return false; } if (this.tree.node.board.illegal(move)) { return false; } let correct_node = this.tree.node; let correct_behaviour = config.behaviour; // Use a setTimeout to prevent recursion (since move() will cause a call to behave()) setTimeout(() => { if (this.tree.node === correct_node && config.behaviour === correct_behaviour) { this.move(move); } }, 0); return true; }, maybe_infer_info: function() { // This function creates "ghost" info in the info table when possible and necessary; // such info is inferred from ancestral info. It is also deleted upon leaving the node. // // The whole thing is a bit sketchy, maybe. if (config.behaviour === "play_white" || config.behaviour === "play_black") { return; } let node = this.tree.node; if (node.terminal_reason()) { return; } if (!node.parent) { return; } for (let info of Object.values(node.table.moveinfo)) { if (info.__touched) { return; } } // So the current node has no real info. let moves = [node.move]; let ancestor = null; let foo = node.parent; while (foo) { for (let info of Object.values(foo.table.moveinfo)) { if (info.__touched) { ancestor = foo; break; } } if (!ancestor) { moves.push(foo.move); foo = foo.parent; } else { break; } } if (!ancestor) { return; } // So we found the closest ancestor with info. moves.reverse(); let oldinfo = ancestor.table.moveinfo[moves[0]]; if (!oldinfo) { return; } if (Array.isArray(oldinfo.pv) === false || oldinfo.pv.length <= moves.length) { return; } let pv = Array.from(oldinfo.pv); for (let n = 0; n < moves.length; n++) { if (pv[n] !== moves[n]) { return; } } // So, everything matches and we can use the PV... let nextmove = pv[moves.length]; pv = pv.slice(moves.length); let new_info = NewInfo(node.board, nextmove); new_info.set_pv(pv); new_info.__ghost = true; new_info.__touched = true; new_info.subcycle = 1; // Crude hack, makes draw_infobox() make other moves gray. new_info.q = oldinfo.q; new_info.cp = oldinfo.cp; new_info.multipv = 1; // Flip our evals if the colour changes... if (oldinfo.board.active !== node.board.active) { if (typeof new_info.q === "number") { new_info.q *= -1; } if (typeof new_info.cp === "number") { new_info.cp *= -1; } } node.table.moveinfo[nextmove] = new_info; }, node_exit_cleanup: function() { if (!this.node_to_clean || this.node_to_clean.destroyed) { return; } // Remove ghost info; which is only allowed in the node we're currently looking at... // By remove, I mean, replace it with a neutral info object. for (let key of Object.keys(this.node_to_clean.table.moveinfo)) { if (this.node_to_clean.table.moveinfo[key].__ghost) { this.node_to_clean.table.moveinfo[key] = NewInfo(this.node_to_clean.board, key); } } }, // --------------------------------------------------------------------------------------------------------------------- // Spin, our main loop... spin: function() { this.tick++; this.draw(); this.purge_finished_loaders(); this.maybe_save_window_size(); setTimeout(this.spin.bind(this), config.update_delay); }, purge_finished_loaders: function() { this.loaders = this.loaders.filter(o => o.callback); }, maybe_save_window_size: function() { if (this.window_resize_time && performance.now() - this.window_resize_time > 1000) { this.window_resize_time = null; this.save_window_size(); } }, // --------------------------------------------------------------------------------------------------------------------- // Drawing properties... draw: function() { // We do the :hover reaction first. This way, we are detecting hover based on the previous cycle's state. // This should prevent the sort of flicker that can occur if we try to detect hover based on changes we // just made (i.e. if we drew then detected hover instantly). let did_hoverdraw = this.hoverdraw(); if (did_hoverdraw) { canvas.style.outline = "2px dashed #b4b4b4"; } else { this.hoverdraw_div = -1; boardfriends.style.display = "block"; canvas.style.outline = "none"; this.draw_move_and_active_squares(this.tree.node.move, this.active_square); this.draw_enemies_in_table(this.tree.node.board); this.draw_canvas_arrows(); this.draw_friendlies_in_table(this.tree.node.board); } this.draw_statusbox(); this.draw_infobox(); this.grapher.draw(this.tree.node); }, draw_friendlies_in_table: function(board) { for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { let piece_to_draw = ""; if (board.colour(Point(x, y)) === board.active) { piece_to_draw = board.state[x][y]; } if (piece_to_draw === this.friendly_draws[x][y]) { continue; } // So if we get to here, we need to draw... this.friendly_draws[x][y] = piece_to_draw; let s = S(x, y); let td = document.getElementById("overlay_" + s); if (piece_to_draw === "") { td.style["background-image"] = "none"; } else { td.style["background-image"] = images[piece_to_draw].string_for_bg_style; } } } }, draw_enemies_in_table: function(board) { for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { let piece_to_draw = ""; if (board.colour(Point(x, y)) === OppositeColour(board.active)) { piece_to_draw = board.state[x][y]; } if (piece_to_draw === this.enemy_draws[x][y]) { continue; } // So if we get to here, we need to draw... this.enemy_draws[x][y] = piece_to_draw; let s = S(x, y); let td = document.getElementById("underlay_" + s); if (piece_to_draw === "") { td.style["background-image"] = "none"; } else { td.style["background-image"] = images[piece_to_draw].string_for_bg_style; } } } }, draw_move_and_active_squares: function(move, active_square) { // These constants are stupidly used in set_active_square() also. const EMPTY = 0; const HIGHLIGHT = 1; const ACTIVE = 2; if (!this.dmaas_scratch) { this.dmaas_scratch = New2DArray(8, 8, null); } // First, set each element of the array to indicate what state we want // its background-color to be in. for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { this.dmaas_scratch[x][y] = EMPTY; } } let move_points = []; if (typeof move === "string") { let source = Point(move.slice(0, 2)); let dest = Point(move.slice(2, 4)); if (source && dest) { move_points = PointsBetween(source, dest); } } for (let p of move_points) { this.dmaas_scratch[p.x][p.y] = HIGHLIGHT; } if (active_square) { this.dmaas_scratch[active_square.x][active_square.y] = ACTIVE; } // Now the dmaas_scratch array has what we actually want. // We check whether each square is already so, and change it otherwise. for (let x = 0; x < 8; x++) { for (let y = 0; y < 8; y++) { switch (this.dmaas_scratch[x][y]) { case EMPTY: if (this.dirty_squares[x][y] !== EMPTY) { let s = S(x, y); let td = document.getElementById("underlay_" + s); td.style["background-color"] = "transparent"; this.dirty_squares[x][y] = EMPTY; } break; case HIGHLIGHT: if (this.dirty_squares[x][y] !== HIGHLIGHT) { let s = S(x, y); let td = document.getElementById("underlay_" + s); td.style["background-color"] = config.move_squares_with_alpha; this.dirty_squares[x][y] = HIGHLIGHT; } break; case ACTIVE: if (this.dirty_squares[x][y] !== ACTIVE) { let s = S(x, y); let td = document.getElementById("underlay_" + s); td.style["background-color"] = config.active_square; this.dirty_squares[x][y] = ACTIVE; } break; } } } }, hoverdraw: function() { if (!config.hover_draw || this.info_handler.clickers_are_valid_for_node(this.tree.node) === false) { return false; } if (performance.now() - this.position_change_time < 1000) { return false; } let overlist = document.querySelectorAll(":hover"); // Find what div we are over by looking for infoline_n let div = null; let div_index = null; for (let item of overlist) { if (typeof item.id === "string" && item.id.startsWith("infoline_")) { div = item; div_index = parseInt(item.id.slice("infoline_".length), 10); break; } } if (!div || typeof div_index !== "number" || Number.isNaN(div_index)) { return false; } // Find what infobox clicker we are over by looking for infobox_n let click_n = null; for (let item of overlist) { if (typeof item.id === "string" && item.id.startsWith("infobox_")) { click_n = parseInt(item.id.slice("infobox_".length), 10); break; } } if (typeof click_n !== "number" || Number.isNaN(click_n)) { // We failed to get a click_n value. But if we are in Animate or Final Position mode, // it should still work even if the user isn't hovering over a move exactly; we can // just pass any valid click_n from the line... this is a pretty dumb hack. if (config.hover_method !== 0 && config.hover_method !== 2) { return false; } for (let item of div.childNodes) { if (typeof item.id === "string" && item.id.startsWith("infobox_")) { click_n = parseInt(item.id.slice("infobox_".length), 10); break; } } if (typeof click_n !== "number" || Number.isNaN(click_n)) { return false; } } // if (config.hover_method === 0) { return this.hoverdraw_animate(div_index, click_n); // Sets this.hoverdraw_div } else if (config.hover_method === 1) { return this.hoverdraw_single(div_index, click_n); // Sets this.hoverdraw_div } else if (config.hover_method === 2) { return this.hoverdraw_final(div_index, click_n); // Sets this.hoverdraw_div } else { return false; // Caller must set this.hoverdraw_div to -1 } }, hoverdraw_animate: function(div_index, click_n) { // If the user is hovering over an unexpected div index in the infobox, reset depth... if (div_index !== this.hoverdraw_div) { this.hoverdraw_div = div_index; this.hoverdraw_depth = 0; } // Sometimes increase depth... if (this.tick % config.animate_delay_multiplier === 0) { this.hoverdraw_depth++; } let moves = this.info_handler.moves_from_click_n(click_n, this.hoverdraw_depth); if (Array.isArray(moves) === false || moves.length === 0) { return false; } return this.draw_fantasy_from_moves(moves); }, hoverdraw_single: function(div_index, click_n) { this.hoverdraw_div = div_index; let moves = this.info_handler.moves_from_click_n(click_n); if (Array.isArray(moves) === false || moves.length === 0) { return false; } return this.draw_fantasy_from_moves(moves); }, hoverdraw_final: function(div_index, click_n) { this.hoverdraw_div = div_index; let moves = this.info_handler.moves_from_click_n(click_n, 999); if (Array.isArray(moves) === false || moves.length === 0) { return false; } return this.draw_fantasy_from_moves(moves); }, draw_fantasy_from_moves: function(moves) { // We don't assume moves is an array of legal moves, or even an array. // This is probably paranoid at this point but meh. if (Array.isArray(moves) === false) { return false; } let board = this.tree.node.board; for (let move of moves) { let illegal_reason = board.illegal(move); if (illegal_reason) { return false; } board = board.move(move); } let move = moves[moves.length - 1]; // Possibly undefined... this.draw_fantasy(board, move); return true; }, draw_fantasy: function(board, move) { this.draw_move_and_active_squares(move, null); this.draw_enemies_in_table(board); boardctx.clearRect(0, 0, canvas.width, canvas.height); // Clearing the canvas arrows. this.draw_friendlies_in_table(board); }, draw_canvas_arrows: function() { boardctx.clearRect(0, 0, canvas.width, canvas.height); if (config.book_explorer) { this.draw_explorer_arrows(); } else if (config.lichess_explorer) { this.draw_lichess_arrows(); } else { let arrow_spotlight_square = config.click_spotlight ? this.active_square : null; let next_move = (config.next_move_arrow && this.tree.node.children.length > 0) ? this.tree.node.children[0].move : null; this.info_handler.draw_arrows(this.tree.node, arrow_spotlight_square, next_move); } }, draw_explorer_arrows: function() { // This is all pretty isolated from everything else. Keep it that way. if (!this.book) { this.explorer_objects_cache = null; this.explorer_cache_node_id = null; this.info_handler.draw_explorer_arrows(this.tree.node, [], null); // Needs to happen, to update the one_click_moves. return; } if (!this.explorer_objects_cache || this.explorer_cache_node_id !== this.tree.node.id) { let objects = BookProbe(KeyFromBoard(this.tree.node.board), this.book); let total_weight = 0; if (Array.isArray(objects)) { for (let o of objects) { total_weight += o.weight; } } if (total_weight <= 0) { total_weight = 1; // Avoid div by zero. } let tmp = {}; for (let o of objects) { if (!this.tree.node.board.illegal(o.move)) { if (tmp[o.move] === undefined) { tmp[o.move] = {move: o.move, weight: o.weight / total_weight}; } } } this.explorer_cache_node_id = this.tree.node.id; this.explorer_objects_cache = Object.values(tmp); this.explorer_objects_cache.sort((a, b) => b.weight - a.weight); } let arrow_spotlight_square = config.click_spotlight ? this.active_square : null; this.info_handler.draw_explorer_arrows(this.tree.node, this.explorer_objects_cache, arrow_spotlight_square); }, draw_lichess_arrows: function() { // Modified version of the above. let ok = true; if (config.looker_api !== "lichess_masters" && config.looker_api !== "lichess_plebs") { ok = false; } let entry = this.looker.lookup(config.looker_api, this.tree.node.board); if (!entry) { ok = false; } if (!ok) { this.explorer_objects_cache = null; this.explorer_cache_node_id = null; this.info_handler.draw_explorer_arrows(this.tree.node, [], null); // Needs to happen, to update the one_click_moves. return; } if (!this.explorer_objects_cache || this.explorer_cache_node_id !== this.tree.node.id) { let total_weight = 0; for (let o of Object.values(entry.moves)) { total_weight += o.total; } if (total_weight <= 0) { total_weight = 1; // Avoid div by zero. } let tmp = {}; for (let move of Object.keys(entry.moves)) { if (!this.tree.node.board.illegal(move)) { if (tmp[move] === undefined) { tmp[move] = {move: move, weight: entry.moves[move].total / total_weight}; } } } this.explorer_cache_node_id = this.tree.node.id; this.explorer_objects_cache = Object.values(tmp); this.explorer_objects_cache.sort((a, b) => b.weight - a.weight); } let arrow_spotlight_square = config.click_spotlight ? this.active_square : null; this.info_handler.draw_explorer_arrows(this.tree.node, this.explorer_objects_cache, arrow_spotlight_square); }, draw_statusbox: function() { let analysing_other = null; if (config.behaviour === "analysis_locked" && this.leela_lock_node && this.leela_lock_node !== this.tree.node) { if (!this.leela_lock_node.parent) { analysing_other = "root"; } else { analysing_other = "position after " + this.leela_lock_node.token(false, true); } } let loading_message = null; for (let loader of this.loaders) { if (loader.callback) { // By our rules, can only exist if the load is still pending... if (performance.now() - loader.starttime > 100) { loading_message = loader.msg; break; } } } this.status_handler.draw_statusbox( this.tree.node, this.engine, analysing_other, loading_message, this.book ? true : false ); }, draw_infobox: function() { this.info_handler.draw_infobox( this.tree.node, this.mouse_point(), this.active_square, this.tree.node.board.active, this.hoverdraw_div, config.behaviour === "halt" || config.never_suppress_searchmoves, config.looker_api ? this.looker.lookup(config.looker_api, this.tree.node.board) : null); }, // --------------------------------------------------------------------------------------------------------------------- // Fundamental engine methods... not to be called directly, except by behave() and handle_search_params_change()... __halt: function() { this.engine.set_search_desired(null); }, __go: function(node) { this.hide_fullbox(); if (!node || node.destroyed || node.terminal_reason()) { this.engine.set_search_desired(null); return; } this.engine.set_search_desired(node, this.node_limit(), engineconfig[this.engine.filepath].limit_by_time, node.searchmoves); }, // --------------------------------------------------------------------------------------------------------------------- // Info receivers... receive_bestmove: function(s, relevant_node) { let ok; // Could be used by 2 different parts of the switch (but not at time of writing...) switch (config.behaviour) { case "self_play": case "play_white": case "play_black": if (relevant_node !== this.tree.node) { LogBoth(`(ignored bestmove, relevant_node !== hub.tree.node, config.behaviour was "${config.behaviour}")`); this.set_behaviour("halt"); break; } let tokens = s.split(" ").filter(z => z !== ""); ok = this.move(tokens[1]); if (!ok) { LogBoth(`BAD BESTMOVE (${tokens[1]}) IN POSITION ${this.tree.node.board.fen(true)}`); this.set_special_message(`WARNING! Bad bestmove (${tokens[1]}) received!`, "yellow", 10000); } else { if (this.tree.node.terminal_reason()) { this.set_behaviour("halt"); } } break; case "auto_analysis": case "back_analysis": if (relevant_node !== this.tree.node) { LogBoth(`(ignored bestmove, relevant_node !== hub.tree.node, config.behaviour was "${config.behaviour}")`); this.set_behaviour("halt"); } else { this.continue_auto_analysis(); } break; case "analysis_free": // We hit the node limit. if (!config.allow_stopped_analysis) { this.set_behaviour("halt"); } break; case "analysis_locked": // We hit the node limit. If the node we're looking at isn't the locked node, don't // change behaviour. (It will get changed when we enter the locked node.) if (this.tree.node === this.leela_lock_node) { this.set_behaviour("halt"); } break; } }, receive_misc: function(s) { if (s.startsWith("id name")) { // Note that we do need to set the leelaish flag on the engine here (rather than relying on the // autodetection in info.js) so that correct options can be sent. this.engine.leelaish = false; for (let name of config.leelaish_names) { if (s.includes(name)) { this.engine.leelaish = true; break; } } // Our defaults in engineconfig_io.newentry() are appropriate for Leelaish engines. // But if this is the first time we see an A/B engine, we must adjust them... if (!this.engine.leelaish && !engineconfig[this.engine.filepath].options["MultiPV"]) { // This likely indicates the engine is new to the config. engineconfig[this.engine.filepath].options["MultiPV"] = 3; // Will get ack'd when engine_send_all_options() happens engineconfig[this.engine.filepath].search_nodes_special = 10000000; this.send_ack_node_limit(true); } // Pass unknown engines to the error handler to be displayed... if (!s.includes("Lc0") && !s.includes("Ceres") && !s.includes("Stockfish")) { this.info_handler.err_receive(s.slice("id name".length).trim()); } return; } if (s.startsWith("uciok")) { // Until we receive uciok and readyok, set_behaviour() does nothing and set_search_desired() ignores calls, so "go" cannot have been sent. this.engine_send_all_options(); this.engine.send("isready"); return; } if (s.startsWith("readyok")) { // Until we receive uciok and readyok, set_behaviour() does nothing and set_search_desired() ignores calls, so "go" cannot have been sent. this.set_behaviour("halt"); // Likely redundant (should be "halt" anyway), but ensures the hub is in a sane state. this.engine.send_ucinewgame(); // Relies on the engine not running. return; } // Misc messages. Treat ones that aren't valid UCI as errors to be passed along... if (!s.startsWith("id") && !s.startsWith("option") && !s.startsWith("bestmove") && // These messages shouldn't reach this function !s.startsWith("info") // These messages shouldn't reach this function ) { this.info_handler.err_receive(s); } }, err_receive: function(s) { // Some highlights... this is obviously super-fragile based on the precise strings Leela sends. if (s.startsWith("Found configuration file: ")) { this.info_handler.err_receive(HighlightString(s, "Found configuration file: ", "blue")); return; } if (s.startsWith("Loading Syzygy tablebases from ")) { this.info_handler.err_receive(HighlightString(s, "Loading Syzygy tablebases from ", "blue")); return; } if (s.startsWith("Loading weights file from: ")) { this.info_handler.err_receive(HighlightString(s, "Loading weights file from: ", "blue")); return; } if (s.startsWith("Found pb network file: ")) { this.info_handler.err_receive(HighlightString(s, "Found pb network file: ", "blue")); return; } this.info_handler.err_receive(s); }, // --------------------------------------------------------------------------------------------------------------------- // Node limits... node_limit: function() { // Given the current state of the config, what is the node limit? // Note that this value is used as a time limit instead, if engineconfig[this.engine.filepath].limit_by_time is set. let cfg_value; switch (config.behaviour) { case "play_white": case "play_black": case "self_play": case "auto_analysis": case "back_analysis": cfg_value = engineconfig[this.engine.filepath].search_nodes_special; break; default: cfg_value = engineconfig[this.engine.filepath].search_nodes; break; } // Should match the system in engine.js. if (typeof cfg_value === "number" && cfg_value >= 1) { return cfg_value; } else { return null; } }, adjust_node_limit: function(direction, special_flag) { let cfg_value = special_flag ? engineconfig[this.engine.filepath].search_nodes_special : engineconfig[this.engine.filepath].search_nodes; if (direction > 0) { if (typeof cfg_value !== "number" || cfg_value <= 0) { // Already unlimited this.set_node_limit_generic(null, special_flag); return; } for (let i = 0; i < limit_options.length; i++) { if (limit_options[i] > cfg_value) { this.set_node_limit_generic(limit_options[i], special_flag); return; } } this.set_node_limit_generic(null, special_flag); } else { if (typeof cfg_value !== "number" || cfg_value <= 0) { // Unlimited; reduce to highest finite option this.set_node_limit_generic(limit_options[limit_options.length - 1], special_flag); return; } for (let i = limit_options.length - 1; i >= 0; i--) { if (limit_options[i] < cfg_value) { this.set_node_limit_generic(limit_options[i], special_flag); return; } } this.set_node_limit_generic(1, special_flag); } }, set_node_limit: function(val) { this.set_node_limit_generic(val, false); }, set_node_limit_special: function(val) { this.set_node_limit_generic(val, true); }, set_node_limit_generic: function(val, special_flag) { if (typeof val !== "number" || val <= 0) { val = null; } let msg_start; let by_time = engineconfig[this.engine.filepath].limit_by_time; if (by_time) { msg_start = special_flag ? "Special time limit" : "Time limit"; } else { msg_start = special_flag ? "Special node limit" : "Node limit"; } if (val) { this.set_special_message(`${msg_start} now ${CommaNum(val)} ${by_time ? "ms" : ""}`, "blue"); } else { this.set_special_message(`${msg_start} removed!`, "blue"); } if (special_flag) { engineconfig[this.engine.filepath].search_nodes_special = val; } else { engineconfig[this.engine.filepath].search_nodes = val; } this.send_ack_node_limit(special_flag); this.handle_search_params_change(); }, send_ack_node_limit: function(special_flag) { let ack_type = special_flag ? "ack_special_node_limit" : "ack_node_limit"; let val; if (special_flag) { val = engineconfig[this.engine.filepath].search_nodes_special; } else { val = engineconfig[this.engine.filepath].search_nodes; } if (val) { ipcRenderer.send(ack_type, CommaNum(val)); } else { ipcRenderer.send(ack_type, "Unlimited"); } }, toggle_limit_by_time: function() { engineconfig[this.engine.filepath].limit_by_time = !engineconfig[this.engine.filepath].limit_by_time; this.send_ack_limit_by_time(); this.handle_search_params_change(); }, send_ack_limit_by_time: function() { ipcRenderer.send("ack_limit_by_time", engineconfig[this.engine.filepath].limit_by_time); }, // --------------------------------------------------------------------------------------------------------------------- // Engine-related acks... send_ack_engine: function() { this.engine.send_ack_engine(); }, send_ack_setoption: function(name) { this.engine.send_ack_setoption(name); }, // --------------------------------------------------------------------------------------------------------------------- // Misc engine methods... soft_engine_reset: function() { this.set_behaviour("halt"); // Will cause "stop" to be sent. this.engine.send_ucinewgame(); // Must happen after "stop" is sent. }, forget_analysis: function() { CleanTree(this.tree.root); this.tree.node.table.autopopulate(this.tree.node); this.set_behaviour("halt"); // Will cause "stop" to be sent. this.engine.send_ucinewgame(); // Must happen after "stop" is sent. this.engine.suppress_cycle_info = this.info_handler.engine_cycle; // Ignore further info updates from this cycle. }, // --------------------------------------------------------------------------------------------------------------------- // UCI options... set_uci_option: function(name, val, save_to_cfg = false, blue_text = true) { // Note that all early returns from this function need to send an ack // of the prevailing value to fix checkmarks in the main process. if (!this.engine.ever_received_uciok) { // Correct leelaish flag not yet known. alert(messages.too_soon_to_set_options); this.engine.send_ack_setoption(name); return; } if (this.engine.leelaish && name.toLowerCase() === "multipv") { this.set_special_message("MultiPV should be 500 for this engine", "blue"); this.engine.send_ack_setoption(name); return; } if (!this.engine.known(name)) { this.set_special_message(`${name} not known by this engine`, "blue"); this.engine.send_ack_setoption(name); return; } if (save_to_cfg) { if (val === null || val === undefined) { delete engineconfig[this.engine.filepath].options[name]; } else { engineconfig[this.engine.filepath].options[name] = val; } } if (val === null || val === undefined) { val = ""; } this.set_behaviour("halt"); let sent = this.engine.setoption(name, val); // Will ack the new value. if (blue_text) { this.set_special_message(sent, "blue"); } }, set_uci_option_permanent: function(name, val) { this.set_uci_option(name, val, true); }, set_uci_option_permanent_and_cleartree: function(name, val) { this.set_uci_option(name, val, true); if (this.engine.leelaish) { this.set_uci_option("ClearTree", true, false, false); } }, disable_syzygy: function() { delete engineconfig[this.engine.filepath].options["SyzygyPath"]; this.restart_engine(); // Causes the correct ack to be sent. }, auto_weights: function() { delete engineconfig[this.engine.filepath].options["EvalFile"]; delete engineconfig[this.engine.filepath].options["WeightsFile"]; this.restart_engine(); // Causes the correct acks to be sent. }, // --------------------------------------------------------------------------------------------------------------------- // Engine startup... reload_engineconfig: function() { [load_err2, engineconfig] = engineconfig_io.load(); if (load_err2) { alert(load_err2); } this.restart_engine(); }, switch_engine: function(filename) { this.set_behaviour("halt"); if (this.engine_start(filename)) { config.path = filename; } else { alert("Failed to start this engine."); this.engine.send_ack_engine(); } }, restart_engine: function() { this.engine.warn_send_fail = false; // Don't want "send failed" warnings from old engine any more. this.set_behaviour("halt"); if (this.engine_start(config.path)) { // pass } else { alert("Failed to restart the engine."); this.engine.send_ack_engine(); } }, engine_start: function(filepath, blue_fail) { if (!filepath || typeof filepath !== "string" || fs.existsSync(filepath) === false) { if (blue_fail && !load_err1 && !load_err2) { this.err_receive(`${messages.engine_not_present}`); this.err_receive(""); } return false; } let args = engineconfig[filepath] ? engineconfig[filepath].args : []; let new_engine = NewEngine(this); let success = new_engine.setup(filepath, args, this); if (success === false) { if (blue_fail && !load_err1 && !load_err2) { this.err_receive(`${messages.engine_failed_to_start}`); this.err_receive(""); } return false; } this.engine.shutdown(); this.engine = new_engine; // Don't reuse engine objects, not even the dummy object. There are sync issues due to fake "go"s. if (!engineconfig[this.engine.filepath]) { engineconfig[this.engine.filepath] = engineconfig_io.newentry(); console.log(`Creating new entry in engineconfig for ${filepath}`); } this.engine.send("uci"); this.send_ack_node_limit(false); // Ack the node limits that are set in engineconfig[this.engine.filepath] this.send_ack_node_limit(true); this.send_ack_limit_by_time(); // Also ack the limit_by_time boolean for that menu item. this.info_handler.reset_engine_info(); this.info_handler.must_draw_infobox(); // To display the new stderr log that appears. return true; }, engine_send_all_options: function() { // The engine should never have been given a "go" before this. // Options that are sent regardless of whether the engine seems to know about them... let forced_engine_options = this.engine.leelaish ? forced_lc0_options : forced_ab_options; for (let [key, value] of Object.entries(forced_engine_options)) { this.engine.setoption(key, value); } // Standard options... only sent if the engine has said it knows them... let standard_engine_options = this.engine.leelaish ? standard_lc0_options : standard_ab_options; for (let [key, value] of Object.entries(standard_engine_options)) { if (this.engine.known(key)) { this.engine.setoption(key, value); } } // Now send user-selected options. Thus, the user can override anything above. let options = engineconfig[this.engine.filepath].options; let keys = Object.keys(options); keys.sort((a, b) => { // "It is recommended to set Hash after setting Threads." if (a.toLowerCase() === "hash" && b.toLowerCase() !== "hash") return 1; if (a.toLowerCase() !== "hash" && b.toLowerCase() === "hash") return -1; return 0; }); for (let key of keys) { this.engine.setoption(key, options[key]); } }, // --------------------------------------------------------------------------------------------------------------------- // Tree manipulation methods... move: function(s) { // It is safe to call this with illegal moves. if (typeof s !== "string") { console.log(`hub.move(${s}) - bad argument`); return false; } let board = this.tree.node.board; let source = Point(s.slice(0, 2)); if (!source) { console.log(`hub.move(${s}) - invalid source`); return false; } // First deal with old-school castling in Standard Chess... s = board.c960_castling_converter(s); // If a promotion character is required and not present, show the promotion chooser and return // without committing to anything. if (s.length === 4) { if ((board.piece(source) === "P" && source.y === 1) || (board.piece(source) === "p" && source.y === 6)) { let illegal_reason = board.illegal(s + "q"); if (illegal_reason) { console.log(`hub.move(${s}) - ${illegal_reason}`); } else { this.show_promotiontable(s); } return false; } } // The promised legality check... let illegal_reason = board.illegal(s); if (illegal_reason) { console.log(`hub.move(${s}) - ${illegal_reason}`); return false; } this.tree.make_move(s); this.position_changed(); return true; }, random_move: function() { let legals = this.tree.node.board.movegen(); if (legals.length > 0) { this.move(RandChoice(legals)); } }, play_info_index: function(n) { let line_starts = this.info_handler.info_clickers.filter(o => o.is_start); if (n < line_starts.length) { let move = line_starts[n].move; let table_move = this.tree.node.table.moveinfo[move]; if (table_move && table_move.__touched) { // Allow this to happen if the move is touched this.move(move); } else if (config.looker_api) { // Allow this to happen if the move is in the selected API database let db_entry = this.looker.lookup(config.looker_api, this.tree.node.board); if (db_entry && db_entry.moves[move]) { this.move(move); } } } }, // Note that the various tree.methods() return whether or not the current node changed. return_to_lock: function() { if (config.behaviour === "analysis_locked") { if (this.tree.set_node(this.leela_lock_node)) { // Fool-proof against null / destroyed. this.position_changed(false, true); } } }, prev: function() { if (this.tree.prev()) { this.position_changed(false, true); } }, next: function() { if (this.tree.next()) { this.position_changed(false, true); } }, goto_root: function() { if (this.tree.goto_root()) { this.position_changed(false, true); } }, goto_end: function() { if (this.tree.goto_end()) { this.position_changed(false, true); } }, previous_sibling: function() { if (this.tree.previous_sibling()) { this.position_changed(false, true); } }, next_sibling: function() { if (this.tree.next_sibling()) { this.position_changed(false, true); } }, return_to_main_line: function() { if (this.tree.return_to_main_line()) { this.position_changed(false, true); } }, delete_node: function() { if (this.tree.delete_node()) { this.position_changed(false, true); } }, promote_to_main_line: function() { this.tree.promote_to_main_line(); }, promote: function() { this.tree.promote(); }, delete_other_lines: function() { this.tree.delete_other_lines(); }, delete_children: function() { this.tree.delete_children(); }, delete_siblings: function() { this.tree.delete_siblings(); }, // --------------------------------------------------------------------------------------------------------------------- new_game: function() { this.load_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); }, new_960: function(n) { if (n === undefined) { n = RandInt(0, 960); } this.load_fen(c960_fen(n), true); }, // --------------------------------------------------------------------------------------------------------------------- pgn_to_clipboard: function() { PGNToClipboard(this.tree.node); }, save: function(filename) { SavePGN(filename, this.tree.node); }, // --------------------------------------------------------------------------------------------------------------------- // Loading PGN... open: function(filename) { if (filename === __dirname || filename === ".") { // Can happen when extra args are passed to main process. Silently return. return; } if (fs.existsSync(filename) === false) { // Can happen when extra args are passed to main process. Silently return. return; } if (!config.ignore_filesize_limits && FileExceedsGigabyte(filename, 2)) { alert(messages.file_too_big); return; } for (let loader of this.loaders) { if (loader.type === "pgn") { loader.shutdown(); } } console.log(`Loading PGN: ${filename}`); let loader = NewFastPGNLoader(filename, (err, pgndata) => { if (!err) { pgndata.source = path.basename(filename); this.handle_loaded_pgndata(pgndata); } else { console.log(err); } }); this.loaders.push(loader); }, handle_loaded_pgndata: function(pgndata) { if (!pgndata || pgndata.count() === 0) { alert("No data found."); return; } if (pgndata.count() === 1) { let success = this.load_pgn_object(pgndata.getrecord(0)); if (success) { this.pgndata = pgndata; this.pgn_choices_start = 0; } } else { this.pgndata = pgndata; this.pgn_choices_start = 0; this.show_pgn_chooser(); } }, load_pgn_object: function(o) { // Returns true or false - whether this actually succeeded. let root_node; try { root_node = LoadPGNRecord(o); } catch (err) { alert(err); return false; } this.tree.replace_tree(root_node); this.position_changed(true, true); return true; }, // --------------------------------------------------------------------------------------------------------------------- // Books... unload_book: function() { this.book = null; for (let loader of this.loaders) { if (loader.type === "book") { loader.shutdown(); } } this.send_ack_book(); }, load_polyglot_book: function(filename) { if (!config.ignore_filesize_limits && FileExceedsGigabyte(filename, 2)) { alert(messages.file_too_big); this.send_ack_book(); return; } this.book = null; this.send_ack_book(); for (let loader of this.loaders) { if (loader.type === "book") { loader.shutdown(); } } console.log(`Loading Polyglot book: ${filename}`); let loader = NewPolyglotBookLoader(filename, (err, data) => { if (!err) { if (BookSortedTest(data)) { this.book = data; this.explorer_objects_cache = null; this.send_ack_book(); this.set_special_message(`Finished loading book (moves: ${Math.floor(data.length / 16)})`, "green"); } else { alert(messages.bad_bin_book); } } else { console.log(err); } }); this.loaders.push(loader); }, load_pgn_book: function(filename) { if (!config.ignore_filesize_limits && FileExceedsGigabyte(filename, 0.02)) { alert(messages.pgn_book_too_big); this.send_ack_book(); return; } this.book = null; this.send_ack_book(); for (let loader of this.loaders) { if (loader.type === "book") { loader.shutdown(); } } console.log(`Loading PGN book: ${filename}`); let loader = NewPGNBookLoader(filename, (err, data) => { if (!err) { this.book = data; this.explorer_objects_cache = null; this.send_ack_book(); this.set_special_message(`Finished loading book (moves: ${data.length})`, "green"); } else { console.log(err); } }); this.loaders.push(loader); }, send_ack_book: function() { let msg = false; if (this.book) { msg = this.book instanceof Buffer ? "polyglot" : "pgn"; } ipcRenderer.send("ack_book", msg); }, // --------------------------------------------------------------------------------------------------------------------- // Loading from clipboard or fenbox... load_fen_or_pgn_from_string: function(s) { if (typeof s !== "string") return; s = s.trim(); try { LoadFEN(s); // Used as a test. Throws on any error. this.load_fen(s); } catch (err) { this.load_pgn_from_string(s); } }, load_pgn_from_string: function(s) { if (typeof s !== "string") { return; } let buf = Buffer.from(s); console.log(`Loading PGN from string...`); for (let loader of this.loaders) { if (loader.type === "pgn") { loader.shutdown(); } } let loader = NewFastPGNLoader(buf, (err, pgndata) => { if (!err) { pgndata.source = "From clipboard"; this.handle_loaded_pgndata(pgndata); } else { console.log(err); } }); this.loaders.push(loader); }, load_fen: function(s, abnormal) { let board; try { board = LoadFEN(s); // If the FEN loader thought it looked like normal chess, we must // override it if the caller passed the abnormal flag. Note that // it is never permissible to go in the opposite direction... if // the loader thought it was abnormal, we never say it's normal. if (abnormal) { board.normalchess = false; } } catch (err) { alert(err); return; } this.tree.replace_tree(NewRoot(board)); this.position_changed(true, true); }, load_from_fenbox: function(s) { s = s.trim(); if (s === this.tree.node.board.fen(true)) { return; } let abnormal = false; // Allow loading a Chess 960 position by giving its ID: if (s.length <= 3) { let n = parseInt(s, 10); if (Number.isNaN(n) === false && n < 960) { s = c960_fen(n); abnormal = true; } } // Allow loading a fruity start position by giving the pieces: if (s.length === 8) { let ok = true; for (let c of s) { if (["K", "k", "Q", "q", "R", "r", "B", "b", "N", "n"].includes(c) === false) { ok = false; break; } } if (ok) { s = `${s.toLowerCase()}/pppppppp/8/8/8/8/PPPPPPPP/${s.toUpperCase()} w KQkq - 0 1`; abnormal = true; } } this.load_fen(s, abnormal); }, // --------------------------------------------------------------------------------------------------------------------- // Mouse and mouseclicks... set_active_square: function(new_point) { // We do this immediately so it's snappy and responsive, rather than waiting for the next draw cycle. But we don't // want to actually call draw() here since whatever called this may well end up triggering a draw anyway. let old_point = this.active_square; if (old_point) { let td = document.getElementById("underlay_" + old_point.s); td.style["background-color"] = "transparent"; this.dirty_squares[old_point.x][old_point.y] = 0; // Lame. This is the constant for EMPTY. } if (new_point) { let td = document.getElementById("underlay_" + new_point.s); td.style["background-color"] = config.active_square; this.dirty_squares[new_point.x][new_point.y] = 2; // Lame. This is the constant for ACTIVE. } this.active_square = new_point ? new_point : null; }, boardfriends_click: function(event) { let s = EventPathString(event, "overlay_"); let p = Point(s); if (!p) { return; } this.hide_promotiontable(); // Just in case it's up. let ocm = this.info_handler.one_click_moves[p.x][p.y]; let board = this.tree.node.board; if (!this.active_square && ocm && board.colour(p) !== board.active) { // Note that we test colour difference this.set_active_square(null); // to disallow castling moves from OCM this.move(ocm); // since the dest is the rook (which return; // the user might want to click on.) } if (this.active_square) { let move = this.active_square.s + p.s; // e.g. "e2e4" - note promotion char is handled by hub.move() this.set_active_square(null); let ok = this.move(move); if (!ok && config.click_spotlight) { // No need to worry about spotlight arrows if the move actually happened this.draw_canvas_arrows(); } return; } // So there is no active_square... create one? if (board.active === "w" && board.is_white(p)) { this.set_active_square(p); if (config.click_spotlight) { this.draw_canvas_arrows(); } } if (board.active === "b" && board.is_black(p)) { this.set_active_square(p); if (config.click_spotlight) { this.draw_canvas_arrows(); } } }, infobox_click: function(event) { if (this.info_handler.clickers_are_valid_for_node(this.tree.node) === false) { return; } let n = EventPathN(event, "infobox_"); let moves = this.info_handler.moves_from_click_n(n); if (!moves || moves.length === 0) { // We do assume length > 0 below. this.maybe_searchmove_click(event); return; } // So it appears to be a real click in the infobox......................................... // I doubt moves can be an illegal sequence now but this check is not too expensive here... let illegal_reason = this.tree.node.board.sequence_illegal(moves); if (illegal_reason) { console.log("infobox_click(): " + illegal_reason); return; } switch (config.pv_click_event) { case 0: return; case 1: this.tree.make_move_sequence(moves); this.position_changed(false, true); return; case 2: this.tree.add_move_sequence(moves); return; } }, maybe_searchmove_click: function(event) { let sm = EventPathString(event, "searchmove_"); if (typeof sm !== "string" || (sm.length < 4 || sm.length > 5)) { return; } if (this.tree.node.searchmoves.includes(sm)) { this.tree.node.searchmoves = this.tree.node.searchmoves.filter(move => move !== sm); } else { this.tree.node.searchmoves.push(sm); } this.tree.node.searchmoves.sort(); this.handle_search_params_change(); }, movelist_click: function(event) { if (this.tree.handle_click(event)) { this.position_changed(false, true); } }, winrate_click: function(event) { let node = this.grapher.node_from_click(this.tree.node, event); if (!node) { return; } if (this.tree.set_node(node)) { this.position_changed(false, true); } }, statusbox_click: function(event) { if (EventPathString(event, "gobutton")) { this.set_behaviour("analysis_free"); return; } if (EventPathString(event, "haltbutton")) { this.set_behaviour("halt"); return; } if (EventPathString(event, "lock_return")) { this.return_to_lock(); return; } if (EventPathString(event, "loadabort")) { for (let loader of this.loaders) { loader.shutdown(); } return; } }, fullbox_click: function(event) { let n; // Config item editor... if (EventPathString(event, "config_item_save") !== null) { if (event.button !== 2) { this.apply_fullbox_config_item_edit(); } return; } if (EventPathString(event, "config_item_cancel") !== null) { this.hide_fullbox(); return; } if (EventPathString(event, "config_item_web_link") !== null) { ipcRenderer.send("web_link", this.fullbox_web_link); return; } // PGN chooser... n = EventPathN(event, "pgn_chooser_"); if (typeof n === "number") { if (this.pgndata && n >= 0 && n < this.pgndata.count()) { this.load_pgn_object(this.pgndata.getrecord(n)); } return; } // PGN chooser, prev / next page buttons... n = EventPathN(event, "pgn_index_chooser_"); if (typeof n !== "number") { n = EventPathN(event, "pgn_index_b_chooser_"); } if (typeof n === "number") { this.pgn_choices_start = n; this.show_pgn_chooser(); return; } // Engine chooser... n = EventPathN(event, "engine_chooser_"); if (typeof n === "number") { let filepath = this.engine_choices[n]; // The array is remade every time the fast engine chooser is displayed if (filepath) { if (event.button === 2) { // Right-click if (this.engine.filepath !== filepath) { delete engineconfig[filepath]; this.show_fast_engine_chooser(); } } else { // Any other click this.switch_engine(filepath); this.hide_fullbox(); } } return; } }, promotiontable_click: function(event) { let s = EventPathString(event, "promotion_chooser_"); this.hide_promotiontable(); this.move(s); }, handle_file_drop: function(event) { if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0] && get_path_for_file(event.dataTransfer.files[0])) { this.open(get_path_for_file(event.dataTransfer.files[0])); return; } }, mouse_point: function() { let overlist = document.querySelectorAll(":hover"); for (let item of overlist) { if (typeof item.id === "string" && item.id.startsWith("overlay_")) { return Point(item.id.slice(8)); // Possibly null } } return null; }, // --------------------------------------------------------------------------------------------------------------------- // Settings (but NOT including UCI options)... toggle: function(option) { // Cases with their own handler... if (option === "flip") { this.toggle_flip(); return; } // Normal cases... config[option] = !config[option]; // Cases that have additional actions after... if (option === "book_explorer") { config.lichess_explorer = false; this.explorer_objects_cache = null; } if (option === "lichess_explorer") { config.book_explorer = false; this.explorer_objects_cache = null; } if (option === "look_past_25") { if (config.look_past_25 && this.tree.node.board.fullmove > 25) { this.looker.add_to_queue(this.tree.node.board); } } if (option === "searchmoves_buttons") { this.tree.node.searchmoves = []; // This is reasonable regardless of which way the toggle went. this.handle_search_params_change(); } this.info_handler.must_draw_infobox(); this.draw(); }, toggle_flip: function() { // config.flip should not be directly set, call this function instead. config.flip = !config.flip; for (let x = 0; x < 8; x++) { for (let y = 0; y < 4; y++) { let first = document.getElementById(`overlay_${S(x, y)}`); let second = document.getElementById(`overlay_${S(7 - x, 7 - y)}`); SwapElements(first, second); first = document.getElementById(`underlay_${S(x, y)}`); second = document.getElementById(`underlay_${S(7 - x, 7 - y)}`); SwapElements(first, second); } } this.draw(); // For the canvas stuff. }, set_arrow_filter: function(type, value) { config.arrow_filter_type = type; config.arrow_filter_value = value; this.draw(); }, set_looker_api: function(value) { if (config.looker_api === value) { return; } config.looker_api = value; if (value && value.includes("lichess") && !config.lichess_token) { alert(messages.lichess_token_needed); } this.looker.clear_queue(); if (value) { this.looker.add_to_queue(this.tree.node.board); } this.explorer_objects_cache = null; }, invert_searchmoves: function() { if (!config.searchmoves_buttons || Array.isArray(this.tree.node.searchmoves) === false) { return; } // It's no disaster if the result is wrong somehow, because // searchmoves are validated before being sent to Leela. let moveset = Object.create(null); for (let move of Object.keys(this.tree.node.table.moveinfo)) { moveset[move] = true; } for (let move of this.tree.node.searchmoves) { delete moveset[move]; } this.tree.node.searchmoves = Object.keys(moveset); this.tree.node.searchmoves.sort(); this.handle_search_params_change(); }, clear_searchmoves: function() { this.tree.node.searchmoves = []; this.handle_search_params_change(); }, set_pgn_font_size: function(n) { movelist.style["font-size"] = n.toString() + "px"; fenbox.style["font-size"] = n.toString() + "px"; config.pgn_font_size = n; config.fen_font_size = n; }, set_arrow_size: function(width, radius, fontsize) { config.arrow_width = width; config.arrowhead_radius = radius; config.board_font = `${fontsize}px Arial`; }, set_info_font_size: function(n) { infobox.style["font-size"] = n.toString() + "px"; statusbox.style["font-size"] = n.toString() + "px"; fullbox.style["font-size"] = n.toString() + "px"; config.info_font_size = n; this.rebuild_sizes(); }, set_graph_height: function(sz) { config.graph_height = sz; this.rebuild_sizes(); this.grapher.draw(this.tree.node, true); }, set_board_size: function(sz) { config.square_size = Math.floor(sz / 8); config.board_size = config.square_size * 8; this.rebuild_sizes(); }, change_piece_set: function(directory) { if (directory) { if (images.validate_folder(directory) === false) { alert(messages.invalid_pieces_directory); return; } images.load_from(directory); } else { directory = null; images.load_from(path.join(__dirname, "pieces")); } this.friendly_draws = New2DArray(8, 8, null); this.enemy_draws = New2DArray(8, 8, null); config["override_piece_directory"] = directory; }, change_background: function(file, config_save = true) { if (file && fs.existsSync(file)) { let img = new Image(); img.src = file; // Automagically gets converted to "file:///C:/foo/bar/whatever.png" boardsquares.style["background-image"] = `url("${img.src}")`; } else { boardsquares.style["background-image"] = background(config.light_square, config.dark_square, config.square_size); } if (config_save) { config.override_board = file; } }, rebuild_sizes: function() { // This assumes everything already exists. // Derived from the longer version in start.js, which it does not replace. boardfriends.width = canvas.width = boardsquares.width = config.board_size; boardfriends.height = canvas.height = boardsquares.height = config.board_size; rightgridder.style["height"] = `${canvas.height}px`; for (let y = 0; y < 8; y++) { for (let x = 0; x < 8; x++) { let td1 = document.getElementById("underlay_" + S(x, y)); let td2 = document.getElementById("overlay_" + S(x, y)); td1.width = td2.width = config.square_size; td1.height = td2.height = config.square_size; } } if (config.graph_height <= 0) { graph.style.display = "none"; } else { graph.style.height = config.graph_height.toString() + "px"; graph.style.display = ""; } promotiontable.style.left = (boardsquares.offsetLeft + config.square_size * 2).toString() + "px"; promotiontable.style.top = (boardsquares.offsetTop + config.square_size * 3.5).toString() + "px"; promotiontable.style["background-color"] = config.active_square; this.draw(); }, save_window_size: function() { let zoomfactor = parseFloat(querystring.parse(global.location.search.slice(1))["zoomfactor"]); config.width = Math.floor(window.innerWidth * zoomfactor); config.height = Math.floor(window.innerHeight * zoomfactor); }, set_logfile: function(filename) { // Arg can be null to stop logging. config.logfile = null; Log("Stopping log."); // This will do nothing, but calling Log() forces it to close any open file. config.logfile = filename; this.send_ack_logfile(); }, set_language: function(s) { config.language = s; alert(translate.t("RESTART_REQUIRED", s)); }, send_ack_logfile: function() { ipcRenderer.send("ack_logfile", config.logfile); }, save_config: function() { if (!load_err1) { // If the config file was broken, never save to it, let the user fix it. config_io.save(config); } }, save_engineconfig: function() { if (!load_err2) { // If the config file was broken, never save to it, let the user fix it. engineconfig_io.save(engineconfig); } }, // --------------------------------------------------------------------------------------------------------------------- // Misc... quit: function() { this.engine.shutdown(); this.save_config(); this.save_engineconfig(); ipcRenderer.send("terminate"); }, set_special_message: function(s, css_class, duration) { this.status_handler.set_special_message(s, css_class, duration); this.draw_statusbox(); }, infobox_to_clipboard: function() { let s = infobox.innerText; s = ReplaceAll(s, `${config.focus_on_text} `, ""); s = ReplaceAll(s, `${config.focus_off_text} `, ""); clipboard.writeText(this.tree.node.board.fen(true) + "\n" + statusbox.innerText + "\n\n" + s); }, send_title: function() { let title = "Nibbler"; let root = this.tree.root; if (root.tags && root.tags.White && root.tags.White !== "White" && root.tags.Black && root.tags.Black !== "Black") { title += `: ${root.tags.White} - ${root.tags.Black}`; } ipcRenderer.send("set_title", UnsafeStringHTML(title)); // Fix any & and that sort of thing in the names. }, generate_simple_book: function() { // For https://github.com/rooklift/lc0_lichess let histories = this.tree.root.end_nodes().map(end => end.history_old_format()); let text_lines = histories.map(h => "\t\"" + h.join(" ") + "\""); console.log("[\n" + text_lines.join(",\n") + "\n]"); }, run_script: function(filename) { const disallowed = ["position", "go", "stop", "ponderhit", "quit"]; let buf; try { buf = fs.readFileSync(filename); } catch (err) { alert(err); return; } this.set_behaviour("halt"); let s = buf.toString(); let lines = s.split("\n").map(z => z.trim()).filter(z => z !== ""); if (!config.allow_arbitrary_scripts) { for (let line of lines) { for (let d of disallowed) { if (line.startsWith(d)) { this.set_special_message(`${messages.invalid_script}`, "yellow"); console.log(`Refused to run script: ${filename}`); return; } } } } console.log(`Running script: ${filename}`); for (let line of lines) { if (config.allow_arbitrary_scripts) { this.engine.send(line, true); // Force mode, so setoptions don't get held back } else { this.engine.send(line); } console.log(line); } this.set_special_message(`${path.basename(filename)}: Sent ${lines.length} lines`, "blue"); }, fire_gc: function() { if (!global || !global.gc) { alert("Unable."); } else { global.gc(); } }, log_ram: function() { console.log(`RAM after ${Math.floor(performance.now() / 1000)} seconds:`); for (let foo of Object.entries(process.memoryUsage())) { let type = foo[0] + " ".repeat(12 - foo[0].length); let mb = foo[1] / (1024 * 1024); let mb_rounded = Math.floor(mb * 1000) / 1000; // 3 d.p. console.log(type, "(MB)", mb_rounded); } }, console: function(...args) { console.log(...args); }, toggle_debug_css: function() { let ss = document.styleSheets[0]; let i = 0; for (let rule of Object.values(ss.cssRules)) { if (rule.selectorText && rule.selectorText === "*") { ss.deleteRule(i); return; } i++; } ss.insertRule("* {outline: 1px dotted red;}"); }, // --------------------------------------------------------------------------------------------------------------------- // Fullbox (our full size info div)... show_pgn_chooser: function() { const interval = 100; if (!this.pgndata || this.pgndata.count() === 0) { fullbox_content.innerHTML = `No PGN loaded`; this.show_fullbox(); return; } let count = this.pgndata.count(); if (this.pgn_choices_start >= count) { this.pgn_choices_start = Math.floor((count - 1) / interval) * interval; } if (this.pgn_choices_start < 0) { // The most important thing, values < 0 will crash. this.pgn_choices_start = 0; } let lines = []; let max_ordinal_length = count.toString().length; let prevnextfoo = (count > interval) ? // All these values get fixed on function entry if they're out-of-bounds. ids should be unique. `Start |` + ` <<<< |` + ` <<< |` + ` << |` + ` >> |` + ` >>> |` + ` >>>> |` + ` End (${count}) ` + `— ${this.pgndata.source}` : `${this.pgndata.source}`; lines.push(prevnextfoo); lines.push("
    "); for (let n = this.pgn_choices_start; n < this.pgn_choices_start + interval; n++) { if (n < count) { let pad = n < 10 ? " " : ""; let p = this.pgndata.getrecord(n); let s; if (p.tags.Result === "1-0") { s = `${pad}${n}. ${p.tags.White || "Unknown"} - ${p.tags.Black || "Unknown"}`; } else if (p.tags.Result === "0-1") { s = `${pad}${n}. ${p.tags.White || "Unknown"} - ${p.tags.Black || "Unknown"}`; } else { s = `${pad}${n}. ${p.tags.White || "Unknown"} - ${p.tags.Black || "Unknown"}`; } if (p.tags.Opening && p.tags.Opening !== "?") { s += ` (${p.tags.Opening})`; } else if (p.tags.Variant && p.tags.Variant.toLowerCase() !== "standard" && p.tags.Variant.toLowerCase() !== "from position") { s += ` (${p.tags.Variant})`; } lines.push(`
  • ${s}
  • `); } else if (count > interval) { // Pad the chooser with blank lines so the buttons at the bottom behave nicely. This is stupid though. lines.push(`
  • ${n}.${n === count ? " [end]" : ""}
  • `); } } lines.push("
"); if (count > interval) { prevnextfoo = ReplaceAll(prevnextfoo, `span id="pgn_index_chooser_`, `span id="pgn_index_b_chooser_`); // id should be unique per element. lines.push(prevnextfoo); } fullbox_content.innerHTML = lines.join(""); this.show_fullbox(); }, show_sent_options: function() { let lines = []; lines.push(`${this.engine.filepath || "No engine loaded"}`); lines.push(""); for (let name of Object.keys(this.engine.sent_options)) { lines.push(`${name}
${this.engine.sent_options[name]}`); } fullbox_content.innerHTML = lines.join("
"); this.show_fullbox(); }, show_error_log: function() { fullbox_content.innerHTML = this.info_handler.error_log; this.show_fullbox(); }, parse_fullbox_config_item_value: function(item_name, raw) { raw = raw.trim(); let defaults_has_item = Object.prototype.hasOwnProperty.call(config_io.defaults, item_name); let expected = defaults_has_item ? config_io.defaults[item_name] : config[item_name]; if (Array.isArray(expected)) { try { let parsed = JSON.parse(raw); if (Array.isArray(parsed)) { return [parsed, null]; } return [null, "Expected JSON array"]; } catch (err) { return [null, "Expected JSON array"]; } } if (typeof expected === "string") { return [raw, null]; } if (typeof expected === "number") { let n = Number(raw); if (Number.isNaN(n)) { return [null, "Expected number"]; } return [n, null]; } if (typeof expected === "boolean") { let s = raw.toLowerCase(); if (s === "true" || s === "1" || s === "yes" || s === "on") { return [true, null]; } if (s === "false" || s === "0" || s === "no" || s === "off") { return [false, null]; } return [null, `Expected boolean (true / false)`]; } if (expected === null) { // Null defaults are usually nullable strings. if (raw.toLowerCase() === "null") { return [null, null]; } return [raw, null]; } if (typeof expected === "object") { try { let parsed = JSON.parse(raw); if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed) === false) { return [parsed, null]; } return [null, "Expected JSON object"]; } catch (err) { return [null, "Expected JSON object"]; } } return [raw, null]; }, apply_fullbox_config_item_edit: function() { if (typeof this.fullbox_config_item !== "string") { return; } let textarea = document.getElementById("config_item_input"); if (!textarea) { return; } let [value, err] = this.parse_fullbox_config_item_value(this.fullbox_config_item, textarea.value); if (err) { let errdiv = document.getElementById("config_item_error"); if (errdiv) { errdiv.innerHTML = `${SafeStringHTML(err)}`; } return; } config[this.fullbox_config_item] = value; this.info_handler.must_draw_infobox(); this.draw(); this.hide_fullbox(); }, show_config_item_editor: function(item_name, web_link = null, web_text = "See web") { if (typeof item_name !== "string" || item_name === "") { return; } if (!Object.prototype.hasOwnProperty.call(config, item_name)) { fullbox_content.innerHTML = `Unknown config item: ${SafeStringHTML(item_name)}

` + `Cancel`; this.show_fullbox(); return; } this.fullbox_config_item = item_name; this.fullbox_web_link = web_link; let current = config[item_name]; let expected = Object.prototype.hasOwnProperty.call(config_io.defaults, item_name) ? config_io.defaults[item_name] : current; let expected_type = Array.isArray(expected) ? "array" : (expected === null ? "string or null" : typeof expected); let current_text = SafeStringHTML(stringify(current)); let initial_input; if (typeof current === "string") { initial_input = current; } else if (current !== null && typeof current === "object") { try { initial_input = JSON.stringify(current, null, "\t"); } catch (err) { initial_input = stringify(current); } } else { initial_input = stringify(current); } let lines = []; lines.push(`
Editing: config.${SafeStringHTML(item_name)}
`); lines.push(`
Current: ${current_text}
`); if (web_link) { lines.push(`
${SafeStringHTML(web_text)}:
`); lines.push(`
${SafeStringHTML(web_link)}
`); } lines.push(``); lines.push(`
Save | Cancel
`); lines.push(`
 
`); fullbox_content.innerHTML = lines.join(""); this.show_fullbox(); let textarea = document.getElementById("config_item_input"); if (textarea) { textarea.value = initial_input; textarea.focus(); textarea.select(); } }, show_fast_engine_chooser: function() { this.engine_choices = []; let divs = []; for (let filepath of Object.keys(engineconfig)) { if (filepath === "") { continue; } let ac = (this.engine.filepath === filepath) ? ` (active)` : ""; divs.push(`
${path.dirname(filepath)}` + `
${path.basename(filepath)}${ac}
`); this.engine_choices.push(filepath); // After the above calc using length } if (divs.length === 0) { divs.push(`
No engines known yet.
`); } else { divs.unshift(`
Click to load.
Right-click to remove from ${engineconfig_io.filename}.
`); } fullbox_content.innerHTML = divs.join(""); this.show_fullbox(); }, // --------------------------------------------------------------------------------------------------------------------- // Showing and hiding things... show_promotiontable: function(partial_move) { let pieces = this.tree.node.board.active === "w" ? ["Q", "R", "B", "N"] : ["q", "r", "b", "n"]; for (let piece of pieces) { let td = document.getElementsByClassName("promotion_" + piece.toLowerCase())[0]; // Our 4 TDs each have a unique class. td.id = "promotion_chooser_" + partial_move + piece.toLowerCase(); // We store the actual move in the id. td.width = config.square_size; td.height = config.square_size; td.style["background-image"] = images[piece].string_for_bg_style; } promotiontable.style.display = "block"; }, hide_promotiontable: function() { promotiontable.style.display = "none"; }, show_fullbox: function() { this.set_behaviour("halt"); this.hide_promotiontable(); fullbox.style.display = "block"; }, hide_fullbox: function() { this.fullbox_config_item = null; this.fullbox_web_link = null; fullbox.style.display = "none"; }, escape: function() { // Set things into a clean state. this.hide_fullbox(); this.hide_promotiontable(); if (this.active_square) { this.set_active_square(null); if (config.click_spotlight) { this.draw_canvas_arrows(); } } }, }; ================================================ FILE: files/src/renderer/97_drag.js ================================================ "use strict" // Drag improvements submitted by ObnubiladO in PR #291 // Back in the day, something like this would be referenced in hub, but nowadays I leave such things in global space. const drag_handler = { drag_state: null, cancel_drag: function() { // Must also be called after a successful drag. (Maybe misnamed, hmm?) if (!this.drag_state) { return; } if (this.drag_state.floating) { // Drag is in progress... hub.set_active_square(null); this.drag_state.floating.remove(); this.drag_state.floating = null; // Not strictly needed. } this.drag_state.from_element.style.opacity = ""; this.drag_state = null; document.body.classList.remove("dragging-piece"); if (config.click_spotlight) { hub.draw_canvas_arrows(); // Might need to clear spotlight arrows. } }, mousedown_event_on_board_td: function(overlay_td, event) { if (event.button !== 0 || this.drag_state) { return; } event.preventDefault(); // I forget why? let piece_style = overlay_td.style.backgroundImage; if (!piece_style || piece_style === "none") { return; } let rect = overlay_td.getBoundingClientRect(); this.drag_state = { from_element: overlay_td, from_square: overlay_td.id.slice(8), // e.g. "e4" or similar. piece_style: piece_style, rect: rect, startX: event.clientX, startY: event.clientY, offsetX: rect.width / 2, offsetY: rect.height / 2, floating: null, // The actual element - not created until we're sure we're really dragging. }; }, mousemove_handler: function(event) { if (!this.drag_state) { return; } // I dunno if this can happen but for safety... if (!(event.buttons & 1)) { // Bitmask: right-most bit means left click is down. console.log("drag_handler: mousemove handler saw active drag state while button 1 up!") this.cancel_drag(); return; } let dx = event.clientX - this.drag_state.startX; let dy = event.clientY - this.drag_state.startY; let dist = Math.hypot(dx, dy); if (!this.drag_state.floating) { // Treat small mouse movement as a normal click so boardfriends_click keeps its select/move behavior. if (dist < 5) { return; } // Drag starting now! hub.set_active_square(Point(this.drag_state.from_square)); if (config.click_spotlight) { hub.draw_canvas_arrows(); } let floating = document.createElement("div"); // A custom ghost piece instead of HTML5 drag-and-drop. floating.style.position = "fixed"; floating.style.pointerEvents = "none"; floating.style.width = this.drag_state.rect.width + "px"; floating.style.height = this.drag_state.rect.height + "px"; floating.style.backgroundImage = this.drag_state.piece_style; floating.style.backgroundSize = "contain"; floating.style.backgroundRepeat = "no-repeat"; floating.style.zIndex = 1000; document.body.appendChild(floating); document.body.classList.add("dragging-piece"); // This is just a css change. this.drag_state.from_element.style.opacity = "0.35"; this.drag_state.floating = floating; } this.drag_state.floating.style.left = (event.clientX - this.drag_state.offsetX) + "px"; this.drag_state.floating.style.top = (event.clientY - this.drag_state.offsetY) + "px"; }, mouseup_handler: function(event) { if (hub.grapher.dragging) { hub.grapher.dragging = false; // Always stop graph dragging. } if (!this.drag_state) { return; } if (event.button !== 0) { return; } if (this.drag_state.floating) { // Real drag was in progress... let e = document.elementFromPoint(event.clientX, event.clientY); let target_element = null; while (e && e !== document.body) { if (e.id && e.id.startsWith("overlay_")) { target_element = e; break; } e = e.parentElement; } if (target_element) { let move = this.drag_state.from_square + target_element.id.slice(8); let ok = hub.move(move); if (!ok && config.click_spotlight) { // The spotlight needs to be cleared. hub.draw_canvas_arrows(); } } } this.cancel_drag(); // Final cleanup needed in all cases. } }; // Setup drag-and-drop... window.addEventListener("mousemove", (event) => { drag_handler.mousemove_handler(event); }); window.addEventListener("mouseup", (event) => { drag_handler.mouseup_handler(event); }); window.addEventListener("drop", (event) => { event.preventDefault(); if (drag_handler.drag_state) { // Ignore if handler is in the middle of internal piece drag. return; } let dt = event.dataTransfer; if (dt && dt.files && dt.files.length > 0) { hub.handle_file_drop(event); } }); window.addEventListener("blur", () => { drag_handler.cancel_drag(); }); window.addEventListener("mouseleave", () => { drag_handler.cancel_drag(); }); // Native dragenter / dragover prevention is required so external file drops are accepted by the window... window.addEventListener("dragenter", (event) => { event.preventDefault(); }); window.addEventListener("dragover", (event) => { event.preventDefault(); }); ================================================ FILE: files/src/renderer/99_start.js ================================================ "use strict"; // Upon first run, hopefully the prefs directory exists by now // (I think the main process makes it...) config_io.create_if_needed(config); engineconfig_io.create_if_needed(engineconfig); custom_uci.create_if_needed(); Log(""); Log("======================================================================================================================================"); Log(`Nibbler startup at ${new Date().toUTCString()}`); let hub = NewHub(); hub.engine_start(config.path, true); if (load_err1) { hub.err_receive(`While loading config.json: ${load_err1}`); hub.err_receive(""); } else if (load_err2) { hub.err_receive(`While loading engines.json: ${load_err2}`); hub.err_receive(""); } else if (config.options) { alert(messages.engine_options_reset); config.args_unused = config.args; config.options_unused = config.options; hub.save_config(); // Ensure the options object is deleted from the file. } fenbox.value = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; // We have 3 main things that get drawn to: // // - boardsquares, lowest z-level table with enemy pieces and coloured squares. // - canvas, which gets arrows drawn on it. // - boardfriends, a table with friendly pieces. // // boardsquares has its natural position, while the other three get // fixed position that is set to be on top of it. boardfriends.width = canvas.width = boardsquares.width = config.board_size; boardfriends.height = canvas.height = boardsquares.height = config.board_size; rightgridder.style["height"] = `${canvas.height}px`; // Set up the squares in both tables. Note that, upon flips, the elements // themselves are moved to their new position, so everything works, e.g. // the x and y values are still correct for the flipped view. hub.change_background(config.override_board, false); for (let y = 0; y < 8; y++) { let tr1 = document.createElement("tr"); let tr2 = document.createElement("tr"); boardsquares.appendChild(tr1); boardfriends.appendChild(tr2); for (let x = 0; x < 8; x++) { let td1 = document.createElement("td"); let td2 = document.createElement("td"); td1.id = "underlay_" + S(x, y); td2.id = "overlay_" + S(x, y); td1.width = td2.width = config.square_size; td1.height = td2.height = config.square_size; tr1.appendChild(td1); tr2.appendChild(td2); td2.addEventListener("mousedown", (event) => { drag_handler.mousedown_event_on_board_td(td2, event); }); } } statusbox.style["font-size"] = config.info_font_size.toString() + "px"; infobox.style["font-size"] = config.info_font_size.toString() + "px"; fullbox.style["font-size"] = config.info_font_size.toString() + "px"; movelist.style["font-size"] = config.pgn_font_size.toString() + "px"; fenbox.style["font-size"] = config.fen_font_size.toString() + "px"; if (config.graph_height <= 0) { graph.style.display = "none"; } else { graph.style.height = config.graph_height.toString() + "px"; graph.style.display = ""; } // The promotion table pops up when needed... promotiontable.style.left = (boardsquares.offsetLeft + config.square_size * 2).toString() + "px"; promotiontable.style.top = (boardsquares.offsetTop + config.square_size * 3.5).toString() + "px"; promotiontable.style["background-color"] = config.active_square; // -------------------------------------------------------------------------------------------- // In bad cases of super-large trees, the UI can become unresponsive. To mitigate this, we // put user input in a queue, and drop certain user actions if needed... let input_queue = []; ipcRenderer.on("set", (event, msg) => { // Should only be for things that don't need any action except redraw. for (let [key, value] of Object.entries(msg)) { config[key] = value; } hub.info_handler.must_draw_infobox(); hub.draw(); }); let droppables = [ // If the UI is already lagging, dropping one of these won't make it feel any worse. "goto_root", "goto_end", "prev", "next", "previous_sibling", "next_sibling", "return_to_main_line", "promote_to_main_line", "promote", "delete_node", "delete_children", "delete_siblings", "delete_other_lines", "return_to_lock", "play_info_index", "clear_searchmoves", "invert_searchmoves", ]; ipcRenderer.on("call", (event, msg) => { // Adds stuff to the queue, or drops some stuff. let fn; if (typeof msg === "string") { // msg is function name if (input_queue.length > 0 && droppables.includes(msg)) { return; } fn = hub[msg].bind(hub); } else if (typeof msg === "object" && typeof msg.fn === "string" && Array.isArray(msg.args)) { // msg is object with fn and args if (input_queue.length > 0 && droppables.includes(msg.fn)) { return; } fn = hub[msg.fn].bind(hub, ...msg.args); } else { console.log("Bad call, msg was..."); console.log(msg); } if (fn) { input_queue.push(fn); } }); // The queue needs to be examined very regularly and acted upon. function input_loop() { if (input_queue.length > 0) { for (let fn of input_queue) { fn(); } input_queue = []; } setTimeout(input_loop, 10); } input_loop(); // -------------------------------------------------------------------------------------------- // We had some problems with the various clickers: we used to destroy and create // clickable objects a lot. This seemed to lead to moments where clicks wouldn't // register. // // A better approach is to use event handlers on the outer elements, and examine // the event.path to see what was actually clicked on. fullbox.addEventListener("mousedown", (event) => { hub.fullbox_click(event); }); boardfriends.addEventListener("mousedown", (event) => { hub.boardfriends_click(event); }); infobox.addEventListener("mousedown", (event) => { hub.infobox_click(event); }); movelist.addEventListener("mousedown", (event) => { hub.movelist_click(event); }); statusbox.addEventListener("mousedown", (event) => { hub.statusbox_click(event); }); promotiontable.addEventListener("mousedown", (event) => { hub.promotiontable_click(event); }); // Graph clicks and dragging, borrowed from Ogatak... graph.addEventListener("mousedown", (event) => { hub.winrate_click(event); hub.grapher.dragging = true; }); for (let s of ["mousemove", "mouseleave"]) { graph.addEventListener(s, (event) => { if (!hub.grapher.dragging) { return; } if (!event.buttons) { hub.grapher.dragging = false; return; } hub.winrate_click(event); }); } // window.addEventListener("wheel", (event) => { // Only if the PGN chooser is closed, and the mouse is over the board or graph. // (Not over the moveslist or infobox, because those can have scroll bars, which // the mouse wheel should interact with.) if (fullbox.style.display !== "none") { return; } // Not if the GUI has pending actions... if (input_queue.length > 0) { return; } let allow = false; let path = event.path || (event.composedPath && event.composedPath()); if (path) { for (let item of path) { if (item.id === "boardfriends" || item.id === "graph") { allow = true; break; } } } if (allow) { if (event.deltaY && event.deltaY < 0) input_queue.push(hub.prev.bind(hub)); if (event.deltaY && event.deltaY > 0) input_queue.push(hub.next.bind(hub)); } }); // Setup return key on FEN box... fenbox.addEventListener("keydown", (event) => { if (event.key === "Enter") { hub.load_from_fenbox(fenbox.value); } }); // Set space-bar to toggle go/halt, unless we're in the FEN box... window.addEventListener("keydown", (event) => { if (event.key === " ") { let ae = document.activeElement; if (ae.tagName !== "INPUT" && ae.tagName !== "TEXTAREA" && !ae.isContentEditable) { event.preventDefault(); // Prevent scrolling e.g. when the moves area is big enough to have a scroll bar. if (!event.repeat) { hub.toggle_go(); } } } }); window.addEventListener("resize", (event) => { hub.window_resize_time = performance.now(); }); window.addEventListener("error", (event) => { alert(messages.uncaught_exception); }, {once: true}); // Forced garbage collection. For reasons I can't begin to fathom, Node isn't // garbage collecting everything, and the heaps seems to grow and grow. It's // not what you would call a memory leak, since manually triggering the GC // does clear everything... note --max-old-space-size is another option. function force_gc() { if (!global || !global.gc) { console.log("Triggered GC not enabled."); return; } global.gc(); setTimeout(force_gc, 300000); // Once every 5 minutes or so? } setTimeout(force_gc, 300000); // Go... function enter_loop() { if (images.fully_loaded()) { hub.spin(); ipcRenderer.send("renderer_ready", null); } else { setTimeout(enter_loop, 25); } } enter_loop();