From faccfbc1c523024cb9e4cfce324c0e464a2c60cc Mon Sep 17 00:00:00 2001 From: Paul-Nicolas Madelaine Date: Mon, 29 Apr 2024 02:00:26 +0200 Subject: [PATCH] eschac --- COPYING | 661 ++++++++++++++++++++ Cargo.lock | 7 + Cargo.toml | 12 + README.md | 62 ++ src/array_vec.rs | 134 +++++ src/bitboard.rs | 177 ++++++ src/board.rs | 676 +++++++++++++++++++++ src/lib.rs | 90 +++ src/lookup.rs | 202 +++++++ src/magics.rs | 326 ++++++++++ src/position.rs | 1488 ++++++++++++++++++++++++++++++++++++++++++++++ src/rays.rs | 44 ++ src/san.rs | 219 +++++++ src/setup.rs | 659 ++++++++++++++++++++ src/uci.rs | 95 +++ tests/tests.rs | 302 ++++++++++ 16 files changed, 5154 insertions(+) create mode 100644 COPYING create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/array_vec.rs create mode 100644 src/bitboard.rs create mode 100644 src/board.rs create mode 100644 src/lib.rs create mode 100644 src/lookup.rs create mode 100644 src/magics.rs create mode 100644 src/position.rs create mode 100644 src/rays.rs create mode 100644 src/san.rs create mode 100644 src/setup.rs create mode 100644 src/uci.rs create mode 100644 tests/tests.rs diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fca901e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "eschac" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..02f1792 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "eschac" +version = "0.1.0" +edition = "2021" +authors = ["Paul-Nicolas Madelaine "] +license = "AGPL-3.0-or-later" +description = "computing chess moves" +repository = "https://git.pnm.tf/pnm/eschac" +keywords = ["chess"] + +[profile.dev] +opt-level = 3 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5fae89b --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# eschac + +A library for computing chess moves. + +## Overview + +eschac implements fast legal move generation and a copy-make interface that enforces at compile +time that no illegal move is played, with no runtime checks and no potential panics. + +## Example + +```rust +use eschac::prelude::*; + +// read a position from a text record +let setup = "7k/4P1rp/5Q2/5p2/1Pp1bP2/8/r4K1P/6R1 w - -".parse::()?; +let position = setup.validate()?; + +// read a move in algebraic notation +let san = "Ke1".parse::()?; +let m = san.to_move(&position)?; + +// play the move (note the absence of error handling) +let position = m.make(); + +// generate all the legal moves on the new position +let moves = position.legal_moves(); +for m in moves { + // print the UCI notation of each move + println!("{}", m.to_uci()); +} +``` + +## Comparison with [shakmaty](https://crates.io/crates/shakmaty) + +shakmaty is another Rust library for chess processing. It is written by Niklas Fiekas, whose work +greatly inspired the development of eschac. For most purposes, shakmaty is probably a better +option, as eschac comes short of its miriad of features. + +Both libraries have the same core features: +- vocabulary to describe the chessboard (squares, pieces, etc.) +- parsing and editing positions +- parsing standard move notations +- fast legal move generation and play + +**eschac** distinguishes itself with: +- a focus on performance +- a more compact board representation +- its use of the borrow checker to guarantee only legal moves are played + +**shakmaty** will be more suitable for a lot of applications, with: +- vocabulary to describe and work with games, not just positions +- insufficient material detection +- PGN parsing +- Zobrist hashing +- Syzygy endgame tablebases +- chess960 and other variants +- etc. + +## License + +eschac is licensed under [AGPL-3.0](./COPYING) (or any later version at your option). diff --git a/src/array_vec.rs b/src/array_vec.rs new file mode 100644 index 0000000..ad11407 --- /dev/null +++ b/src/array_vec.rs @@ -0,0 +1,134 @@ +use std::iter::ExactSizeIterator; +use std::iter::FusedIterator; +use std::mem::MaybeUninit; + +#[derive(Clone)] +pub(crate) struct ArrayVec { + len: usize, + array: [MaybeUninit; N], +} +impl ArrayVec { + #[inline] + pub(crate) fn new() -> Self { + Self { + len: 0, + array: [const { MaybeUninit::uninit() }; N], + } + } + #[inline] + pub(crate) unsafe fn push_unchecked(&mut self, m: T) { + debug_assert!(self.len < N); + unsafe { + self.array.get_unchecked_mut(self.len).write(m); + self.len = self.len.unchecked_add(1); + } + } + #[inline] + pub(crate) fn len(&self) -> usize { + self.len + } + #[inline] + pub(crate) fn get(&self, index: usize) -> Option<&T> { + if index < self.len { + Some(unsafe { self.array.as_slice().get_unchecked(index).assume_init_ref() }) + } else { + None + } + } + #[inline] + pub(crate) fn as_slice_mut(&mut self) -> &mut [T] { + unsafe { std::mem::transmute::<_, &mut [T]>(self.array.get_unchecked_mut(0..self.len)) } + } +} +impl<'l, T: Copy, const N: usize> IntoIterator for &'l ArrayVec { + type Item = T; + type IntoIter = ArrayVecIter<'l, T, N>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + ArrayVecIter { + array: self, + index: 0, + } + } +} +pub(crate) struct ArrayVecIter<'l, T: Copy, const N: usize> { + array: &'l ArrayVec, + index: usize, +} +impl<'l, T: Copy, const N: usize> Iterator for ArrayVecIter<'l, T, N> { + type Item = T; + #[inline] + fn next(&mut self) -> Option { + if self.index < self.array.len { + unsafe { + let item = self + .array + .array + .get_unchecked(self.index) + .assume_init_read(); + self.index = self.index.unchecked_add(1); + Some(item) + } + } else { + None + } + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} +impl<'l, T: Copy, const N: usize> FusedIterator for ArrayVecIter<'l, T, N> {} +impl<'l, T: Copy, const N: usize> ExactSizeIterator for ArrayVecIter<'l, T, N> { + #[inline] + fn len(&self) -> usize { + unsafe { self.array.len().unchecked_sub(self.index) } + } +} +impl IntoIterator for ArrayVec { + type Item = T; + type IntoIter = ArrayVecIntoIter; + #[inline] + fn into_iter(self) -> Self::IntoIter { + ArrayVecIntoIter { + array: self, + index: 0, + } + } +} +pub(crate) struct ArrayVecIntoIter { + array: ArrayVec, + index: usize, +} +impl Iterator for ArrayVecIntoIter { + type Item = T; + #[inline] + fn next(&mut self) -> Option { + if self.index < self.array.len { + unsafe { + let item = self + .array + .array + .get_unchecked(self.index) + .assume_init_read(); + self.index = self.index.unchecked_add(1); + Some(item) + } + } else { + None + } + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} +impl FusedIterator for ArrayVecIntoIter {} +impl ExactSizeIterator for ArrayVecIntoIter { + #[inline] + fn len(&self) -> usize { + unsafe { self.array.len().unchecked_sub(self.index) } + } +} diff --git a/src/bitboard.rs b/src/bitboard.rs new file mode 100644 index 0000000..c74e435 --- /dev/null +++ b/src/bitboard.rs @@ -0,0 +1,177 @@ +use crate::board::*; + +use std::iter::ExactSizeIterator; +use std::iter::FusedIterator; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Bitboard(pub(crate) u64); + +impl Bitboard { + #[inline] + pub fn new() -> Self { + Self(0) + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.0 == 0 + } + + #[inline] + pub fn first(&self) -> Option { + let mask = self.0; + match mask { + 0 => None, + _ => Some(unsafe { Square::transmute(mask.trailing_zeros() as u8) }), + } + } + + #[inline] + pub fn pop(&mut self) -> Option { + let Self(ref mut mask) = self; + let square = match mask { + 0 => None, + _ => Some(unsafe { Square::transmute(mask.trailing_zeros() as u8) }), + }; + *mask &= mask.wrapping_sub(1); + square + } + + #[inline] + pub fn insert(&mut self, square: Square) { + self.0 |= 1 << square as u8; + } + + #[inline] + pub fn trans(&self, direction: Direction) -> Self { + match direction { + Direction::North => Self(self.0 << 8), + Direction::NorthEast => Self(self.0 << 9) & !File::A.bitboard(), + Direction::East => Self(self.0 << 1) & !File::A.bitboard(), + Direction::SouthEast => Self(self.0 >> 7) & !File::A.bitboard(), + Direction::South => Self(self.0 >> 8), + Direction::SouthWest => Self(self.0 >> 9) & !File::H.bitboard(), + Direction::West => Self(self.0 >> 1) & !File::H.bitboard(), + Direction::NorthWest => Self(self.0 << 7) & !File::H.bitboard(), + } + } + + #[inline] + pub fn contains(&self, square: Square) -> bool { + self.0 & (1 << square as u8) != 0 + } + + #[inline] + pub fn mirror(self) -> Bitboard { + let [a, b, c, d, e, f, g, h] = self.0.to_le_bytes(); + Self(u64::from_le_bytes([h, g, f, e, d, c, b, a])) + } +} + +impl std::ops::BitOr for Bitboard { + type Output = Self; + #[inline] + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} +impl std::ops::BitAnd for Bitboard { + type Output = Self; + #[inline] + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } +} +impl std::ops::BitXor for Bitboard { + type Output = Self; + #[inline] + fn bitxor(self, rhs: Self) -> Self::Output { + Self(self.0 ^ rhs.0) + } +} +impl std::ops::BitOrAssign for Bitboard { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} +impl std::ops::BitAndAssign for Bitboard { + #[inline] + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } +} +impl std::ops::BitXorAssign for Bitboard { + #[inline] + fn bitxor_assign(&mut self, rhs: Self) { + self.0 ^= rhs.0; + } +} +impl std::ops::Not for Bitboard { + type Output = Self; + #[inline] + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl Iterator for Bitboard { + type Item = Square; + #[inline] + fn next(&mut self) -> Option { + self.pop() + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } + #[inline] + fn for_each(self, mut f: F) + where + Self: Sized, + F: FnMut(Self::Item), + { + let mut mask = self.0; + while mask != 0 { + f(unsafe { Square::transmute(mask.trailing_zeros() as u8) }); + mask &= mask.wrapping_sub(1); + } + } + #[inline] + fn fold(self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + let mut mask = self.0; + let mut acc = init; + while mask != 0 { + acc = f(acc, unsafe { + Square::transmute(mask.trailing_zeros() as u8) + }); + mask &= mask.wrapping_sub(1); + } + acc + } +} +impl FusedIterator for Bitboard {} +impl ExactSizeIterator for Bitboard { + #[inline] + fn len(&self) -> usize { + self.0.count_ones() as usize + } +} + +pub(crate) trait BitboardIterExt { + fn reduce_or(self) -> Bitboard; +} +impl BitboardIterExt for T +where + T: Iterator, +{ + #[inline] + fn reduce_or(self) -> Bitboard { + self.fold(Bitboard(0), |a, b| a | b) + } +} diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..db22a3d --- /dev/null +++ b/src/board.rs @@ -0,0 +1,676 @@ +//! Chessboard vocabulary. + +use crate::bitboard::*; + +macro_rules! container { + ($a:ident, $b:ident, $n:literal) => { + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub(crate) struct $b(pub(crate) [T; $n]); + #[allow(unused)] + impl $b { + #[inline] + pub fn new(f: F) -> Self + where + F: FnMut($a) -> T, + { + Self($a::all().map(f)) + } + #[inline] + pub fn get(&self, k: $a) -> &T { + unsafe { self.0.get_unchecked(k as usize) } + } + #[inline] + pub fn get_mut(&mut self, k: $a) -> &mut T { + unsafe { self.0.get_unchecked_mut(k as usize) } + } + } + }; +} + +/// The players. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum Color { + White, + Black, +} + +container!(Color, ByColor, 2); + +impl Color { + #[inline] + pub fn all() -> [Self; 2] { + [Self::White, Self::Black] + } + + #[inline] + pub(crate) fn home_rank(self) -> Rank { + match self { + Self::White => Rank::First, + Self::Black => Rank::Eighth, + } + } + + #[inline] + pub(crate) fn promotion_rank(self) -> Rank { + match self { + Self::White => Rank::Eighth, + Self::Black => Rank::First, + } + } + + #[inline] + pub(crate) fn forward(self) -> Direction { + match self { + Self::White => Direction::North, + Self::Black => Direction::South, + } + } +} + +impl std::ops::Not for Color { + type Output = Self; + #[inline] + fn not(self) -> Self::Output { + match self { + Self::Black => Self::White, + Self::White => Self::Black, + } + } +} + +/// A column of the board. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum File { + A, + B, + C, + D, + E, + F, + G, + H, +} + +container!(File, ByFile, 8); + +impl File { + #[inline] + pub fn all() -> [Self; 8] { + [ + Self::A, + Self::B, + Self::C, + Self::D, + Self::E, + Self::F, + Self::G, + Self::H, + ] + } + + #[inline] + pub fn to_char(self) -> char { + self.to_ascii() as char + } + + #[inline] + pub fn from_char(c: char) -> Option { + Self::from_ascii(c as u8) + } + + #[inline] + pub(crate) fn to_ascii(self) -> u8 { + self as u8 + b'a' + } + + #[inline] + pub(crate) fn from_ascii(c: u8) -> Option { + (c <= b'h') + .then(|| c.checked_sub(b'a').map(|i| unsafe { Self::transmute(i) })) + .flatten() + } + + #[inline] + pub(crate) fn bitboard(self) -> Bitboard { + Bitboard(0x0101010101010101 << (self as u8)) + } + + #[inline] + pub(crate) unsafe fn transmute(value: u8) -> Self { + debug_assert!(value < 8); + std::mem::transmute(value) + } +} + +impl std::fmt::Display for File { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_char()) + } +} + +/// A row of the board. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum Rank { + First, + Second, + Third, + Fourth, + Fifth, + Sixth, + Seventh, + Eighth, +} + +container!(Rank, ByRank, 8); + +impl Rank { + #[inline] + pub fn all() -> [Self; 8] { + [ + Self::First, + Self::Second, + Self::Third, + Self::Fourth, + Self::Fifth, + Self::Sixth, + Self::Seventh, + Self::Eighth, + ] + } + + #[inline] + pub fn to_char(self) -> char { + self.to_ascii() as char + } + + #[inline] + pub fn from_char(c: char) -> Option { + Self::from_ascii(c as u8) + } + + #[inline] + pub fn mirror(self) -> Self { + unsafe { Self::transmute(7_u8.unchecked_sub(self as u8)) } + } + + #[inline] + pub(crate) fn to_ascii(self) -> u8 { + self as u8 + b'1' + } + + #[inline] + pub(crate) fn from_ascii(c: u8) -> Option { + (c <= b'8') + .then(|| c.checked_sub(b'1').map(|i| unsafe { Self::transmute(i) })) + .flatten() + } + + #[inline] + pub(crate) fn bitboard(self) -> Bitboard { + Bitboard(0xFF << ((self as u64) << 3)) + } + + #[inline] + pub(crate) unsafe fn transmute(value: u8) -> Self { + debug_assert!(value < 8); + std::mem::transmute(value) + } +} + +impl std::fmt::Display for Rank { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_char()) + } +} + +/// A square of the board. +#[rustfmt::skip] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum Square{ + A1, B1, C1, D1, E1, F1, G1, H1, + A2, B2, C2, D2, E2, F2, G2, H2, + A3, B3, C3, D3, E3, F3, G3, H3, + A4, B4, C4, D4, E4, F4, G4, H4, + A5, B5, C5, D5, E5, F5, G5, H5, + A6, B6, C6, D6, E6, F6, G6, H6, + A7, B7, C7, D7, E7, F7, G7, H7, + A8, B8, C8, D8, E8, F8, G8, H8, +} + +container!(Square, BySquare, 64); + +impl Square { + #[inline] + #[rustfmt::skip] + pub fn all() -> [Self; 64] { + [ + Self::A1, Self::B1, Self::C1, Self::D1, Self::E1, Self::F1, Self::G1, Self::H1, + Self::A2, Self::B2, Self::C2, Self::D2, Self::E2, Self::F2, Self::G2, Self::H2, + Self::A3, Self::B3, Self::C3, Self::D3, Self::E3, Self::F3, Self::G3, Self::H3, + Self::A4, Self::B4, Self::C4, Self::D4, Self::E4, Self::F4, Self::G4, Self::H4, + Self::A5, Self::B5, Self::C5, Self::D5, Self::E5, Self::F5, Self::G5, Self::H5, + Self::A6, Self::B6, Self::C6, Self::D6, Self::E6, Self::F6, Self::G6, Self::H6, + Self::A7, Self::B7, Self::C7, Self::D7, Self::E7, Self::F7, Self::G7, Self::H7, + Self::A8, Self::B8, Self::C8, Self::D8, Self::E8, Self::F8, Self::G8, Self::H8, + ] + } + + #[inline] + pub fn new(file: File, rank: Rank) -> Self { + unsafe { Self::transmute(((rank as u8) << 3) + file as u8) } + } + + #[inline] + pub fn file(self) -> File { + unsafe { File::transmute((self as u8) & 7) } + } + + #[inline] + pub fn rank(self) -> Rank { + unsafe { Rank::transmute((self as u8) >> 3) } + } + + #[inline] + pub fn mirror(self) -> Self { + Self::new(self.file(), self.rank().mirror()) + } + + #[inline] + pub(crate) fn bitboard(self) -> Bitboard { + Bitboard(1 << self as u8) + } + + #[inline] + #[rustfmt::skip] + pub(crate) fn to_str(self) -> &'static str { + match self { + Self::A1 => "a1", Self::B1 => "b1", Self::C1 => "c1", Self::D1 => "d1", Self::E1 => "e1", Self::F1 => "f1", Self::G1 => "g1", Self::H1 => "h1", + Self::A2 => "a2", Self::B2 => "b2", Self::C2 => "c2", Self::D2 => "d2", Self::E2 => "e2", Self::F2 => "f2", Self::G2 => "g2", Self::H2 => "h2", + Self::A3 => "a3", Self::B3 => "b3", Self::C3 => "c3", Self::D3 => "d3", Self::E3 => "e3", Self::F3 => "f3", Self::G3 => "g3", Self::H3 => "h3", + Self::A4 => "a4", Self::B4 => "b4", Self::C4 => "c4", Self::D4 => "d4", Self::E4 => "e4", Self::F4 => "f4", Self::G4 => "g4", Self::H4 => "h4", + Self::A5 => "a5", Self::B5 => "b5", Self::C5 => "c5", Self::D5 => "d5", Self::E5 => "e5", Self::F5 => "f5", Self::G5 => "g5", Self::H5 => "h5", + Self::A6 => "a6", Self::B6 => "b6", Self::C6 => "c6", Self::D6 => "d6", Self::E6 => "e6", Self::F6 => "f6", Self::G6 => "g6", Self::H6 => "h6", + Self::A7 => "a7", Self::B7 => "b7", Self::C7 => "c7", Self::D7 => "d7", Self::E7 => "e7", Self::F7 => "f7", Self::G7 => "g7", Self::H7 => "h7", + Self::A8 => "a8", Self::B8 => "b8", Self::C8 => "c8", Self::D8 => "d8", Self::E8 => "e8", Self::F8 => "f8", Self::G8 => "g8", Self::H8 => "h8", + } + } + + #[inline] + pub(crate) fn from_str(s: &str) -> Option { + match s.as_bytes() { + [f, r] => Self::from_ascii(&[*f, *r]), + _ => None, + } + } + + #[inline] + pub(crate) fn from_ascii(s: &[u8; 2]) -> Option { + let [f, r] = *s; + Some(Self::new(File::from_ascii(f)?, Rank::from_ascii(r)?)) + } + + #[inline] + pub(crate) fn trans(self, direction: Direction) -> Option { + self.check_trans(direction).then(|| unsafe { + // SAFETY: condition is checked before doing the translation + self.trans_unchecked(direction) + }) + } + + /// SAFETY: the translation must not move the square outside the board + #[inline] + pub(crate) unsafe fn trans_unchecked(self, direction: Direction) -> Self { + debug_assert!(self.check_trans(direction)); + let i = self as u8; + unsafe { + Self::transmute(match direction { + Direction::East => i.unchecked_add(1), + Direction::NorthEast => i.unchecked_add(9), + Direction::North => i.unchecked_add(8), + Direction::NorthWest => i.unchecked_add(7), + Direction::SouthEast => i.unchecked_sub(7), + Direction::South => i.unchecked_sub(8), + Direction::SouthWest => i.unchecked_sub(9), + Direction::West => i.unchecked_sub(1), + }) + } + } + + /// Returns `false` if the translation would move the square outside the board + #[inline] + fn check_trans(self, direction: Direction) -> bool { + match direction { + Direction::East => self.file() < File::H, + Direction::NorthEast => self.file() < File::H && self.rank() < Rank::Eighth, + Direction::North => self.rank() < Rank::Eighth, + Direction::NorthWest => self.file() > File::A && self.rank() < Rank::Eighth, + Direction::SouthEast => self.file() < File::H && self.rank() > Rank::First, + Direction::South => self.rank() > Rank::First, + Direction::SouthWest => self.file() > File::A && self.rank() > Rank::First, + Direction::West => self.file() > File::A, + } + } + + #[inline] + pub(crate) unsafe fn transmute(value: u8) -> Self { + debug_assert!(value < 64); + std::mem::transmute(value) + } +} + +impl std::fmt::Display for Square { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(self.to_str()) + } +} + +/// An error while parsing a [`Square`]. +#[derive(Debug)] +pub struct ParseSquareError; +impl std::fmt::Display for ParseSquareError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("invalid square syntax") + } +} +impl std::error::Error for ParseSquareError {} + +impl std::str::FromStr for Square { + type Err = ParseSquareError; + #[inline] + fn from_str(s: &str) -> Result { + Self::from_str(s).ok_or(ParseSquareError) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +#[rustfmt::skip] +pub(crate) enum OptionSquare { + _A1, _B1, _C1, _D1, _E1, _F1, _G1, _H1, + _A2, _B2, _C2, _D2, _E2, _F2, _G2, _H2, + _A3, _B3, _C3, _D3, _E3, _F3, _G3, _H3, + _A4, _B4, _C4, _D4, _E4, _F4, _G4, _H4, + _A5, _B5, _C5, _D5, _E5, _F5, _G5, _H5, + _A6, _B6, _C6, _D6, _E6, _F6, _G6, _H6, + _A7, _B7, _C7, _D7, _E7, _F7, _G7, _H7, + _A8, _B8, _C8, _D8, _E8, _F8, _G8, _H8, + None, +} + +impl OptionSquare { + #[inline] + pub(crate) fn new(square: Option) -> OptionSquare { + match square { + Some(square) => Self::from_square(square), + None => Self::None, + } + } + + #[inline] + pub(crate) fn try_into_square(self) -> Option { + unsafe { + match self { + Self::None => None, + _ => Some(Square::transmute(self as u8)), + } + } + } + + #[inline] + pub(crate) fn from_square(square: Square) -> Self { + unsafe { std::mem::transmute(square) } + } +} + +/// A type of piece. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum Role { + Pawn = 1, + Knight, + Bishop, + Rook, + Queen, + King, +} + +impl Role { + #[inline] + pub fn all() -> [Self; 6] { + [ + Self::Pawn, + Self::Knight, + Self::Bishop, + Self::Rook, + Self::Queen, + Self::King, + ] + } + + #[inline] + pub(crate) fn to_char_uppercase(self) -> char { + match self { + Self::Pawn => 'P', + Self::Knight => 'N', + Self::Bishop => 'B', + Self::Rook => 'R', + Self::Queen => 'Q', + Self::King => 'K', + } + } + + #[inline] + pub(crate) fn to_char_lowercase(self) -> char { + match self { + Self::Pawn => 'p', + Self::Knight => 'n', + Self::Bishop => 'b', + Self::Rook => 'r', + Self::Queen => 'q', + Self::King => 'k', + } + } + + #[inline] + pub(crate) fn from_ascii(x: u8) -> Option { + Some(match x { + b'p' | b'P' => Self::Pawn, + b'n' | b'N' => Self::Knight, + b'b' | b'B' => Self::Bishop, + b'r' | b'R' => Self::Rook, + b'q' | b'Q' => Self::Queen, + b'k' | b'K' => Self::King, + _ => return None, + }) + } + + #[inline] + pub(crate) unsafe fn transmute(i: u8) -> Self { + debug_assert!(i > 0 && i <= 6, "got {i}"); + unsafe { std::mem::transmute(i) } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct ByRole(pub(crate) [T; 6]); +#[allow(unused)] +impl ByRole { + #[inline] + pub fn new(f: F) -> Self + where + F: FnMut(Role) -> T, + { + Self(Role::all().map(f)) + } + + #[inline] + pub fn get(&self, kind: Role) -> &T { + unsafe { self.0.get_unchecked((kind as usize).unchecked_sub(1)) } + } + + #[inline] + pub fn get_mut(&mut self, kind: Role) -> &mut T { + unsafe { self.0.get_unchecked_mut((kind as usize).unchecked_sub(1)) } + } +} + +impl ByRole +where + T: Copy, +{ + #[inline] + pub(crate) fn pawn(&self) -> T { + *self.get(Role::Pawn) + } + #[inline] + pub(crate) fn knight(&self) -> T { + *self.get(Role::Knight) + } + #[inline] + pub(crate) fn bishop(&self) -> T { + *self.get(Role::Bishop) + } + #[inline] + pub(crate) fn rook(&self) -> T { + *self.get(Role::Rook) + } + #[inline] + pub(crate) fn queen(&self) -> T { + *self.get(Role::Queen) + } + #[inline] + pub(crate) fn king(&self) -> T { + *self.get(Role::King) + } +} + +/// A chess piece (i.e. its [`Role`] and [`Color`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Piece { + pub role: Role, + pub color: Color, +} + +#[derive(Clone, Copy)] +#[repr(u8)] +pub(crate) enum Direction { + East, + NorthEast, + North, + NorthWest, + SouthEast, + South, + SouthWest, + West, +} + +container!(Direction, ByDirection, 8); + +impl Direction { + #[inline] + pub fn all() -> [Self; 8] { + [ + Self::East, + Self::NorthEast, + Self::North, + Self::NorthWest, + Self::SouthEast, + Self::South, + Self::SouthWest, + Self::West, + ] + } + + #[inline] + unsafe fn transmute(value: u8) -> Self { + debug_assert!(value < 8); + std::mem::transmute(value) + } +} + +impl std::ops::Not for Direction { + type Output = Self; + #[inline] + fn not(self) -> Self::Output { + unsafe { Self::transmute(self as u8 ^ 0b111) } + } +} + +/// A side of the board. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum CastlingSide { + /// King's side + Short, + /// Queen's side + Long, +} + +container!(CastlingSide, ByCastlingSide, 2); + +impl CastlingSide { + #[inline] + pub fn all() -> [Self; 2] { + [Self::Short, Self::Long] + } + + #[inline] + pub(crate) fn rook_origin_file(self) -> File { + match self { + Self::Short => File::H, + Self::Long => File::A, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct CastlingRights(u8); + +impl CastlingRights { + #[inline] + pub(crate) fn new() -> Self { + Self(0) + } + + #[inline] + pub(crate) const fn full() -> Self { + Self(15) + } + + #[inline] + pub(crate) fn get(&self, color: Color, side: CastlingSide) -> bool { + (self.0 & Self::mask(color, side)) != 0 + } + + #[inline] + pub(crate) fn set(&mut self, color: Color, side: CastlingSide) { + self.0 |= Self::mask(color, side); + } + + #[inline] + pub(crate) fn unset(&mut self, color: Color, side: CastlingSide) { + self.0 &= !Self::mask(color, side); + } + + #[inline] + pub(crate) fn mirror(&self) -> Self { + Self(((self.0 & 3) << 2) | (self.0 >> 2)) + } + + #[inline] + const fn mask(color: Color, side: CastlingSide) -> u8 { + match (color, side) { + (Color::White, CastlingSide::Short) => 1, + (Color::White, CastlingSide::Long) => 2, + (Color::Black, CastlingSide::Short) => 4, + (Color::Black, CastlingSide::Long) => 8, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6c36c79 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,90 @@ +//! # eschac - a library for computing chess moves +//! +//! eschac implements fast legal move generation and a copy-make interface that enforces at compile +//! time that no illegal move is played, with no runtime checks and no potential panics. +//! +//! ## Overview +//! +//! The most important type in eschac is [`Position`](position::Position), it represents a chess +//! position from which legal moves are generated. [`Position::new`](position::Position::new) +//! returns the starting position of a chess game, and arbitrary positions can be built using the +//! [`Setup`](setup::Setup) type, but they must be validated and converted to a +//! [`Position`](position::Position) to generate moves as eschac does not handle certain illegal -- +//! as in unreachable in a normal game -- positions (see +//! [`IllegalPositionReason`](setup::IllegalPositionReason) to know more). Legal moves are then +//! generated using the [`Position::legal_moves`](position::Position::legal_moves) method or +//! obtained from chess notation like [`UciMove`](uci::UciMove) or [`San`](san::San). Moves are +//! represented with the [`Move<'l>`](position::Move) type, which holds a reference to the origin +//! position (hence the lifetime), this ensures the move is played on the correct position. +//! Finally, moves are played using the [`Move::make`](position::Move) method which returns a new +//! [`Position`](position::Position), and on it goes. +//! +//! ## Example +//! +//! ``` +//! # (|| -> Result<(), Box> { +//! +//! use eschac::prelude::*; +//! +//! // read a position from a text record +//! let setup = "7k/4P1rp/5Q2/5p2/1Pp1bP2/8/r4K1P/6R1 w - -".parse::()?; +//! let position = setup.validate()?; +//! +//! // read a move in algebraic notation +//! let san = "Ke1".parse::()?; +//! let m = san.to_move(&position)?; +//! +//! // play the move (note the absence of error handling) +//! let position = m.make(); +//! +//! // generate all the legal moves on the new position +//! let moves = position.legal_moves(); +//! for m in moves { +//! // print the UCI notation of each move +//! println!("{}", m.to_uci()); +//! } +//! # Ok(()) }); +//! ``` +//! +//! ## Comparison with [shakmaty](https://crates.io/crates/shakmaty) +//! +//! shakmaty is another Rust library for chess processing. It is written by Niklas Fiekas, whose +//! work greatly inspired the development of eschac. For most purposes, shakmaty is probably a +//! better option, as eschac comes short of its miriad of features. +//! +//! Both libraries have the same core features: +//! - vocabulary to describe the chessboard (squares, pieces, etc.) +//! - parsing and editing positions +//! - parsing standard move notations +//! - fast legal move generation and play +//! +//! **eschac** distinguishes itself with: +//! - a focus on performance +//! - a more compact board representation +//! - its use of the borrow checker to guarantee only legal moves are played +//! +//! **shakmaty** will be more suitable for a lot of applications, with: +//! - vocabulary to describe and work with games, not just positions +//! - insufficient material detection +//! - PGN parsing +//! - Zobrist hashing +//! - Syzygy endgame tablebases +//! - chess960 and other variants +//! - etc. + +pub(crate) mod array_vec; +pub(crate) mod bitboard; +pub(crate) mod magics; +pub(crate) mod rays; + +pub mod board; +pub mod lookup; +pub mod position; +pub mod san; +pub mod setup; +pub mod uci; + +/// The eschac prelude. +pub mod prelude { + pub use crate::{position::Position, san::San, setup::Setup, uci::UciMove}; +} diff --git a/src/lookup.rs b/src/lookup.rs new file mode 100644 index 0000000..d1378a6 --- /dev/null +++ b/src/lookup.rs @@ -0,0 +1,202 @@ +//! Lookup tables initialisation. +//! +//! Move generation in eschac requires about 1MB of precomputed lookup tables. + +use crate::bitboard::*; +use crate::board::*; +use crate::magics::*; +use crate::rays::*; + +pub(crate) use init::InitialisedLookup; + +/// Forces the initialisation of the lookup tables. +/// +/// It is not necessary to call this function, as lookup tables are initialised lazily, but it can +/// be used to ensure that they are initialised before a given time. +pub fn init() { + InitialisedLookup::init(); +} + +pub(crate) struct Lookup { + rays: Rays, + lines: BySquare>, + segments: BySquare>, + pawn_attacks: ByColor>, + king_moves: BySquare, + knight_moves: BySquare, + pub(crate) magics: Magics, +} + +impl Lookup { + #[inline] + pub(crate) fn line(&self, a: Square, b: Square) -> Bitboard { + *self.lines.get(a).get(b) + } + + #[inline] + pub(crate) fn segment(&self, a: Square, b: Square) -> Bitboard { + *self.segments.get(a).get(b) + } + + #[inline] + pub(crate) fn ray(&self, square: Square, direction: Direction) -> Bitboard { + self.rays.ray(square, direction) + } + + #[inline] + pub(crate) fn king(&self, square: Square) -> Bitboard { + *self.king_moves.get(square) + } + + #[inline] + pub(crate) fn knight(&self, square: Square) -> Bitboard { + *self.knight_moves.get(square) + } + + #[inline] + pub(crate) fn pawn_attack(&self, color: Color, square: Square) -> Bitboard { + *self.pawn_attacks.get(color).get(square) + } + + #[inline] + pub(crate) fn bishop(&self, square: Square, blockers: Bitboard) -> Bitboard { + self.magics.bishop(square, blockers) + } + + #[inline] + pub(crate) fn rook(&self, square: Square, blockers: Bitboard) -> Bitboard { + self.magics.rook(square, blockers) + } + + /// `role != Pawn` + #[inline] + pub(crate) fn targets(&self, role: Role, from: Square, blockers: Bitboard) -> Bitboard { + match role { + Role::Pawn => unreachable!(), + Role::Knight => self.knight(from), + Role::Bishop => self.bishop(from, blockers), + Role::Rook => self.rook(from, blockers), + Role::Queen => self.bishop(from, blockers) | self.rook(from, blockers), + Role::King => self.king(from), + } + } + + pub(crate) fn compute() -> Self { + let rays = Rays::new(); + + let lines = BySquare::new(|a| { + BySquare::new(|b| { + for d in Direction::all() { + let r = rays.ray(a, d); + if r.contains(b) { + return r; + } + } + Bitboard::new() + }) + }); + + let segments = BySquare::new(|a| { + BySquare::new(|b| { + for d in Direction::all() { + let r = rays.ray(a, d); + if r.contains(b) { + return r & !rays.ray(b, d); + } + } + b.bitboard() + }) + }); + + let pawn_attacks = ByColor::new(|color| { + let direction = match color { + Color::White => Direction::North, + Color::Black => Direction::South, + }; + BySquare::new(|square| { + let mut res = Bitboard::new(); + if let Some(square) = square.trans(direction) { + square.trans(Direction::East).map(|s| res.insert(s)); + square.trans(Direction::West).map(|s| res.insert(s)); + } + res + }) + }); + + let king_moves = BySquare::new(|square| { + let mut res = Bitboard::new(); + for direction in Direction::all() { + if let Some(x) = square.trans(direction) { + res |= x.bitboard(); + } + } + res + }); + + let knight_moves = BySquare::new(|s| { + let mut res = Bitboard::new(); + if let Some(s) = s.trans(Direction::North) { + s.trans(Direction::NorthEast).map(|s| res.insert(s)); + s.trans(Direction::NorthWest).map(|s| res.insert(s)); + } + if let Some(s) = s.trans(Direction::West) { + s.trans(Direction::NorthWest).map(|s| res.insert(s)); + s.trans(Direction::SouthWest).map(|s| res.insert(s)); + } + if let Some(s) = s.trans(Direction::South) { + s.trans(Direction::SouthWest).map(|s| res.insert(s)); + s.trans(Direction::SouthEast).map(|s| res.insert(s)); + } + if let Some(s) = s.trans(Direction::East) { + s.trans(Direction::SouthEast).map(|s| res.insert(s)); + s.trans(Direction::NorthEast).map(|s| res.insert(s)); + } + res + }); + + let magics = Magics::compute(&rays); + + Self { + rays, + lines, + segments, + pawn_attacks, + king_moves, + knight_moves, + magics, + } + } +} + +mod init { + use std::{mem::MaybeUninit, sync::LazyLock}; + + use super::Lookup; + + static mut LOOKUP: MaybeUninit = MaybeUninit::uninit(); + + #[allow(static_mut_refs)] + static INIT: LazyLock<()> = LazyLock::new(|| unsafe { + LOOKUP.write(Lookup::compute()); + }); + + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub(crate) struct InitialisedLookup(()); + + impl InitialisedLookup { + #[inline] + pub(crate) fn init() -> Self { + LazyLock::force(&INIT); + Self(()) + } + } + + impl std::ops::Deref for InitialisedLookup { + type Target = Lookup; + #[allow(static_mut_refs)] + #[inline] + fn deref(&self) -> &Self::Target { + unsafe { LOOKUP.assume_init_ref() } + } + } +} diff --git a/src/magics.rs b/src/magics.rs new file mode 100644 index 0000000..d82e531 --- /dev/null +++ b/src/magics.rs @@ -0,0 +1,326 @@ +use crate::bitboard::*; +use crate::board::*; +use crate::rays::Rays; + +const BISHOP_SHR: u8 = 55; +const ROOK_SHR: u8 = 52; + +pub(crate) struct Magics { + bishop: BySquare, + rook: BySquare, + table: Box<[Bitboard]>, +} + +#[derive(Clone, Copy)] +struct Magic { + premask: Bitboard, + factor: u64, + offset: isize, +} + +impl Magics { + pub(crate) fn compute(rays: &Rays) -> Self { + let mut data = Vec::new(); + + let mut aux = + |shr, + factors: fn(Square) -> u64, + make_table: fn(&Rays, Square) -> (Bitboard, Vec<(Bitboard, Bitboard)>)| { + BySquare::new(|square| { + let (premask, table) = make_table(rays, square); + let factor = factors(square); + let offset = fill_table(&mut data, shr, factor, table); + Magic { + premask, + factor, + offset, + } + }) + }; + + let bishop = aux(BISHOP_SHR, bishop_factors, make_bishop_table); + let rook = aux(ROOK_SHR, rook_factors, make_rook_table); + + let mut table = Box::new_uninit_slice(data.len()); + for (i, entry) in data.into_iter().enumerate() { + table[i].write(entry); + } + + Self { + bishop, + rook, + table: unsafe { table.assume_init() }, + } + } + + #[inline] + pub(crate) fn bishop(&self, square: Square, blockers: Bitboard) -> Bitboard { + unsafe { self.get_unchecked(BISHOP_SHR, *self.bishop.get(square), blockers) } + } + + #[inline] + pub(crate) fn rook(&self, square: Square, blockers: Bitboard) -> Bitboard { + unsafe { self.get_unchecked(ROOK_SHR, *self.rook.get(square), blockers) } + } + + #[inline] + unsafe fn get_unchecked(&self, shr: u8, magic: Magic, blockers: Bitboard) -> Bitboard { + let Magic { + premask, + factor, + offset, + } = magic; + *self.table.get_unchecked( + ((hash(shr, factor, blockers | premask) as isize).unchecked_add(offset)) as usize, + ) + } +} + +fn fill_table( + data: &mut Vec, + shr: u8, + factor: u64, + table: Vec<(Bitboard, Bitboard)>, +) -> isize { + let offset = data.len() as isize + - table + .iter() + .map(|(x, _)| hash(shr, factor, *x) as isize) + .min() + .unwrap(); + for (x, y) in &table { + let i = (hash(shr, factor, *x) as isize + offset) as usize; + while data.len() <= i { + data.push(Bitboard::new()); + } + if data[i] != Bitboard::new() && data[i] != *y { + panic!(); + } + data[i] = *y; + } + offset +} + +fn make_bishop_table(rays: &Rays, square: Square) -> (Bitboard, Vec<(Bitboard, Bitboard)>) { + let mut premask = Bitboard::new(); + for direction in [ + Direction::NorthWest, + Direction::SouthWest, + Direction::SouthEast, + Direction::NorthEast, + ] { + premask |= rays.ray(square, direction); + } + premask &= !Rank::First.bitboard(); + premask &= !Rank::Eighth.bitboard(); + premask &= !File::A.bitboard(); + premask &= !File::H.bitboard(); + + let mut table = make_table(premask, |blockers| { + let mut res = Bitboard::new(); + for direction in [ + Direction::NorthWest, + Direction::SouthWest, + Direction::SouthEast, + Direction::NorthEast, + ] { + res |= rays.blocked(square, direction, blockers); + } + res + }); + + premask = !premask; + for (x, _) in &mut table { + *x |= premask; + } + + (premask, table) +} + +fn make_rook_table(rays: &Rays, square: Square) -> (Bitboard, Vec<(Bitboard, Bitboard)>) { + let mut premask = Bitboard::new(); + premask |= rays.ray(square, Direction::North) & !Rank::Eighth.bitboard(); + premask |= rays.ray(square, Direction::West) & !File::A.bitboard(); + premask |= rays.ray(square, Direction::South) & !Rank::First.bitboard(); + premask |= rays.ray(square, Direction::East) & !File::H.bitboard(); + + let mut table = make_table(premask, |blockers| { + let mut res = Bitboard::new(); + for direction in [ + Direction::North, + Direction::West, + Direction::South, + Direction::East, + ] { + res |= rays.blocked(square, direction, blockers); + } + res + }); + + premask = !premask; + for (x, _) in &mut table { + *x |= premask; + } + + (premask, table) +} + +fn make_table(premask: Bitboard, f: F) -> Vec<(Bitboard, T)> +where + F: Fn(Bitboard) -> T, +{ + let mut res = Vec::new(); + let mut subset: u64 = 0; + loop { + subset = subset.wrapping_sub(premask.0) & premask.0; + let x = Bitboard(subset); + let y = f(x); + res.push((x, y)); + if subset == 0 { + break; + } + } + res +} + +fn hash(shr: u8, factor: u64, x: Bitboard) -> usize { + (x.0.wrapping_mul(factor) >> shr) as usize +} + +fn bishop_factors(square: Square) -> u64 { + match square { + Square::A1 => 0x0000404040404040, + Square::B1 => 0x0040C100081000E8, + Square::C1 => 0x0000401020200000, + Square::D1 => 0x0040802004000000, + Square::E1 => 0x10403C0180000000, + Square::F1 => 0x0040210100800000, + Square::G1 => 0x0068104002008000, + Square::H1 => 0x0048082080040080, + Square::A2 => 0x0000004040404040, + Square::B2 => 0x0000002020202020, + Square::C2 => 0x00040080184001E4, + Square::D2 => 0x0040008020040000, + Square::E2 => 0x1040003C01800000, + Square::F2 => 0x0078002001008000, + Square::G2 => 0x0068001040020080, + Square::H2 => 0x0068000820010040, + Square::A3 => 0x0000400080808080, + Square::B3 => 0x0000200040404040, + Square::C3 => 0x0000400080808080, + Square::D3 => 0x0000200200801000, + Square::E3 => 0x0060200100080000, + Square::F3 => 0x0000100021C60021, + Square::G3 => 0x0000040010410040, + Square::H3 => 0x0000020008208020, + Square::A4 => 0x0000804000810100, + Square::B4 => 0x0000402000408080, + Square::C4 => 0x0000040800802080, + Square::D4 => 0x000020100C010020, + Square::E4 => 0x0000840000802000, + Square::F4 => 0x0001801800240010, + Square::G4 => 0x0000080800104100, + Square::H4 => 0x0000040400082080, + Square::A5 => 0x0000010278010040, + Square::B5 => 0x000000813C004040, + Square::C5 => 0x000001027A010040, + Square::D5 => 0x0000018180280200, + Square::E5 => 0x0000204018003080, + Square::F5 => 0x0000202040008040, + Square::G5 => 0x0000101010002080, + Square::H5 => 0x0000080808001040, + Square::A6 => 0x0000004100F90080, + Square::B6 => 0x0000002080BC0040, + Square::C6 => 0x0000004103440080, + Square::D6 => 0x0000000080FD0080, + Square::E6 => 0x0000020040100100, + Square::F6 => 0x0000404040400080, + Square::G6 => 0x000000206027D010, + Square::H6 => 0x00000008400DE806, + Square::A7 => 0x0000002101007200, + Square::B7 => 0x0000001041003900, + Square::C7 => 0x080000000F8080A0, + Square::D7 => 0x0000000008003FC0, + Square::E7 => 0x0000000100202000, + Square::F7 => 0x0000004040802000, + Square::G7 => 0x00000060401043D0, + Square::H7 => 0x00000020200413F0, + Square::A8 => 0x1400000F00410088, + Square::B8 => 0x0000000010410039, + Square::C8 => 0x000080000800807E, + Square::D8 => 0x000C69003008003F, + Square::E8 => 0x0000000001002020, + Square::F8 => 0x0000000040408020, + Square::G8 => 0x001980004010801F, + Square::H8 => 0x0000404040404040, + } +} + +fn rook_factors(square: Square) -> u64 { + match square { + Square::A1 => 0x002000A28110000C, + Square::B1 => 0x0018000C01060001, + Square::C1 => 0x0040080010004004, + Square::D1 => 0x0028004084200028, + Square::E1 => 0x0030018000900300, + Square::F1 => 0x0020008020010202, + Square::G1 => 0x001800410080001F, + Square::H1 => 0x0068006801040004, + Square::A2 => 0x000028010114000A, + Square::B2 => 0x00000C0083000600, + Square::C2 => 0x0000080401020008, + Square::D2 => 0x0000200200040020, + Square::E2 => 0x0000200100020020, + Square::F2 => 0x00001800C0006018, + Square::G2 => 0x0000180070400018, + Square::H2 => 0x0000180030640018, + Square::A3 => 0x00300018010C0004, + Square::B3 => 0x0004001000080010, + Square::C3 => 0x0001000804020008, + Square::D3 => 0x0002002004002002, + Square::E3 => 0x0001002002002001, + Square::F3 => 0x0001001000801040, + Square::G3 => 0x0000004040008001, + Square::H3 => 0x0000802000200040, + Square::A4 => 0x0040200010080008, + Square::B4 => 0x0000080010040010, + Square::C4 => 0x0001020008040008, + Square::D4 => 0x0000020020040020, + Square::E4 => 0x0000010020020020, + Square::F4 => 0x0000008020010020, + Square::G4 => 0x0000008020200040, + Square::H4 => 0x0000200020004081, + Square::A5 => 0x0000081C00380020, + Square::B5 => 0x0000080400100010, + Square::C5 => 0x0000400880410010, + Square::D5 => 0x0000200200200400, + Square::E5 => 0x0000200100200200, + Square::F5 => 0x0000200080200100, + Square::G5 => 0x0000008000404001, + Square::H5 => 0x0000802000200040, + Square::A6 => 0x0000010B14002800, + Square::B6 => 0x0000030086000C00, + Square::C6 => 0x0000084040804200, + Square::D6 => 0x0000020004002020, + Square::E6 => 0x0000009001803003, + Square::F6 => 0x0000004001004002, + Square::G6 => 0x000000100800A804, + Square::H6 => 0x000000082800D002, + Square::A7 => 0x00000109040200A8, + Square::B7 => 0x000000808A050014, + Square::C7 => 0x0000004048038018, + Square::D7 => 0x0000020020040020, + Square::E7 => 0x0000002030018030, + Square::F7 => 0x0000001800E08018, + Square::G7 => 0x0000000810580050, + Square::H7 => 0x0000000C04600050, + Square::A8 => 0x0000001020891046, + Square::B8 => 0x00000080090015C1, + Square::C8 => 0x0000004005489101, + Square::D8 => 0x0000040810204002, + Square::E8 => 0x000C040810002022, + Square::F8 => 0x0008000404883002, + Square::G8 => 0x0008000400548802, + Square::H8 => 0x0000000224104486, + } +} diff --git a/src/position.rs b/src/position.rs new file mode 100644 index 0000000..8e227fb --- /dev/null +++ b/src/position.rs @@ -0,0 +1,1488 @@ +//! **Move generation.** + +use crate::array_vec::*; +use crate::bitboard::*; +use crate::board::*; +use crate::lookup::*; +use crate::san::*; +use crate::setup::*; +use crate::uci::*; + +use std::iter::ExactSizeIterator; +use std::iter::FusedIterator; + +/// **A chess position.** +/// +/// ## Game's state +/// +/// This type records the following information: +/// - the position of pieces on the board +/// - the color to play +/// - the available castling rights +/// - the optional en passant target square (even if taking is not possible) +/// +/// ## Validity & Legality +/// +/// This type can only represent "valid" chess positions. Valid positions include but are not +/// limited to legal chess positions (i.e. positions that can be reached from a sequence of legal +/// moves starting on the initial position). It is neither computably feasible nor desirable to +/// reject all illegal positions. eschac will only reject illegal positions when chess rules can't +/// be applied unambiguously or when doing so enables some code optimisation. See +/// [`IllegalPositionReason`] for details about rejected positions. +/// +/// ## Move generation & play +/// +/// The [`legal_moves`](Position::legal_moves) method generates the list of all legal moves on the +/// position. [`UciMove::to_move`] and [`San::to_move`] can also be used to convert chess notation +/// to playable moves. +/// +/// Playing a move is done through a copy-make interface. [`legal_moves`](Position::legal_moves) +/// returns a sequence of [`Move`] objects. Moves hold a reference to the position from where they +/// were computed. They can be played without further checks and without potential panics using +/// [`Move::make`]. +/// +/// ## En passant & Equality +/// +/// The en passant target square is set when the [`Position`] is obtained after playing a double +/// pawn advance, even when there is no pawn to take or when taking is not legal. In that case, +/// [`remove_en_passant_target_square`](Position::remove_en_passant_target_square) can be used to +/// remove the target square from the record. As a consequence, [`Eq`] is not defined in accordance +/// with the FIDE laws of chess. +/// +/// ## Ordering +// +/// The order on [`Position`] is defined only for use in data structures. Hence its only +/// requirement is to be efficient while respecting the [`Ord`] trait protocol. It should not be +/// considered stable. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Position { + setup: Setup, + lookup: InitialisedLookup, +} + +const MAX_LEGAL_MOVES: usize = 218; + +impl std::fmt::Debug for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_tuple("Position") + .field(&self.as_setup().to_string()) + .finish() + } +} + +impl Position { + /// Returns the initial position of a chess game. + /// + /// i.e. `rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -` + #[inline] + pub fn new() -> Self { + Self { + setup: Setup { + w: Bitboard(0x000000000000FFFF), + p_b_q: Bitboard(0x2CFF00000000FF2C), + n_b_k: Bitboard(0x7600000000000076), + r_q_k: Bitboard(0x9900000000000099), + turn: Color::White, + castling_rights: CastlingRights::full(), + en_passant: OptionSquare::None, + }, + lookup: InitialisedLookup::init(), + } + } + + /// Tries to read a valid position from a text record. + /// + /// This is a shortcut for parsing and validating a [`Setup`]: + /// ``` + /// # use eschac::setup::Setup; + /// # |s: &str| -> Option { + /// s.parse::().ok().and_then(|pos| pos.validate().ok()) + /// # }; + /// ``` + #[inline] + pub fn from_text_record(s: &str) -> Option { + s.parse::().ok().and_then(|pos| pos.validate().ok()) + } + + /// Returns all the legal moves on the position. + #[inline] + pub fn legal_moves<'l>(&'l self) -> Moves<'l> { + fn aux(position: &Position, visitor: &mut Moves) { + position.generate_moves(visitor); + } + let mut visitor = Moves { + position: self, + is_check: false, + en_passant_is_legal: false, + array: ArrayVec::new(), + }; + aux(self, &mut visitor); + visitor + } + + /// Counts the legal moves on the position. + /// + /// This is equivalent but faster than: + /// ``` + /// # use eschac::position::Position; + /// # |position: &Position| -> usize { + /// position.legal_moves().len() + /// # }; + /// ``` + #[inline] + pub fn count_legal_moves(&self) -> usize { + struct VisitorImpl { + len: usize, + } + impl VisitorImpl { + fn new() -> Self { + Self { len: 0 } + } + } + impl Visitor for VisitorImpl { + #[inline] + fn is_check(&mut self) {} + #[inline] + fn en_passant_is_legal(&mut self) {} + #[inline] + fn moves(&mut self, iter: I) + where + I: Iterator + ExactSizeIterator, + { + self.len += iter.len(); + } + } + fn aux(position: &Position, visitor: &mut VisitorImpl) { + position.generate_moves(visitor); + } + let mut visitor = VisitorImpl::new(); + aux(self, &mut visitor); + visitor.len + } + + /// Discards the optional en passant target square. + /// + /// This function is useful to check for position equality, notably when implementing FIDE's + /// draw by repetition rules. Note that this function will remove the en passant target square + /// even if taking en passant is legal. If this is not desirable, it is the caller's + /// responsibility to rule out the legality of en passant before calling this function. + #[inline] + pub fn remove_en_passant_target_square(&mut self) { + self.setup.en_passant = OptionSquare::None; + } + + /// Returns the occupancy of a square. + #[inline] + pub fn get(&self, square: Square) -> Option { + self.setup.get(square) + } + + /// Returns the color whose turn it is to play. + #[inline] + pub fn turn(&self) -> Color { + self.setup.turn() + } + + /// Returns `true` if castling is available for the given color and side. + #[inline] + pub fn castling_rights(&self, color: Color, side: CastlingSide) -> bool { + self.setup.castling_rights(color, side) + } + + /// Returns the en passant target square if it exists. + /// + /// Note that if an en passant target square exists, it does not mean that taking en passant is + /// legal or even pseudo-legal. + #[inline] + pub fn en_passant_target_square(&self) -> Option { + self.setup.en_passant_target_square() + } + + /// Discards the castling rights for the given color and side. + #[inline] + pub fn remove_castling_rights(&mut self, color: Color, side: CastlingSide) { + self.setup.set_castling_rights(color, side, false); + } + + /// Borrows the position as a [`Setup`]. + #[inline] + pub fn as_setup(&self) -> &Setup { + &self.setup + } + + /// Converts a position to a [`Setup`], allowing to edit the position without enforcing its + /// legality. + #[inline] + pub fn into_setup(self) -> Setup { + self.setup + } + + /// Tries to pass the turn to the other color, failing if it would leave the king in check. + /// + /// When possible, this inverts the color to play and removes the en passant square if it + /// exists. + pub fn pass(&self) -> Option { + let d = self.lookup; + let setup = &self.setup; + let blockers = setup.p_b_q | setup.n_b_k | setup.r_q_k; + let k = setup.n_b_k & setup.r_q_k; + let q = setup.p_b_q & setup.r_q_k; + let b = setup.p_b_q & setup.n_b_k; + let n = setup.n_b_k ^ b ^ k; + let r = setup.r_q_k ^ q ^ k; + let p = setup.p_b_q ^ b ^ q; + let (us, them) = match setup.turn { + Color::White => (setup.w, blockers ^ setup.w), + Color::Black => (blockers ^ setup.w, setup.w), + }; + let king_square = (us & k).next().unwrap(); + let checkers = them + & (d.pawn_attack(setup.turn, king_square) & p + | d.knight(king_square) & n + | d.bishop(king_square, blockers) & (q | b) + | d.rook(king_square, blockers) & (q | r)); + checkers.is_empty().then(|| Self { + setup: Setup { + turn: !setup.turn, + en_passant: OptionSquare::None, + ..setup.clone() + }, + lookup: d, + }) + } + + /// Returns the mirror image of the position (see [`Setup::mirror`]). + #[inline] + pub fn mirror(&self) -> Self { + Self { + setup: self.setup.mirror(), + lookup: self.lookup, + } + } + + /// Returns the number of possible chess games for a given number of moves. + /// + /// This function is intended for benchmarking and is written as a simple recursion without any + /// caching. + pub fn perft(&self, depth: usize) -> u128 { + fn aux(position: &Position, depth: usize) -> u128 { + match depth.checked_sub(1) { + None => position.count_legal_moves() as u128, + Some(depth) => position + .legal_moves() + .into_iter() + .map(|m| aux(&m.make(), depth)) + .sum(), + } + } + match depth.checked_sub(1) { + None => 1, + Some(depth) => aux(self, depth), + } + } + + pub(crate) fn move_from_uci<'l>(&'l self, uci: UciMove) -> Result, InvalidUciMove> { + struct VisitorImpl { + role: Role, + from: Bitboard, + to: Bitboard, + found: Option, + } + impl VisitorImpl { + #[inline] + fn new(role: Role, from: Bitboard, to: Bitboard) -> Self { + Self { + role, + from, + to, + found: None, + } + } + } + impl Visitor for VisitorImpl { + #[inline] + fn roles(&self, role: Role) -> bool { + role as u8 == ROLE + } + #[inline] + fn from(&self) -> Bitboard { + self.from + } + #[inline] + fn to(&self) -> Bitboard { + self.to + } + #[inline] + fn is_check(&mut self) {} + #[inline] + fn en_passant_is_legal(&mut self) {} + #[inline] + fn moves(&mut self, iter: I) + where + I: Iterator + ExactSizeIterator, + { + iter.for_each(|raw| { + debug_assert!(raw.role() as u8 == ROLE); + debug_assert!(self.from.contains(raw.from())); + debug_assert!(self.to.contains(raw.to())); + if raw.role == self.role { + debug_assert!(self.found.is_none()); + self.found = Some(raw); + } + }); + } + } + let UciMove { + from, + to, + promotion, + } = uci; + let role = self.setup.get_role(from).ok_or(InvalidUciMove::Illegal)?; + #[inline] + fn aux<'l, const ROLE: u8>( + position: &'l Position, + role: Role, + from: Square, + to: Square, + ) -> Result, InvalidUciMove> { + let mut visitor = VisitorImpl::::new(role, from.bitboard(), to.bitboard()); + position.generate_moves(&mut visitor); + let raw = visitor.found.ok_or(InvalidUciMove::Illegal)?; + Ok(Move { position, raw }) + } + let promotion = if role == Role::Pawn { + promotion.unwrap_or(Role::Pawn) + } else if promotion.is_some() { + return Err(InvalidUciMove::Illegal); + } else { + role + }; + match role { + Role::Pawn => aux::<1>(self, promotion, from, to), + Role::Knight => aux::<2>(self, promotion, from, to), + Role::Bishop => aux::<3>(self, promotion, from, to), + Role::Rook => aux::<4>(self, promotion, from, to), + Role::Queen => aux::<5>(self, promotion, from, to), + Role::King => aux::<6>(self, promotion, from, to), + } + } + + pub(crate) fn move_from_san<'l>(&'l self, san: &San) -> Result, InvalidSan> { + struct VisitorImpl { + role: Role, + from: Bitboard, + to: Bitboard, + found: Option, + found_other: bool, + } + impl VisitorImpl { + #[inline] + fn new(role: Role, from: Bitboard, to: Bitboard) -> Self { + Self { + role, + from, + to, + found: None, + found_other: false, + } + } + } + impl Visitor for VisitorImpl { + #[inline] + fn roles(&self, role: Role) -> bool { + role as u8 == ROLE + } + #[inline] + fn from(&self) -> Bitboard { + self.from + } + #[inline] + fn to(&self) -> Bitboard { + self.to + } + #[inline] + fn is_check(&mut self) {} + #[inline] + fn en_passant_is_legal(&mut self) {} + #[inline] + fn moves(&mut self, iter: I) + where + I: Iterator + ExactSizeIterator, + { + iter.for_each(|raw| { + debug_assert!(raw.role() as u8 == ROLE); + debug_assert!(self.from.contains(raw.from())); + debug_assert!(self.to.contains(raw.to())); + if raw.role == self.role { + match self.found { + Some(_) => self.found_other = true, + None => self.found = Some(raw), + } + } + }); + } + } + let (role, promotion, from, to) = match san.inner { + SanInner::Castle(CastlingSide::Short) => ( + Role::King, + Role::King, + Square::new(File::E, self.turn().home_rank()).bitboard(), + Square::new(File::G, self.turn().home_rank()).bitboard(), + ), + SanInner::Castle(CastlingSide::Long) => ( + Role::King, + Role::King, + Square::new(File::E, self.turn().home_rank()).bitboard(), + Square::new(File::C, self.turn().home_rank()).bitboard(), + ), + SanInner::Normal { + role, + target, + file, + rank, + promotion, + .. + } => ( + role, + if role == Role::Pawn { + promotion.unwrap_or(Role::Pawn) + } else if promotion.is_some() { + return Err(InvalidSan::Illegal); + } else { + role + }, + file.map_or(!Bitboard::new(), |file| file.bitboard()) + & rank.map_or(!Bitboard::new(), |rank| rank.bitboard()), + target.bitboard(), + ), + }; + #[inline] + fn aux<'l, const ROLE: u8>( + position: &'l Position, + role: Role, + from: Bitboard, + to: Bitboard, + ) -> Result, InvalidSan> { + let mut visitor = VisitorImpl::::new(role, from, to); + position.generate_moves(&mut visitor); + match visitor.found { + None => Err(InvalidSan::Illegal), + Some(raw) => match visitor.found_other { + true => Err(InvalidSan::Ambiguous), + false => Ok(Move { position, raw }), + }, + } + } + match role { + Role::Pawn => aux::<1>(self, promotion, from, to), + Role::Knight => aux::<2>(self, promotion, from, to), + Role::Bishop => aux::<3>(self, promotion, from, to), + Role::Rook => aux::<4>(self, promotion, from, to), + Role::Queen => aux::<5>(self, promotion, from, to), + Role::King => aux::<6>(self, promotion, from, to), + } + } +} + +/// A legal move. +#[derive(Clone, Copy)] +pub struct Move<'l> { + position: &'l Position, + raw: RawMove, +} + +impl<'l> Move<'l> { + /// Returns the position after playing the move. + /// + /// This sets the en passant square after a double pawn advance, even when there is no pawn to + /// capture or when capturing is not legal. + pub fn make(self) -> Position { + let mut position = self.position.clone(); + unsafe { position.play_unchecked(self.raw) }; + position + } + + /// Returns the position the move is played on. + #[inline] + pub fn position(self) -> &'l Position { + self.position + } + + /// Returns the type of piece that moves. + #[inline] + pub fn role(self) -> Role { + self.raw.role() + } + + /// Returns the origin square of the move. + #[inline] + pub fn from(self) -> Square { + self.raw.from() + } + + /// Returns the target square of the move. + #[inline] + pub fn to(self) -> Square { + self.raw.to() + } + + /// Returns the type of piece that the pawn is promoted to, if the move is a promotion. + #[inline] + pub fn promotion(self) -> Option { + self.raw.promotion() + } + + /// Returns `true` if the move is a capture. + #[inline] + pub fn is_capture(self) -> bool { + self.raw.kind == MoveType::EnPassant + || !((self.position.setup.p_b_q + | self.position.setup.n_b_k + | self.position.setup.r_q_k) + & self.to().bitboard()) + .is_empty() + } + + /// Returns the type of piece that is captured, if the move is a capture. + #[inline] + pub fn captured(self) -> Option { + match self.raw.kind { + MoveType::EnPassant => Some(Role::Pawn), + _ => self.position.setup.get_role(self.raw.to), + } + } + + /// Returns the UCI notation of the move. + #[inline] + pub fn to_uci(self) -> UciMove { + self.raw.uci() + } + + /// Returns the standard algebraic notation of the move. + pub fn to_san(self) -> San { + struct VisitorImpl { + to: Bitboard, + candidates: Bitboard, + } + impl VisitorImpl { + #[inline] + fn new(to: Square) -> Self { + Self { + to: to.bitboard(), + candidates: Bitboard::new(), + } + } + } + impl Visitor for VisitorImpl { + #[inline] + fn roles(&self, role: Role) -> bool { + role as u8 == ROLE + } + #[inline] + fn to(&self) -> Bitboard { + self.to + } + #[inline] + fn is_check(&mut self) {} + #[inline] + fn en_passant_is_legal(&mut self) {} + #[inline] + fn moves(&mut self, iter: I) + where + I: Iterator + ExactSizeIterator, + { + iter.for_each(|raw| { + debug_assert!(raw.role() as u8 == ROLE); + debug_assert!(self.to.contains(raw.to())); + self.candidates.insert(raw.from); + }); + } + } + San { + inner: match self.raw.kind { + MoveType::CastleShort => SanInner::Castle(CastlingSide::Short), + MoveType::CastleLong => SanInner::Castle(CastlingSide::Long), + MoveType::PawnAdvance + | MoveType::PawnAdvancePromotion + | MoveType::PawnDoubleAdvance => SanInner::Normal { + role: Role::Pawn, + file: None, + rank: None, + capture: false, + target: self.to(), + promotion: self.promotion(), + }, + MoveType::PawnAttack | MoveType::PawnAttackPromotion | MoveType::EnPassant => { + SanInner::Normal { + role: Role::Pawn, + file: Some(self.from().file()), + rank: None, + capture: true, + target: self.to(), + promotion: self.promotion(), + } + } + _ => { + fn aux(m: &Move) -> SanInner { + let mut visitor = VisitorImpl::::new(m.to()); + m.position().generate_moves(&mut visitor); + let candidates = visitor.candidates; + let (file, rank) = if candidates == m.from().bitboard() { + (None, None) + } else if candidates & m.from().file().bitboard() == m.from().bitboard() { + (Some(m.from().file()), None) + } else if candidates & m.from().rank().bitboard() == m.from().bitboard() { + (None, Some(m.from().rank())) + } else { + (Some(m.from().file()), Some(m.from().rank())) + }; + SanInner::Normal { + role: m.role(), + file, + rank, + capture: m.is_capture(), + target: m.to(), + promotion: None, + } + } + match self.role() { + Role::Pawn => aux::<1>(&self), + Role::Knight => aux::<2>(&self), + Role::Bishop => aux::<3>(&self), + Role::Rook => aux::<4>(&self), + Role::Queen => aux::<5>(&self), + Role::King => aux::<6>(&self), + } + } + }, + suffix: { + let pos = self.make(); + let mut visitor = MateCollector::new(); + pos.generate_moves(&mut visitor); + visitor.is_check.then(|| match visitor.is_mate { + true => SanSuffix::Checkmate, + false => SanSuffix::Check, + }) + }, + } + } +} + +/// A list of legal moves on the same position. +/// +/// It can be obtained using the [`Position::legal_moves`] method. This type is an iterator over +/// [`Move`] objects. +pub struct Moves<'l> { + position: &'l Position, + is_check: bool, + en_passant_is_legal: bool, + array: ArrayVec, +} + +impl<'l> Moves<'l> { + /// Returns the position on which the moves are played. + #[inline] + pub fn position(&self) -> &Position { + self.position + } + + /// Iterates over the moves. + #[inline] + pub fn iter(&'l self) -> MovesIter<'l> { + MovesIter { + position: self.position, + iter: (&self.array).into_iter(), + } + } + + /// Returns the number of moves in the list. + #[inline] + pub fn len(&self) -> usize { + self.array.len() + } + + /// Returns `true` if en passant is legal. + #[inline] + pub fn en_passant_is_legal(&self) -> bool { + self.en_passant_is_legal + } + + /// Returns `true` if the king is in check. + #[inline] + pub fn is_check(&self) -> bool { + self.is_check + } + + /// Returns the move at the given index, if it exists. + #[inline] + pub fn get(&self, index: usize) -> Option> { + self.array.get(index).copied().map(|raw| Move { + position: self.position, + raw, + }) + } + + /// Sorts the moves in the list. + /// + /// See [`slice::sort_unstable_by`] for potential panics. + #[inline] + pub fn sort_by(&mut self, mut compare: F) + where + F: FnMut(Move, Move) -> std::cmp::Ordering, + { + self.array.as_slice_mut().sort_unstable_by(|a, b| { + compare( + Move { + position: self.position, + raw: *a, + }, + Move { + position: self.position, + raw: *b, + }, + ) + }); + } +} + +/// An iterator over legal moves. +pub struct MovesIter<'l> { + position: &'l Position, + iter: ArrayVecIter<'l, RawMove, MAX_LEGAL_MOVES>, +} +impl<'l> Iterator for MovesIter<'l> { + type Item = Move<'l>; + #[inline] + fn next(&mut self) -> Option { + self.iter.next().map(|raw| Move { + position: self.position, + raw, + }) + } +} +impl<'l> FusedIterator for MovesIter<'l> {} +impl<'l> ExactSizeIterator for MovesIter<'l> { + #[inline] + fn len(&self) -> usize { + self.iter.len() + } +} +impl<'l> IntoIterator for &'l Moves<'l> { + type Item = Move<'l>; + type IntoIter = MovesIter<'l>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// An iterator over legal moves. +pub struct MovesIntoIter<'l> { + position: &'l Position, + iter: ArrayVecIntoIter, +} +impl<'l> Iterator for MovesIntoIter<'l> { + type Item = Move<'l>; + #[inline] + fn next(&mut self) -> Option { + self.iter.next().map(|raw| Move { + position: self.position, + raw, + }) + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} +impl<'l> FusedIterator for MovesIntoIter<'l> {} +impl<'l> ExactSizeIterator for MovesIntoIter<'l> { + #[inline] + fn len(&self) -> usize { + self.iter.len() + } +} +impl<'l> IntoIterator for Moves<'l> { + type Item = Move<'l>; + type IntoIter = MovesIntoIter<'l>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + MovesIntoIter { + position: self.position, + iter: self.array.into_iter(), + } + } +} + +#[derive(Clone, Copy)] +pub(crate) struct RawMove { + kind: MoveType, + role: Role, + from: Square, + to: Square, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +enum MoveType { + CastleShort, + CastleLong, + KingMove, + PieceMove, + PawnAdvance, + PawnAttack, + PawnAdvancePromotion, + PawnAttackPromotion, + PawnDoubleAdvance, + EnPassant, +} + +impl RawMove { + #[inline] + fn from(&self) -> Square { + self.from + } + #[inline] + fn to(&self) -> Square { + self.to + } + #[inline] + fn role(&self) -> Role { + match self.kind { + MoveType::CastleShort | MoveType::CastleLong | MoveType::KingMove => Role::King, + MoveType::PieceMove => self.role, + MoveType::PawnAdvance + | MoveType::PawnAttack + | MoveType::PawnAdvancePromotion + | MoveType::PawnAttackPromotion + | MoveType::PawnDoubleAdvance + | MoveType::EnPassant => Role::Pawn, + } + } + #[inline] + fn promotion(&self) -> Option { + match self.kind { + MoveType::PawnAdvancePromotion | MoveType::PawnAttackPromotion => Some(self.role), + _ => None, + } + } + #[inline] + fn uci(&self) -> UciMove { + UciMove { + from: self.from(), + to: self.to(), + promotion: self.promotion(), + } + } +} + +trait Visitor { + #[inline] + fn roles(&self, _role: Role) -> bool { + true + } + #[inline] + fn from(&self) -> Bitboard { + !Bitboard::new() + } + #[inline] + fn to(&self) -> Bitboard { + !Bitboard::new() + } + + fn is_check(&mut self); + fn en_passant_is_legal(&mut self); + fn moves(&mut self, iter: I) + where + I: Iterator + ExactSizeIterator; +} + +impl<'l> Visitor for Moves<'l> { + #[inline] + fn is_check(&mut self) { + self.is_check = true; + } + #[inline] + fn en_passant_is_legal(&mut self) { + self.en_passant_is_legal = true; + } + #[inline] + fn moves(&mut self, iter: I) + where + I: Iterator + ExactSizeIterator, + { + iter.for_each(|raw| unsafe { self.array.push_unchecked(raw) }); + } +} + +impl Position { + /// SAFETY: The position must be valid. + pub(crate) unsafe fn from_setup(setup: Setup) -> Self { + Self { + setup, + lookup: InitialisedLookup::init(), + } + } + + fn generate_moves(&self, visitor: &mut T) + where + T: Visitor, + { + let global_mask_from = visitor.from(); + let global_mask_to = visitor.to(); + + let Setup { + w, + p_b_q, + n_b_k, + r_q_k, + turn, + en_passant, + castling_rights, + } = self.setup; + let d = self.lookup; + + let blockers = p_b_q | n_b_k | r_q_k; + let (us, them) = match turn { + Color::White => (w, blockers ^ w), + Color::Black => (blockers ^ w, w), + }; + + let k = n_b_k & r_q_k; + let q = p_b_q & r_q_k; + let b = p_b_q & n_b_k; + let n = n_b_k ^ b ^ k; + let r = r_q_k ^ q ^ k; + let p = p_b_q ^ b ^ q; + let ours = ByRole([p & us, n & us, b & us, r & us, q & us, k & us]); + let theirs = ByRole([ + p & them, + n & them, + (q | b) & them, + (q | r) & them, + q & them, + k & them, + ]); + let king_square = unsafe { + // SAFETY: the position is legal + ours.king().first().unwrap_unchecked() + }; + + let forward = turn.forward(); + + let x = d.bishop(king_square, blockers); + let y = d.rook(king_square, blockers); + let checkers = d.pawn_attack(turn, king_square) & theirs.pawn() + | d.knight(king_square) & theirs.knight() + | x & theirs.bishop() + | y & theirs.rook(); + let blockers_x_ray = blockers & !(x | y); + let pinned = (d.bishop(king_square, blockers_x_ray) & theirs.bishop() + | d.rook(king_square, blockers_x_ray) & theirs.rook()) + .map(|sq| d.segment(king_square, sq)) + .reduce_or(); + + if visitor.roles(Role::King) && global_mask_from.contains(king_square) { + let attacked = { + let blockers = blockers ^ ours.king(); + theirs + .king() + .map(|sq| d.king(sq)) + .chain(theirs.bishop().map(|sq| d.bishop(sq, blockers))) + .chain(theirs.rook().map(|sq| d.rook(sq, blockers))) + .chain(theirs.knight().map(|sq| d.knight(sq))) + .chain(std::iter::once( + theirs.pawn().trans(!forward).trans(Direction::East), + )) + .chain(std::iter::once( + theirs.pawn().trans(!forward).trans(Direction::West), + )) + .reduce_or() + }; + // king moves + visitor.moves( + (global_mask_to & d.king(king_square) & !us & !attacked).map(|to| RawMove { + kind: MoveType::KingMove, + from: king_square, + to, + role: Role::King, + }), + ); + // castling + if castling_rights.get(turn, CastlingSide::Short) { + let (x, y) = match turn { + Color::White => (Bitboard(0x0000000000000070), Bitboard(0x0000000000000060)), + Color::Black => (Bitboard(0x7000000000000000), Bitboard(0x6000000000000000)), + }; + if (attacked & x | blockers & y).is_empty() { + let from = king_square; + let to = unsafe { + from.trans_unchecked(Direction::East) + .trans_unchecked(Direction::East) + }; + if global_mask_to.contains(to) { + visitor.moves(std::iter::once(RawMove { + kind: MoveType::CastleShort, + from, + to, + role: Role::King, + })) + } + } + } + if castling_rights.get(turn, CastlingSide::Long) { + let (x, y) = match turn { + Color::White => (Bitboard(0x000000000000001C), Bitboard(0x000000000000000E)), + Color::Black => (Bitboard(0x1C00000000000000), Bitboard(0x0E00000000000000)), + }; + if (attacked & x | blockers & y).is_empty() { + let from = king_square; + let to = unsafe { + from.trans_unchecked(Direction::West) + .trans_unchecked(Direction::West) + }; + if global_mask_to.contains(to) { + visitor.moves(std::iter::once(RawMove { + kind: MoveType::CastleLong, + from, + to, + role: Role::King, + })) + } + } + } + } + + if checkers.len() > 1 { + visitor.is_check(); + return; + } + + let checker = checkers.first(); + let block_check = checker + .map(|checker| d.segment(king_square, checker)) + .unwrap_or(Bitboard(!0)); + let target_mask = global_mask_to & block_check; + + // pawns + if visitor.roles(Role::Pawn) { + let kside = match turn { + Color::White => Direction::NorthEast, + Color::Black => Direction::SouthEast, + }; + let qside = match turn { + Color::White => Direction::NorthWest, + Color::Black => Direction::SouthWest, + }; + let third_rank = match turn { + Color::White => Rank::Third, + Color::Black => Rank::Sixth, + }; + let adv = (global_mask_from & ours.pawn() & (!pinned | king_square.file().bitboard())) + .trans(forward) + & !blockers; + let promotion = turn.promotion_rank().bitboard(); + // pawn advances + { + let targets = adv & target_mask; + visitor.moves((targets & !promotion).map(|to| RawMove { + kind: MoveType::PawnAdvance, + from: unsafe { to.trans_unchecked(!forward) }, + to, + role: Role::Pawn, + })); + visitor.moves(WithPromotion::new((targets & promotion).map(|to| { + RawMove { + kind: MoveType::PawnAdvancePromotion, + from: unsafe { to.trans_unchecked(!forward) }, + to, + role: Role::Pawn, + } + }))); + } + // pawn attacks kingside + { + let targets = + (global_mask_from & ours.pawn() & (!pinned | d.ray(king_square, kside))) + .trans(kside) + & them + & target_mask; + visitor.moves((targets & !promotion).map(|to| RawMove { + kind: MoveType::PawnAttack, + from: unsafe { to.trans_unchecked(!kside) }, + to, + role: Role::Pawn, + })); + visitor.moves(WithPromotion::new((targets & promotion).map(|to| { + RawMove { + kind: MoveType::PawnAttackPromotion, + from: unsafe { to.trans_unchecked(!kside) }, + to, + role: Role::Pawn, + } + }))); + } + // pawn attacks queenside + { + let targets = + (global_mask_from & ours.pawn() & (!pinned | d.ray(king_square, qside))) + .trans(qside) + & them + & target_mask; + visitor.moves((targets & !promotion).map(|to| RawMove { + kind: MoveType::PawnAttack, + from: unsafe { to.trans_unchecked(!qside) }, + to, + role: Role::Pawn, + })); + visitor.moves(WithPromotion::new((targets & promotion).map(|to| { + RawMove { + kind: MoveType::PawnAttackPromotion, + from: unsafe { to.trans_unchecked(!qside) }, + to, + role: Role::Pawn, + } + }))); + } + // pawn double advances + visitor.moves( + ((adv & third_rank.bitboard()).trans(forward) & !blockers & target_mask).map( + |to| RawMove { + kind: MoveType::PawnDoubleAdvance, + from: unsafe { to.trans_unchecked(!forward).trans_unchecked(!forward) }, + to, + role: Role::Pawn, + }, + ), + ); + // en passant + if let Some(to) = en_passant.try_into_square() { + if global_mask_to.contains(to) { + let capture_square = unsafe { + // SAFETY: the position is legal + to.trans_unchecked(!forward) + }; + if block_check.contains(to) + || checker.is_none_or(|checker| checker == capture_square) + { + let candidates = d.pawn_attack(!turn, to) & ours.pawn(); + let blockers = blockers ^ capture_square.bitboard(); + let pinned = pinned + | (d.rook(king_square, blockers & !(d.rook(king_square, blockers))) + & theirs.rook()) + .map(|sq| d.segment(king_square, sq)) + .reduce_or(); + (global_mask_from & candidates & (!pinned | d.segment(king_square, to))) + .for_each(|from| { + visitor.en_passant_is_legal(); + visitor.moves(std::iter::once(RawMove { + kind: MoveType::EnPassant, + from, + to, + role: Role::Pawn, + })) + }) + } + } + } + } + + // pieces not pinned + { + let aux = |visitor: &mut T, role| { + for from in global_mask_from & *ours.get(role) & !pinned { + visitor.moves( + (d.targets(role, from, blockers) & !us & target_mask).map(|to| RawMove { + kind: MoveType::PieceMove, + from, + to, + role, + }), + ) + } + }; + if visitor.roles(Role::Knight) { + aux(visitor, Role::Knight) + } + if visitor.roles(Role::Bishop) { + aux(visitor, Role::Bishop) + } + if visitor.roles(Role::Rook) { + aux(visitor, Role::Rook) + } + if visitor.roles(Role::Queen) { + aux(visitor, Role::Queen) + } + } + + if checker.is_some() { + visitor.is_check(); + return; + } + + // pinned pieces + { + let aux = |visitor: &mut T, role| { + for from in global_mask_from & *ours.get(role) & pinned { + visitor.moves( + (global_mask_to + & d.targets(role, from, blockers) + & !us + & d.line(king_square, from)) + .map(|to| RawMove { + kind: MoveType::PieceMove, + from, + to, + role, + }), + ) + } + }; + if visitor.roles(Role::Bishop) { + aux(visitor, Role::Bishop) + } + if visitor.roles(Role::Rook) { + aux(visitor, Role::Rook) + } + if visitor.roles(Role::Queen) { + aux(visitor, Role::Queen) + } + } + } + + #[inline] + unsafe fn play_unchecked(&mut self, m: RawMove) { + let Self { setup, .. } = self; + + setup.en_passant = OptionSquare::None; + + let RawMove { + kind, + from, + to, + role, + } = m; + + match kind { + MoveType::CastleShort => aux_play_castle(setup, CastlingSide::Short), + MoveType::CastleLong => aux_play_castle(setup, CastlingSide::Long), + MoveType::KingMove => aux_play_normal(setup, Role::King, from, to), + MoveType::PieceMove => aux_play_normal(setup, role, from, to), + MoveType::PawnAdvance => aux_play_pawn_advance(setup, Role::Pawn, from, to), + MoveType::PawnAttack => aux_play_normal(setup, Role::Pawn, from, to), + MoveType::PawnAdvancePromotion => aux_play_pawn_advance(setup, role, from, to), + MoveType::PawnAttackPromotion => aux_play_normal(setup, role, from, to), + MoveType::PawnDoubleAdvance => { + aux_play_pawn_advance(setup, Role::Pawn, from, to); + setup.en_passant = OptionSquare::new(Some(Square::new( + from.file(), + match setup.turn { + Color::White => Rank::Third, + Color::Black => Rank::Sixth, + }, + ))); + } + MoveType::EnPassant => { + let direction = !setup.turn.forward(); + let x = (unsafe { to.trans_unchecked(direction) }).bitboard(); + setup.p_b_q ^= x; + setup.w &= !x; + aux_play_pawn_advance(setup, Role::Pawn, from, to); + } + } + + setup.turn = !setup.turn; + } +} + +struct WithPromotion + ExactSizeIterator + FusedIterator> { + inner: I, + cur: std::mem::MaybeUninit, + role: Role, +} +impl WithPromotion +where + I: Iterator + ExactSizeIterator + FusedIterator, +{ + #[inline] + fn new(inner: I) -> Self { + Self { + inner, + cur: std::mem::MaybeUninit::uninit(), + role: Role::King, + } + } +} +impl Iterator for WithPromotion +where + I: Iterator + ExactSizeIterator + FusedIterator, +{ + type Item = RawMove; + #[inline] + fn next(&mut self) -> Option { + if self.role == Role::King { + self.cur.write(self.inner.next()?); + self.role = Role::Knight; + } + let raw = unsafe { self.cur.assume_init() }; + let res = RawMove { + role: self.role, + ..raw + }; + self.role = unsafe { Role::transmute((self.role as u8).unchecked_add(1)) }; + Some(res) + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} +impl FusedIterator for WithPromotion where + I: Iterator + ExactSizeIterator + FusedIterator +{ +} +impl ExactSizeIterator for WithPromotion +where + I: Iterator + ExactSizeIterator + FusedIterator, +{ + #[inline] + fn len(&self) -> usize { + unsafe { self.inner.len().unchecked_mul(4) } + } +} + +#[inline] +fn aux_play_normal(setup: &mut Setup, role: Role, from: Square, target: Square) { + let from = from.bitboard(); + let to = target.bitboard(); + let mask = !(from | to); + setup.w &= mask; + setup.p_b_q &= mask; + setup.n_b_k &= mask; + setup.r_q_k &= mask; + if target == Square::new(File::H, setup.turn.promotion_rank()) { + setup + .castling_rights + .unset(!setup.turn, CastlingSide::Short); + } + if target == Square::new(File::A, setup.turn.promotion_rank()) { + setup.castling_rights.unset(!setup.turn, CastlingSide::Long); + } + match role { + Role::King => { + setup.n_b_k |= to; + setup.r_q_k |= to; + setup.castling_rights.unset(setup.turn, CastlingSide::Short); + setup.castling_rights.unset(setup.turn, CastlingSide::Long); + } + Role::Queen => { + setup.p_b_q |= to; + setup.r_q_k |= to; + } + Role::Bishop => { + setup.p_b_q |= to; + setup.n_b_k |= to; + } + Role::Knight => { + setup.n_b_k |= to; + } + Role::Rook => { + setup.r_q_k |= to; + if from == Square::new(File::H, setup.turn.home_rank()).bitboard() { + setup.castling_rights.unset(setup.turn, CastlingSide::Short); + } + if from == Square::new(File::A, setup.turn.home_rank()).bitboard() { + setup.castling_rights.unset(setup.turn, CastlingSide::Long); + } + } + Role::Pawn => { + setup.p_b_q |= to; + } + } + if setup.turn == Color::White { + setup.w |= to; + } +} + +#[inline] +fn aux_play_pawn_advance(setup: &mut Setup, role: Role, from: Square, to: Square) { + let from = from.bitboard(); + let to = to.bitboard(); + match role { + Role::King => unreachable!(), + Role::Queen => { + setup.p_b_q ^= from | to; + setup.r_q_k |= to; + } + Role::Bishop => { + setup.p_b_q ^= from | to; + setup.n_b_k |= to; + } + Role::Knight => { + setup.p_b_q ^= from; + setup.n_b_k |= to; + } + Role::Rook => { + setup.p_b_q ^= from; + setup.r_q_k |= to; + } + Role::Pawn => setup.p_b_q ^= from | to, + } + if setup.turn == Color::White { + setup.w ^= from | to; + } +} + +#[inline] +fn aux_play_castle(setup: &mut Setup, side: CastlingSide) { + let rank = setup.turn.home_rank(); + let (king_flip, rook_flip) = match side { + CastlingSide::Short => ( + Square::new(File::E, rank).bitboard() | Square::new(File::G, rank).bitboard(), + Square::new(File::H, rank).bitboard() | Square::new(File::F, rank).bitboard(), + ), + CastlingSide::Long => ( + Square::new(File::E, rank).bitboard() | Square::new(File::C, rank).bitboard(), + Square::new(File::A, rank).bitboard() | Square::new(File::D, rank).bitboard(), + ), + }; + + if setup.turn == Color::White { + setup.w ^= king_flip | rook_flip; + } + setup.n_b_k ^= king_flip; + setup.r_q_k ^= king_flip | rook_flip; + + setup.castling_rights.unset(setup.turn, CastlingSide::Short); + setup.castling_rights.unset(setup.turn, CastlingSide::Long); +} + +struct MateCollector { + is_check: bool, + is_mate: bool, +} +impl MateCollector { + #[inline] + fn new() -> Self { + Self { + is_check: false, + is_mate: true, + } + } +} +impl Visitor for MateCollector { + #[inline] + fn is_check(&mut self) { + self.is_check = true; + } + #[inline] + fn en_passant_is_legal(&mut self) {} + #[inline] + fn moves(&mut self, iter: I) + where + I: Iterator + ExactSizeIterator, + { + self.is_mate &= iter.len() == 0; + } +} diff --git a/src/rays.rs b/src/rays.rs new file mode 100644 index 0000000..d758ffc --- /dev/null +++ b/src/rays.rs @@ -0,0 +1,44 @@ +use crate::bitboard::*; +use crate::board::*; + +pub(crate) struct Rays(BySquare>); + +impl Rays { + pub(crate) fn new() -> Self { + Self(BySquare::new(|square| { + ByDirection::new(|direction| { + let mut square = square; + let mut res = Bitboard::new(); + while let Some(x) = square.trans(direction) { + square = x; + res |= square.bitboard(); + } + res + }) + })) + } + + #[inline] + pub(crate) fn ray(&self, square: Square, direction: Direction) -> Bitboard { + *self.0.get(square).get(direction) + } + + pub(crate) fn blocked( + &self, + square: Square, + direction: Direction, + blockers: Bitboard, + ) -> Bitboard { + let blockers = blockers & *self.0.get(square).get(direction); + let square2 = if (direction as u8) < 4 { + blockers.first() + } else { + blockers.last() + }; + *self.0.get(square).get(direction) + & !match square2 { + Some(square2) => *self.0.get(square2).get(direction), + None => Bitboard::new(), + } + } +} diff --git a/src/san.rs b/src/san.rs new file mode 100644 index 0000000..2cf6491 --- /dev/null +++ b/src/san.rs @@ -0,0 +1,219 @@ +//! Standard algebraic notation. +//! +//! SAN notation is the FIDE standard for writing moves. +//! +//! In its simplest form, it consists of the type of the moving piece followed by the target +//! square. This may be ambiguous, in which cases the origin file and/or rank is also specified. +//! The notation is shortened for pawns, and extra information may be added, to specify a capture +//! or a check. +//! +//! Examples: *`e4`*, *`Qxd8#`*, *`O-O`*, *`h7h8=Q`* + +use crate::board::*; +use crate::position::*; + +/// **The standard algebraic notation of a move.** +/// +/// +/// When converting [`San`] notation to a playable [`Move`], the optional capture flag (*x*) and +/// suffix (*+* or *#*) are ignored (as they are redundant). Thus, conversion will not fail if they +/// are incorrectly set. Similarly, conversion will not fail when the move is unnecessarily +/// disambiguated. For example, *Ke1xd1* and *Kd1* will always be equivalent, even if there is no +/// piece on *d1*. +/// +/// SAN notation can be obtained from a legal move using the [`Move::to_san`] method. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct San { + pub(crate) inner: SanInner, + pub(crate) suffix: Option, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum SanInner { + Castle(CastlingSide), + Normal { + role: Role, + file: Option, + rank: Option, + capture: bool, + target: Square, + promotion: Option, + }, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum SanSuffix { + Check, + Checkmate, +} + +impl std::fmt::Display for San { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self.inner { + SanInner::Castle(CastlingSide::Short) => write!(f, "O-O")?, + SanInner::Castle(CastlingSide::Long) => write!(f, "O-O-O")?, + SanInner::Normal { + role, + file, + rank, + capture, + target, + promotion, + } => { + if role != Role::Pawn { + write!(f, "{}", role.to_char_uppercase())?; + } + if let Some(file) = file { + write!(f, "{}", file.to_char())?; + } + if let Some(rank) = rank { + write!(f, "{}", rank.to_char())?; + } + if capture { + write!(f, "x")?; + } + write!(f, "{}", target)?; + if let Some(promotion) = promotion { + write!(f, "={}", promotion.to_char_uppercase())?; + } + } + } + match self.suffix { + Some(SanSuffix::Check) => write!(f, "+")?, + Some(SanSuffix::Checkmate) => write!(f, "#")?, + None => (), + } + Ok(()) + } +} + +impl std::fmt::Debug for San { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_tuple("San").field(&self.to_string()).finish() + } +} + +impl std::str::FromStr for San { + type Err = ParseSanError; + #[inline] + fn from_str(s: &str) -> Result { + San::from_ascii(s.as_bytes()).ok_or(ParseSanError) + } +} + +/// A syntax error when parsing [`San`] notation. +#[derive(Debug)] +pub struct ParseSanError; +impl std::fmt::Display for ParseSanError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("invalid SAN syntax") + } +} +impl std::error::Error for ParseSanError {} + +/// An error while converting [`San`] notation to a playable [`Move`]. +#[derive(Debug, PartialEq, Eq)] +pub enum InvalidSan { + /// There is no move on the position that matches the SAN notation. + Illegal, + /// There is more than one move on the position that matches the SAN notation. + Ambiguous, +} +impl std::fmt::Display for InvalidSan { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let details = match self { + Self::Illegal => "illegal move", + Self::Ambiguous => "ambiguous move", + }; + write!(f, "invalid SAN ({details})") + } +} +impl std::error::Error for InvalidSan {} + +impl San { + /// Tries to convert SAN notation to a playable move. + /// + /// This function ignores the suffix and the capture flag. It also accepts unnecessarily + /// desambiguated moves. + #[inline] + pub fn to_move<'l>(&self, position: &'l Position) -> Result, InvalidSan> { + position.move_from_san(self) + } + + /// Tries to read SAN notation from ascii text. + pub fn from_ascii(s: &[u8]) -> Option { + let mut r = s.iter().copied().rev(); + let mut cur = r.next()?; + + let suffix = match cur { + b'+' => { + cur = r.next()?; + Some(SanSuffix::Check) + } + b'#' => { + cur = r.next()?; + Some(SanSuffix::Checkmate) + } + _ => None, + }; + + let inner = match cur { + b'O' => SanInner::Castle({ + let b'-' = r.next()? else { return None }; + let b'O' = r.next()? else { return None }; + match r.next() { + None => CastlingSide::Short, + Some(b'-') => { + let b'O' = r.next()? else { return None }; + r.next().is_none().then_some(())?; + CastlingSide::Long + } + Some(_) => return None, + } + }), + _ => { + let promotion = Role::from_ascii(cur); + if promotion.is_some() { + (r.next()? == b'=').then_some(())?; + cur = r.next()?; + } + let target_rank = Rank::from_ascii(cur)?; + let target_file = File::from_ascii(r.next()?)?; + let target = Square::new(target_file, target_rank); + let mut cur = r.next(); + let capture = cur == Some(b'x'); + if capture { + cur = r.next(); + } + let rank = cur.and_then(Rank::from_ascii); + if rank.is_some() { + cur = r.next(); + } + let file = cur.and_then(File::from_ascii); + if file.is_some() { + cur = r.next(); + } + let role = match cur { + Some(a) => { + cur = r.next(); + Role::from_ascii(a)? + } + None => Role::Pawn, + }; + cur.is_none().then_some(())?; + (role != Role::Pawn || file.is_some() || !capture).then_some(())?; + (role == Role::Pawn || promotion.is_none()).then_some(())?; + SanInner::Normal { + role, + file, + rank, + capture, + target, + promotion, + } + } + }; + + Some(Self { inner, suffix }) + } +} diff --git a/src/setup.rs b/src/setup.rs new file mode 100644 index 0000000..83fe4f1 --- /dev/null +++ b/src/setup.rs @@ -0,0 +1,659 @@ +//! Building chess positions. +//! +//! [`Setup`] is a builder for the [`Position`] type. + +use crate::bitboard::*; +use crate::board::*; +use crate::lookup::*; +use crate::position::*; + +/// **A builder type for chess positions.** +/// +/// This type is useful to edit a position without having to ensure it stays legal at every step. +/// It must be validated and converted to a [`Position`] using the [`Setup::validate`] method +/// before generating moves. +/// +/// This type implements [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display) to parse +/// and print positions from text records. +/// +/// Forsyth-Edwards Notation (FEN) is typically used to describe chess positions as text. eschac +/// uses a slightly different notation, which simply removes the last two fields of the FEN string +/// (i.e. the halfmove clock and the fullmove number) as the [`Position`] type does not keep +/// track of those. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Setup { + pub(crate) w: Bitboard, + + pub(crate) p_b_q: Bitboard, + pub(crate) n_b_k: Bitboard, + pub(crate) r_q_k: Bitboard, + + pub(crate) turn: Color, + pub(crate) en_passant: OptionSquare, + pub(crate) castling_rights: CastlingRights, +} + +impl Setup { + /// Creates an empty board, i.e. `8/8/8/8/8/8/8/8 w - -`. + #[inline] + pub fn new() -> Self { + Self { + w: Bitboard(0), + p_b_q: Bitboard(0), + n_b_k: Bitboard(0), + r_q_k: Bitboard(0), + turn: Color::White, + en_passant: OptionSquare::None, + castling_rights: CastlingRights::new(), + } + } + + /// Reads a position from an ascii record. + pub fn from_ascii(s: &[u8]) -> Result { + let mut s = s.iter().copied().peekable(); + let mut setup = Setup::new(); + (|| { + let mut accept_empty_square = true; + let mut rank: u8 = 7; + let mut file: u8 = 0; + for c in s.by_ref() { + if c == b'/' { + (file == 8).then_some(())?; + rank = rank.checked_sub(1)?; + file = 0; + accept_empty_square = true; + } else if (b'1'..=b'8').contains(&c) && accept_empty_square { + file = file + c - b'0'; + (file <= 8).then_some(())?; + accept_empty_square = false; + } else if c == b' ' { + break; + } else { + let role = Role::from_ascii(c)?; + let color = match c.is_ascii_uppercase() { + true => Color::White, + false => Color::Black, + }; + (file < 8).then_some(())?; + setup.set( + unsafe { Square::new(File::transmute(file), Rank::transmute(rank)) }, + Some(Piece { role, color }), + ); + file += 1; + accept_empty_square = true; + } + } + (rank == 0).then_some(())?; + (file == 8).then_some(())?; + Some(()) + })() + .ok_or(ParseSetupError::InvalidBoard)?; + (|| { + match s.next()? { + b'w' => setup.set_turn(Color::White), + b'b' => setup.set_turn(Color::Black), + _ => return None, + } + (s.next()? == b' ').then_some(()) + })() + .ok_or(ParseSetupError::InvalidTurn)?; + (|| { + if s.next_if_eq(&b'-').is_none() { + if s.next_if_eq(&b'K').is_some() { + setup.set_castling_rights(Color::White, CastlingSide::Short, true); + } + if s.next_if_eq(&b'Q').is_some() { + setup.set_castling_rights(Color::White, CastlingSide::Long, true); + } + if s.next_if_eq(&b'k').is_some() { + setup.set_castling_rights(Color::Black, CastlingSide::Short, true); + } + if s.next_if_eq(&b'q').is_some() { + setup.set_castling_rights(Color::Black, CastlingSide::Long, true); + } + } + (s.next()? == b' ').then_some(()) + })() + .ok_or(ParseSetupError::InvalidCastlingRights)?; + (|| { + match s.next()? { + b'-' => (), + file => setup.set_en_passant_target_square(Some(Square::new( + File::from_ascii(file)?, + Rank::from_ascii(s.next()?)?, + ))), + } + s.next().is_none().then_some(()) + })() + .ok_or(ParseSetupError::InvalidEnPassantTargetSquare)?; + Ok(setup) + } + + /// Returns the occupancy of a square. + #[inline] + pub fn get(&self, square: Square) -> Option { + Some(Piece { + role: self.get_role(square)?, + color: match (self.w & square.bitboard()).is_empty() { + false => Color::White, + true => Color::Black, + }, + }) + } + + /// Returns the color to play. + #[inline] + pub fn turn(&self) -> Color { + self.turn + } + + /// Returns `true` if castling is available for the given color and side. + #[inline] + pub fn castling_rights(&self, color: Color, side: CastlingSide) -> bool { + self.castling_rights.get(color, side) + } + + /// Returns the optional en passant target square. + #[inline] + pub fn en_passant_target_square(&self) -> Option { + self.en_passant.try_into_square() + } + + /// Sets the occupancy of a square. + #[inline] + pub fn set(&mut self, square: Square, piece: Option) { + let mask = !square.bitboard(); + self.w &= mask; + self.p_b_q &= mask; + self.n_b_k &= mask; + self.r_q_k &= mask; + if let Some(piece) = piece { + let to = square.bitboard(); + match piece.color { + Color::White => self.w |= to, + Color::Black => (), + } + match piece.role { + Role::Pawn => { + self.p_b_q |= to; + } + Role::Knight => { + self.n_b_k |= to; + } + Role::Bishop => { + self.p_b_q |= to; + self.n_b_k |= to; + } + Role::Rook => { + self.r_q_k |= to; + } + Role::Queen => { + self.p_b_q |= to; + self.r_q_k |= to; + } + Role::King => { + self.n_b_k |= to; + self.r_q_k |= to; + } + } + } + } + + /// Sets the color to play. + #[inline] + pub fn set_turn(&mut self, color: Color) { + self.turn = color; + } + + /// Sets the castling rights for the given color and side. + #[inline] + pub fn set_castling_rights(&mut self, color: Color, side: CastlingSide, value: bool) { + match value { + true => self.castling_rights.set(color, side), + false => self.castling_rights.unset(color, side), + } + } + + /// Sets the en passant target square. + #[inline] + pub fn set_en_passant_target_square(&mut self, square: Option) { + self.en_passant = OptionSquare::new(square); + } + + /// Returns the mirror image of the position. + /// + /// The mirror of a position is the position obtained after reflecting the placement of pieces + /// horizontally, inverting the color of all the pieces, inverting the turn, and reflecting the + /// castling rights as well as the en passant target square. + /// + /// For example, the mirror image of `rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b Kq e3` + /// is `rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w Qk e6`. + #[inline] + pub fn mirror(&self) -> Self { + Self { + w: (self.w ^ (self.p_b_q | self.n_b_k | self.r_q_k)).mirror(), + p_b_q: self.p_b_q.mirror(), + n_b_k: self.n_b_k.mirror(), + r_q_k: self.r_q_k.mirror(), + turn: !self.turn, + en_passant: self + .en_passant + .try_into_square() + .map(|square| OptionSquare::from_square(square.mirror())) + .unwrap_or(OptionSquare::None), + castling_rights: self.castling_rights.mirror(), + } + } + + /// Tries to validate the position, i.e. converting it to a [`Position`]. + /// + /// See [`IllegalPositionReason`] for details. + pub fn validate(self) -> Result { + debug_assert!((self.w & !(self.p_b_q | self.n_b_k | self.r_q_k)).is_empty()); + debug_assert!((self.p_b_q & self.n_b_k & self.r_q_k).is_empty()); + + let mut reasons = IllegalPositionReasons::new(); + let d = InitialisedLookup::init(); + + let blockers = self.p_b_q | self.n_b_k | self.r_q_k; + let pieces = self.bitboards(); + + if Color::all() + .into_iter() + .any(|color| pieces.get(color).king().is_empty()) + { + reasons.add(IllegalPositionReason::MissingKing); + } + + if Color::all() + .into_iter() + .any(|color| pieces.get(color).get(Role::King).len() > 1) + { + reasons.add(IllegalPositionReason::TooManyKings); + } + + if pieces.get(!self.turn).king().any(|enemy_king| { + let pieces = pieces.get(self.turn); + !(d.king(enemy_king) & *pieces.get(Role::King) + | d.bishop(enemy_king, blockers) + & (*pieces.get(Role::Queen) | *pieces.get(Role::Bishop)) + | d.rook(enemy_king, blockers) + & (*pieces.get(Role::Queen) | *pieces.get(Role::Rook)) + | d.knight(enemy_king) & *pieces.get(Role::Knight) + | d.pawn_attack(!self.turn, enemy_king) & *pieces.get(Role::Pawn)) + .is_empty() + }) { + reasons.add(IllegalPositionReason::HangingKing); + } + + if Color::all().into_iter().any(|color| { + !(*pieces.get(color).get(Role::Pawn) + & (Rank::First.bitboard() | Rank::Eighth.bitboard())) + .is_empty() + }) { + reasons.add(IllegalPositionReason::PawnOnBackRank); + } + + if Color::all().into_iter().any(|color| { + let dark_squares = Bitboard(0xAA55AA55AA55AA55); + let light_squares = Bitboard(0x55AA55AA55AA55AA); + let pieces = pieces.get(color); + pieces.get(Role::Pawn).len() + + pieces.get(Role::Queen).len().saturating_sub(1) + + (*pieces.get(Role::Bishop) & dark_squares) + .len() + .saturating_sub(1) + + (*pieces.get(Role::Bishop) & light_squares) + .len() + .saturating_sub(1) + + pieces.get(Role::Knight).len().saturating_sub(2) + + pieces.get(Role::Rook).len().saturating_sub(2) + > 8 + }) { + reasons.add(IllegalPositionReason::TooMuchMaterial); + } + + if Color::all().into_iter().any(|color| { + CastlingSide::all().into_iter().any(|side| { + self.castling_rights.get(color, side) + && !(pieces + .get(color) + .get(Role::King) + .contains(Square::new(File::E, color.home_rank())) + && pieces + .get(color) + .get(Role::Rook) + .contains(Square::new(side.rook_origin_file(), color.home_rank()))) + }) + }) { + reasons.add(IllegalPositionReason::InvalidCastlingRights); + } + + if self.en_passant.try_into_square().is_some_and(|en_passant| { + let (target_rank, pawn_rank) = match self.turn { + Color::White => (Rank::Sixth, Rank::Fifth), + Color::Black => (Rank::Third, Rank::Fourth), + }; + let pawn_square = Square::new(en_passant.file(), pawn_rank); + en_passant.rank() != target_rank + || blockers.contains(en_passant) + || !pieces.get(!self.turn).get(Role::Pawn).contains(pawn_square) + }) { + reasons.add(IllegalPositionReason::InvalidEnPassant); + } + + if self.en_passant.try_into_square().is_some_and(|en_passant| { + let blockers = blockers + & !en_passant.bitboard().trans(match self.turn { + Color::White => Direction::South, + Color::Black => Direction::North, + }); + pieces + .get(self.turn) + .king() + .first() + .is_some_and(|king_square| { + !(d.bishop(king_square, blockers) + & (pieces.get(!self.turn).queen() | pieces.get(!self.turn).bishop())) + .is_empty() + }) + }) { + reasons.add(IllegalPositionReason::ImpossibleEnPassantPin); + } + + if reasons.0 != 0 { + Err(IllegalPosition { + setup: self, + reasons, + }) + } else { + Ok(unsafe { Position::from_setup(self) }) + } + } + + #[inline] + pub(crate) fn get_role(&self, square: Square) -> Option { + let mask = square.bitboard(); + let bit0 = (self.p_b_q & mask).0 >> square as u8; + let bit1 = (self.n_b_k & mask).0 >> square as u8; + let bit2 = (self.r_q_k & mask).0 >> square as u8; + match bit0 | bit1 << 1 | bit2 << 2 { + 0 => None, + i => Some(unsafe { Role::transmute(i as u8) }), + } + } + + #[inline] + pub(crate) fn bitboards(&self) -> ByColor> { + let Self { + w, + p_b_q, + n_b_k, + r_q_k, + .. + } = self.clone(); + let k = n_b_k & r_q_k; + let q = p_b_q & r_q_k; + let b = p_b_q & n_b_k; + let n = n_b_k ^ b ^ k; + let r = r_q_k ^ q ^ k; + let p = p_b_q ^ b ^ q; + ByColor::new(|color| { + let mask = match color { + Color::White => w, + Color::Black => !w, + }; + ByRole::new(|kind| { + mask & match kind { + Role::King => k, + Role::Queen => q, + Role::Bishop => b, + Role::Knight => n, + Role::Rook => r, + Role::Pawn => p, + } + }) + }) + } +} + +impl std::fmt::Debug for Setup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_tuple("Setup").field(&self.to_string()).finish() + } +} + +impl std::fmt::Display for Setup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + + for rank in Rank::all().into_iter().rev() { + let mut count = 0; + for file in File::all() { + match self.get(Square::new(file, rank)) { + Some(piece) => { + if count > 0 { + f.write_char(char::from_u32('0' as u32 + count).unwrap())?; + } + count = 0; + f.write_char(match piece.color { + Color::White => piece.role.to_char_uppercase(), + Color::Black => piece.role.to_char_lowercase(), + })?; + } + None => { + count += 1; + } + } + } + if count > 0 { + f.write_char(char::from_u32('0' as u32 + count).unwrap())?; + } + if rank != Rank::First { + f.write_char('/')?; + } + } + + f.write_char(' ')?; + + f.write_char(match self.turn { + Color::White => 'w', + Color::Black => 'b', + })?; + + f.write_char(' ')?; + + let mut no_castle_available = true; + if self.castling_rights(Color::White, CastlingSide::Short) { + f.write_char('K')?; + no_castle_available = false; + } + if self.castling_rights(Color::White, CastlingSide::Long) { + f.write_char('Q')?; + no_castle_available = false; + } + if self.castling_rights(Color::Black, CastlingSide::Short) { + f.write_char('k')?; + no_castle_available = false; + } + if self.castling_rights(Color::Black, CastlingSide::Long) { + f.write_char('q')?; + no_castle_available = false; + } + if no_castle_available { + f.write_char('-')?; + } + + f.write_char(' ')?; + + match self.en_passant.try_into_square() { + Some(sq) => { + f.write_str(sq.to_str())?; + } + None => { + write!(f, "-")?; + } + } + + Ok(()) + } +} + +impl std::str::FromStr for Setup { + type Err = ParseSetupError; + #[inline] + fn from_str(s: &str) -> Result { + Self::from_ascii(s.as_bytes()) + } +} + +/// An error when trying to parse a position record. +/// +/// The variant indicates the field that caused the error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParseSetupError { + InvalidBoard, + InvalidTurn, + InvalidCastlingRights, + InvalidEnPassantTargetSquare, +} +impl std::fmt::Display for ParseSetupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let details = match self { + Self::InvalidBoard => "board", + Self::InvalidTurn => "turn", + Self::InvalidCastlingRights => "castling rights", + Self::InvalidEnPassantTargetSquare => "en passant target square", + }; + write!(f, "invalid text record ({details})") + } +} +impl std::error::Error for ParseSetupError {} + +/// An invalid position. +/// +/// This is an illegal position that can't be represented with the [`Position`] type. +#[derive(Debug)] +pub struct IllegalPosition { + setup: Setup, + reasons: IllegalPositionReasons, +} +impl std::fmt::Display for IllegalPosition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + let setup = &self.setup; + write!(f, "`{setup}` is illegal:")?; + let mut first = true; + for reason in self.reasons { + if !first { + f.write_char(',')?; + } + first = false; + write!(f, " {reason}")?; + } + Ok(()) + } +} +impl std::error::Error for IllegalPosition {} + +impl IllegalPosition { + /// Returns an iterator over the reasons why the position is rejected. + pub fn reasons(&self) -> IllegalPositionReasons { + self.reasons + } + + /// Returns the [`Setup`] that failed validation. + pub fn as_setup(&self) -> &Setup { + &self.setup + } + + /// Returns the [`Setup`] that failed validation. + pub fn into_setup(self) -> Setup { + self.setup + } +} + +/// A set of [`IllegalPositionReason`]s. +#[derive(Clone, Copy)] +pub struct IllegalPositionReasons(u8); + +impl std::fmt::Debug for IllegalPositionReasons { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(*self).finish() + } +} + +impl IllegalPositionReasons { + /// Returns `true` if the given reason appears in the set. + pub fn contains(&self, reason: IllegalPositionReason) -> bool { + (self.0 & reason as u8) != 0 + } + + fn new() -> Self { + IllegalPositionReasons(0) + } + + fn add(&mut self, reason: IllegalPositionReason) { + self.0 |= reason as u8; + } +} + +impl Iterator for IllegalPositionReasons { + type Item = IllegalPositionReason; + fn next(&mut self) -> Option { + if self.0 == 0 { + None + } else { + let reason = 1 << self.0.trailing_zeros(); + self.0 &= !reason; + Some(unsafe { std::mem::transmute::(reason) }) + } + } +} + +/// Reasons for illegal positions to be rejected by eschac. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum IllegalPositionReason { + /// One of the colors misses its king. + MissingKing = 1, + /// There is more than one king of the same color. + TooManyKings = 2, + /// The opponent's king is in check. + HangingKing = 4, + /// There is a pawn on the first or eighth rank. + PawnOnBackRank = 8, + /// Some castling rights are invalid regarding the positions of the rooks and kings. + InvalidCastlingRights = 16, + /// The en passant target square is invalid, either because: + /// - it is not on the correct rank + /// - it is occupied + /// - it is not behind an opponent's pawn + InvalidEnPassant = 32, + /// There is an impossible number of pieces. + /// + /// Enforcing this enables to put an upper limit on the number of legal moves on any position, + /// allowing to reduce the size of [`Moves`]. + TooMuchMaterial = 64, + /// The pawn that can be taken en passant is pinned diagonally to the playing king. + /// + /// This can't happen on a legal position, as it would imply that the king could have be taken + /// on that move. Enforcing this makes it unnecessary to test for a discovery check on the + /// diagonal when taking en passant. + ImpossibleEnPassantPin = 128, +} + +impl std::fmt::Display for IllegalPositionReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::MissingKing => "missing king", + Self::TooManyKings => "too many kings", + Self::HangingKing => "hanging king", + Self::PawnOnBackRank => "pawn on back rank", + Self::InvalidCastlingRights => "invalid castling rights", + Self::InvalidEnPassant => "invalid en passant", + Self::TooMuchMaterial => "too much material", + Self::ImpossibleEnPassantPin => "illegal en passant", + }) + } +} diff --git a/src/uci.rs b/src/uci.rs new file mode 100644 index 0000000..704fba2 --- /dev/null +++ b/src/uci.rs @@ -0,0 +1,95 @@ +//! UCI notation. +//! +//! Move notation as defined by the Universal Chess Interface standard used for most chess engines +//! and chess servers. +//! +//! A move is described by its origin and target squares. Castling is described as the move done by +//! the king. For promotion, a lowercase letter is added at the end of the move. +//! +//! Examples: *`e2e4`*, *`d1d8`*, *`e1g1`* (short castling), *`h7h8q`* (promotion) + +use crate::board::*; +use crate::position::*; + +/// **The UCI notation of a move.** +/// +/// UCI notation can be obtained from a legal move using the [`Move::to_uci`] method. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct UciMove { + pub from: Square, + pub to: Square, + pub promotion: Option, +} + +impl std::fmt::Display for UciMove { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}{}", self.from, self.to)?; + if let Some(promotion) = self.promotion { + write!(f, "{}", promotion.to_char_lowercase())?; + } + Ok(()) + } +} + +impl std::fmt::Debug for UciMove { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_tuple("UciMove").field(&self.to_string()).finish() + } +} + +impl std::str::FromStr for UciMove { + type Err = ParseUciMoveError; + #[inline] + fn from_str(s: &str) -> Result { + Self::from_ascii(s.as_bytes()).ok_or(ParseUciMoveError) + } +} + +/// A syntax error when parsing a [`UciMove`]. +#[derive(Debug)] +pub struct ParseUciMoveError; +impl std::fmt::Display for ParseUciMoveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("invalid UCI notation") + } +} +impl std::error::Error for ParseUciMoveError {} + +/// An error when converting [`UciMove`] notation to a playable [`Move`]. +#[derive(Debug)] +pub enum InvalidUciMove { + /// The is no move on the position that matches the UCI notation. + Illegal, +} +impl std::fmt::Display for InvalidUciMove { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("illegal UCI move") + } +} +impl std::error::Error for InvalidUciMove {} + +impl UciMove { + /// Tries to convert UCI notation to a playable move. + #[inline] + pub fn to_move<'l>(&self, position: &'l Position) -> Result, InvalidUciMove> { + position.move_from_uci(*self) + } + + /// Tries to read UCI notation from ascii text. + #[inline] + pub fn from_ascii(s: &[u8]) -> Option { + match s { + [a, b, c, d, s @ ..] => Some(Self { + from: Square::new(File::from_ascii(*a)?, Rank::from_ascii(*b)?), + to: Square::new(File::from_ascii(*c)?, Rank::from_ascii(*d)?), + promotion: match s { + [] => None, + [c] => Some(Role::from_ascii(*c)?), + _ => return None, + }, + }), + _ => None, + } + } +} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..e010b81 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,302 @@ +use eschac::board::*; +use eschac::position::*; +use eschac::san::*; +use eschac::setup::*; + +static P1: &'static str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -"; +static P2: &'static str = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -"; +static P3: &'static str = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -"; +static P4: &'static str = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq -"; +static P5: &'static str = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ -"; +static P6: &'static str = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - -"; + +fn recursive_check_aux(position: Position, depth: usize) { + assert_eq!( + position, + position + .as_setup() + .to_string() + .parse::() + .unwrap() + .validate() + .unwrap(), + ); + + if let Some(passed) = position.pass() { + let mut position = position.clone(); + position.remove_en_passant_target_square(); + assert_eq!(position, passed.pass().unwrap()); + } + + let computed_mirror = { + let mut setup = Setup::new(); + for square in Square::all() { + setup.set( + square.mirror(), + position.get(square).map(|piece| Piece { + role: piece.role, + color: !piece.color, + }), + ); + } + setup.set_turn(!position.turn()); + for color in Color::all() { + for side in CastlingSide::all() { + setup.set_castling_rights(!color, side, position.castling_rights(color, side)); + } + } + setup.set_en_passant_target_square( + position + .en_passant_target_square() + .map(|square| square.mirror()), + ); + setup.validate().unwrap() + }; + let expected_mirror = position.mirror(); + assert_eq!(computed_mirror, expected_mirror); + assert_eq!(expected_mirror.mirror(), position); + + match depth.checked_sub(1) { + None => (), + Some(depth) => { + position.legal_moves().into_iter().for_each(|m| { + let uci = m.to_uci(); + assert_eq!(uci, uci.to_move(&position).unwrap().to_uci()); + let san: San = m.to_san(); + match san.to_move(&position) { + Ok(m) => assert_eq!(san, m.to_san()), + Err(err) => { + panic!("{san} is {err} on {position:?}") + } + }; + recursive_check_aux(m.make(), depth) + }); + } + } +} +fn recursive_check(record: &str) { + recursive_check_aux(record.parse::().unwrap().validate().unwrap(), 4); +} +#[test] +fn recursive_check_1() { + recursive_check(P1); +} +#[test] +fn recursive_check_2() { + recursive_check(P2); +} +#[test] +fn recursive_check_3() { + recursive_check(P3); +} +#[test] +fn recursive_check_4() { + recursive_check(P4); +} +#[test] +fn recursive_check_5() { + recursive_check(P5); +} +#[test] +fn recursive_check_6() { + recursive_check(P6); +} + +#[test] +fn setup() { + assert_eq!(Position::new().as_setup().to_string(), P1); + assert_eq!(Setup::new().to_string(), "8/8/8/8/8/8/8/8 w - -"); + assert_eq!( + "8/8/8/8/1Pp5/8/R1k5/K7 w - b3" + .parse::() + .unwrap() + .to_string(), + "8/8/8/8/1Pp5/8/R1k5/K7 w - b3", + ); + + for (record, err) in [ + ("", ParseSetupError::InvalidBoard), + (" w - -", ParseSetupError::InvalidBoard), + ("8/8/8/8/8/8/8 w - -", ParseSetupError::InvalidBoard), + ("1/1/1/1/1/1/1/1 w - -", ParseSetupError::InvalidBoard), + ( + "44/44/44/44/44/44/44/44 w - -", + ParseSetupError::InvalidBoard, + ), + ("8/8/8/8/8/8/8/8/8 w - -", ParseSetupError::InvalidBoard), + ("p8/8/8/8/8/8/8/8 w - -", ParseSetupError::InvalidBoard), + ("8/8/8/8/8/8/8/8 - - - ", ParseSetupError::InvalidTurn), + ( + "8/8/8/8/8/8/8/8 w QQQQ -", + ParseSetupError::InvalidCastlingRights, + ), + ] { + assert_eq!(record.parse::(), Err(err), "{record}"); + } + for (record, reason) in [ + ( + "8/8/8/8/8/8/8/8 w KQkq -", + IllegalPositionReason::MissingKing, + ), + ( + "3kk3/8/8/8/8/8/8/3KK3 w KQkq -", + IllegalPositionReason::TooManyKings, + ), + ( + "4k3/8/8/1Q6/8/8/8/4K3 w - -", + IllegalPositionReason::HangingKing, + ), + ( + "4k3/8/3N4/8/8/8/8/4K3 w - -", + IllegalPositionReason::HangingKing, + ), + ( + "4k3/8/8/8/8/8/8/4K2R w KQ -", + IllegalPositionReason::InvalidCastlingRights, + ), + ( + "4k3/8/8/8/8/8/8/P3K3 w KQ -", + IllegalPositionReason::PawnOnBackRank, + ), + ( + "p3k3/8/8/8/8/8/8/4K3 w KQ -", + IllegalPositionReason::PawnOnBackRank, + ), + ( + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq e3", + IllegalPositionReason::InvalidEnPassant, + ), + ( + "rnbqkbnr/pppppppp/8/8/8/4P3/PPPP1PPP/RNBQKBNR b KQkq e3", + IllegalPositionReason::InvalidEnPassant, + ), + ( + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e3", + IllegalPositionReason::InvalidEnPassant, + ), + ( + "8/8/8/5B2/4P3/3k4/8/4K3 b - e3", + IllegalPositionReason::ImpossibleEnPassantPin, + ), + ( + "8/8/8/3k4/4P3/5B2/8/4K3 b - e3", + IllegalPositionReason::ImpossibleEnPassantPin, + ), + ( + "rnbqkbnr/pppppppp/8/8/4B3/8/PPPPPPPP/RNBQKBNR b KQkq -", + IllegalPositionReason::TooMuchMaterial, + ), + ( + "rnbqkbnr/pppppppp/8/8/8/QBNP4/PPPPPPP1/RNBQKBNR b KQkq -", + IllegalPositionReason::TooMuchMaterial, + ), + ] { + assert!( + record.parse::().map(|record| record.to_string()) == Ok(record.to_string()), + "{record}", + ); + assert!( + record + .parse::() + .unwrap() + .validate() + .is_err_and(|e| e.reasons().contains(reason)), + "{record} should be invalid because of {reason:?}", + ); + } + + for record in [ + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPB/RNBQKBNR b KQkq -", + "rnbqkbnr/pppppppp/8/8/8/8/NNNNNNNN/RNBQKBNR b KQkq -", + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3", + "3kr3/8/8/8/4P3/8/8/4K3 b - e3", + "8/8/8/3k4/3P4/8/8/3RK3 b - d3", + ] { + assert!(record.parse::().is_ok(), "{record}"); + } +} + +#[test] +fn mirror() { + assert_eq!(Position::new().pass(), Some(Position::new().mirror())); + let position = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b Kq e3" + .parse::() + .unwrap() + .validate() + .unwrap(); + let mirror = "rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w Qk e6" + .parse::() + .unwrap() + .validate() + .unwrap(); + assert_eq!(mirror, position.mirror()); +} + +fn perft_aux(record: &str, tests: &[u128]) { + let position = record.parse::().unwrap().validate().unwrap(); + for (depth, value) in tests.iter().copied().enumerate() { + assert_eq!( + position.perft(depth), + value, + "\"{record}\" at depth {depth}", + ); + } +} +#[test] +fn perft_1() { + perft_aux(P1, &[1, 20, 400, 8_902, 197_281, 4_865_609, 119_060_324]); +} +#[test] +fn perft_2() { + perft_aux(P2, &[1, 48, 2039, 97_862, 4_085_603, 193_690_690]); +} +#[test] +fn perft_3() { + perft_aux( + P3, + &[1, 14, 191, 2_812, 43_238, 674_624, 11_030_083, 178_633_661], + ); +} +#[test] +fn perft_4() { + perft_aux(P4, &[1, 6, 264, 9_467, 422_333, 15_833_292]); +} +#[test] +fn perft_5() { + perft_aux(P5, &[1, 44, 1_486, 62_379, 2_103_487, 89_941_194]); +} +#[test] +fn perft_6() { + perft_aux(P6, &[1, 46, 2_079, 89_890, 3_894_594, 164_075_551]); +} + +#[test] +fn san() { + let position = "8/2KN1p2/5p2/3N1B1k/5PNp/7P/7P/8 w - -" + .parse::() + .unwrap() + .validate() + .unwrap(); + let san1 = "N7xf6#".parse::().unwrap(); + let m1 = san1.to_move(&position).unwrap(); + let san2 = "N5xf6#".parse::().unwrap(); + let m2 = san2.to_move(&position).unwrap(); + let san3 = "Ngxf6+".parse::().unwrap(); + let m3 = san3.to_move(&position).unwrap(); + assert_eq!(m1.to_san(), san1); + assert_eq!(m2.to_san(), san2); + assert_eq!(m3.to_san(), san3); + assert_eq!( + "Nd7f6" + .parse::() + .unwrap() + .to_move(&position) + .unwrap() + .to_san(), + san1, + ); + assert_eq!( + "Nf6".parse::().unwrap().to_move(&position).map(|_| ()), + Err(InvalidSan::Ambiguous), + ); +}