From c0f36f53a6e5af6c319cb8ac1a1519d01f99bd1d Mon Sep 17 00:00:00 2001 From: winapiadmin Date: Wed, 9 Jul 2025 16:00:06 +0700 Subject: [PATCH] added stockfish nnue support and dropped cmake support --- .clang-format | 44 + .gitignore | 22 +- .vscode/settings.json | 80 + CMakeLists.txt | 76 - COPYING | 674 ++++ Makefile | 32 - eval.cpp | 777 ---- eval.hpp | 18 - main.cpp | 15 - movepick.cpp | 185 - movepick.hpp | 18 - scripts/.gitattributes | 1 + scripts/get_native_properties.sh | 159 + scripts/net.sh | 76 + search.cpp | 355 -- search.hpp | 17 - src/.depend | 105 + src/Makefile | 1128 ++++++ README.md => src/README.md | 2 +- src/bitboard.h | 141 + chess.hpp => src/chess.hpp | 3198 ++++++++++------- src/evaluate.cpp | 124 + src/evaluate.h | 58 + src/incbin/UNLICENCE | 26 + src/incbin/incbin.h | 476 +++ src/main.cpp | 28 + src/memory.cpp | 266 ++ src/memory.h | 218 ++ src/misc.h | 62 + src/movepick.cpp | 200 ++ src/movepick.hpp | 17 + src/nnue/features/half_ka_v2_hm.cpp | 86 + src/nnue/features/half_ka_v2_hm.h | 144 + src/nnue/features/half_ka_v2_hm.o | Bin 0 -> 67976 bytes src/nnue/layers/affine_transform.h | 302 ++ .../layers/affine_transform_sparse_input.h | 262 ++ src/nnue/layers/clipped_relu.h | 164 + src/nnue/layers/sqr_clipped_relu.h | 103 + src/nnue/network.cpp | 442 +++ src/nnue/network.h | 137 + src/nnue/network.o | Bin 0 -> 419632 bytes src/nnue/nnue_accumulator.cpp | 531 +++ src/nnue/nnue_accumulator.h | 187 + src/nnue/nnue_accumulator.o | Bin 0 -> 235480 bytes src/nnue/nnue_architecture.h | 143 + src/nnue/nnue_common.h | 288 ++ src/nnue/nnue_feature_transformer.h | 312 ++ src/nnue/nnue_misc.cpp | 193 + src/nnue/nnue_misc.h | 61 + src/nnue/simd.h | 418 +++ src/position.h | 239 ++ src/search.cpp | 254 ++ src/search.h | 24 + src/timeman.cpp | 107 + src/timeman.hpp | 29 + src/tt.cpp | 69 + src/tt.hpp | 66 + src/types.h | 444 +++ src/uci.cpp | 333 ++ uci.hpp => src/uci.hpp | 7 +- src/ucioptions.cpp | 180 + src/ucioptions.hpp | 67 + timeman.cpp | 110 - timeman.hpp | 31 - tt.cpp | 53 - tt.hpp | 122 - uci.cpp | 270 -- ucioptions.cpp | 168 - ucioptions.hpp | 47 - 69 files changed, 11438 insertions(+), 3553 deletions(-) create mode 100644 .clang-format create mode 100644 .vscode/settings.json delete mode 100644 CMakeLists.txt create mode 100644 COPYING delete mode 100644 Makefile delete mode 100644 eval.cpp delete mode 100644 eval.hpp delete mode 100644 main.cpp delete mode 100644 movepick.cpp delete mode 100644 movepick.hpp create mode 100644 scripts/.gitattributes create mode 100755 scripts/get_native_properties.sh create mode 100755 scripts/net.sh delete mode 100644 search.cpp delete mode 100644 search.hpp create mode 100644 src/.depend create mode 100644 src/Makefile rename README.md => src/README.md (88%) create mode 100644 src/bitboard.h rename chess.hpp => src/chess.hpp (60%) create mode 100644 src/evaluate.cpp create mode 100644 src/evaluate.h create mode 100644 src/incbin/UNLICENCE create mode 100644 src/incbin/incbin.h create mode 100644 src/main.cpp create mode 100644 src/memory.cpp create mode 100644 src/memory.h create mode 100644 src/misc.h create mode 100644 src/movepick.cpp create mode 100644 src/movepick.hpp create mode 100644 src/nnue/features/half_ka_v2_hm.cpp create mode 100644 src/nnue/features/half_ka_v2_hm.h create mode 100644 src/nnue/features/half_ka_v2_hm.o create mode 100644 src/nnue/layers/affine_transform.h create mode 100644 src/nnue/layers/affine_transform_sparse_input.h create mode 100644 src/nnue/layers/clipped_relu.h create mode 100644 src/nnue/layers/sqr_clipped_relu.h create mode 100644 src/nnue/network.cpp create mode 100644 src/nnue/network.h create mode 100644 src/nnue/network.o create mode 100644 src/nnue/nnue_accumulator.cpp create mode 100644 src/nnue/nnue_accumulator.h create mode 100644 src/nnue/nnue_accumulator.o create mode 100644 src/nnue/nnue_architecture.h create mode 100644 src/nnue/nnue_common.h create mode 100644 src/nnue/nnue_feature_transformer.h create mode 100644 src/nnue/nnue_misc.cpp create mode 100644 src/nnue/nnue_misc.h create mode 100644 src/nnue/simd.h create mode 100644 src/position.h create mode 100644 src/search.cpp create mode 100644 src/search.h create mode 100644 src/timeman.cpp create mode 100644 src/timeman.hpp create mode 100644 src/tt.cpp create mode 100644 src/tt.hpp create mode 100644 src/types.h create mode 100644 src/uci.cpp rename uci.hpp => src/uci.hpp (55%) create mode 100644 src/ucioptions.cpp create mode 100644 src/ucioptions.hpp delete mode 100644 timeman.cpp delete mode 100644 timeman.hpp delete mode 100644 tt.cpp delete mode 100644 tt.hpp delete mode 100644 uci.cpp delete mode 100644 ucioptions.cpp delete mode 100644 ucioptions.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a470cc0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,44 @@ +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: Consecutive +AlignConsecutiveDeclarations: Consecutive +AlignEscapedNewlines: DontAlign +AlignOperands: AlignAfterOperator +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +BreakTemplateDeclarations: Yes +BasedOnStyle: WebKit +BitFieldColonSpacing: After +BinPackParameters: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: false + AfterClass: false + AfterControlStatement: true + BeforeElse: true +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: AfterColon +BreakStringLiterals: false +ColumnLimit: 100 +ContinuationIndentWidth: 2 +Cpp11BracedListStyle: true +IndentGotoLabels: false +IndentPPDirectives: BeforeHash +IndentWidth: 4 +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: None +PackConstructorInitializers: Never +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeCaseColon: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeInheritanceColon: false +SpaceInEmptyBlock: false +SpacesBeforeTrailingComments: 2 diff --git a/.gitignore b/.gitignore index a54deda..bce9de6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ # Ignore everything -* - -# But not these directories and files -!*.cpp -!*.hpp -!*.h -!Makefile -!README.md -!LICENSE -!.gitignore -!CMakeLists.txt \ No newline at end of file +!COPYING +src/*.nnue +src/*.o +src/cppchess_engine +!src/*.cpp +!src/*.hpp +!src/*.h +!src/layers/* +!src/features/half_ka_v2_hm.cpp +!src/features/half_ka_v2_hm.h +!scripts \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..39f8ea5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,80 @@ +{ + "files.associations": { + "cmath": "cpp", + "array": "cpp", + "bitset": "cpp", + "cwchar": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "fstream": "cpp", + "functional": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "new": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "tuple": "cpp", + "any": "cpp", + "atomic": "cpp", + "hash_map": "cpp", + "bit": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "codecvt": "cpp", + "compare": "cpp", + "complex": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "forward_list": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_set": "cpp", + "algorithm": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "format": "cpp", + "iomanip": "cpp", + "iostream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "queue": "cpp", + "ranges": "cpp", + "semaphore": "cpp", + "span": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "text_encoding": "cpp", + "thread": "cpp", + "cfenv": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "valarray": "cpp", + "variant": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 8f8f427..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,76 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(chess_engine) - -# Use C++17 -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Optional: enable warnings (MSVC specific flags) -if (MSVC) - add_compile_options(/W4 /permissive-) -else() - add_compile_options(-Wall -Wextra -pedantic) -endif() - -# Add the source files -set(SOURCES - main.cpp - eval.cpp - search.cpp - tt.cpp - movepick.cpp - uci.cpp - ucioptions.cpp - timeman.cpp -) - -# Define the executable -add_executable(${PROJECT_NAME} ${SOURCES}) - -# Threading support -find_package(Threads REQUIRED) -target_link_libraries(${PROJECT_NAME} Threads::Threads) - -# Platform-specific threading configurations -if(WIN32) - if(MSVC) - # Windows with MSVC - uses Windows threading API - target_compile_definitions(${PROJECT_NAME} PRIVATE WIN32_LEAN_AND_MEAN) - elseif(MINGW) - # MinGW on Windows - uses pthread - target_compile_definitions(${PROJECT_NAME} PRIVATE _WIN32_WINNT=0x0601) - # MinGW might need explicit pthread linking - if(CMAKE_THREAD_LIBS_INIT) - target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) - endif() - endif() -elseif(UNIX) - # Unix/Linux systems - uses pthread - if(CMAKE_THREAD_LIBS_INIT) - target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT}) - endif() - # Some older systems might need explicit -pthread flag - if(CMAKE_USE_PTHREADS_INIT) - target_compile_options(${PROJECT_NAME} PRIVATE -pthread) - target_link_options(${PROJECT_NAME} PRIVATE -pthread) - endif() -endif() - -# Optional: Add atomic library support for older compilers -if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "4.9") - target_link_libraries(${PROJECT_NAME} atomic) -endif() - -# Set default build type if none is specified -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the build type (Debug, Release, etc.)" FORCE) -endif() - -# Optional: Add specific compile flags per build type -if(NOT MSVC) - set(CMAKE_CXX_FLAGS_DEBUG "-g -march=native -mtune=native") - set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -march=native -mtune=native -funroll-loops -ftree-vectorize -fomit-frame-pointer") -else() - set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Od") - set(CMAKE_CXX_FLAGS_RELEASE "/O2 /DNDEBUG") -endif() diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile deleted file mode 100644 index 36d7111..0000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -# Compiler and Flags -CXX = g++ - -# Source files and object files -SRC = main.cpp eval.cpp search.cpp tt.cpp movepick.cpp timeman.cpp uci.cpp ucioptions.cpp -OBJ = $(SRC:.cpp=.o) - -# Output file -EXEC = chess_engine - -# Default target: build the project -all: $(EXEC) - -# Linking the object files into the executable -$(EXEC): $(OBJ) - $(CXX) $(LDFLAGS) -o $(EXEC) $(OBJ) - -# Rule to build object files from source files -%.o: %.cpp - $(CXX) $(CXXFLAGS) -std=c++17 -c $< -o $@ - -# Clean up the compiled files -clean: - rm -f $(OBJ) $(EXEC) - -# To run the program after compilation -run: $(EXEC) - ./$(EXEC) - -# Rebuild the project -rebuild: clean all - diff --git a/eval.cpp b/eval.cpp deleted file mode 100644 index a0ba6cd..0000000 --- a/eval.cpp +++ /dev/null @@ -1,777 +0,0 @@ -#include "eval.hpp" -#include -#define EvaluationResult int16_t -// 0. Evaluation result - -struct EvalBreakdown -{ - EvaluationResult phase; - EvaluationResult Material; - EvaluationResult Doubled; - EvaluationResult Isolated; - EvaluationResult Backward; - EvaluationResult Passed; - EvaluationResult Center; - EvaluationResult Mobility; - EvaluationResult Shield; - EvaluationResult KingTropism; - EvaluationResult Space; - EvaluationResult MGPSQT; - EvaluationResult EGPSQT; -}; - -EvalBreakdown wEval{}, bEval{}; -// 1. Weights, in case of tuning (centipawns) -// 1.1. Material weights -constexpr int16_t PawnValue = 100, KnightValue = 325, BishopValue = 350, RookValue = 500, QueenValue = 900; -// 1.2. Pawn structure -constexpr int16_t Doubled = -100, Isolated = -80, Backward = -100, wpassed = 100, cpassed = 50, - Center = 100; -// 1.3. Mobility - -// Mobility weights per piece type -constexpr int MOBILITY_WEIGHTS[] = { - 0, // EMPTY / None - 1, // PAWN (often ignored or handled separately) - 4, // KNIGHT - 4, // BISHOP - 6, // ROOK - 8, // QUEEN - 0 // KING (usually ignored) -}; -// 1.3. King safety -constexpr int16_t pawnShield = 20, KingTropism = 5; -// 1.4. Space -constexpr int16_t Space = 10; -// 1.5. Tempo -constexpr int16_t Tempo = 28; -// 1.6. PQST (source: Chessprogramming Wiki) -// clang-format off -int16_t mg_pawn_table[64] = { - 0, 0, 0, 0, 0, 0, 0, 0, - 98, 134, 61, 95, 68, 126, 34, -11, - -6, 7, 26, 31, 65, 56, 25, -20, - -14, 13, 6, 21, 23, 12, 17, -23, - -27, -2, -5, 12, 17, 6, 10, -25, - -26, -4, -4, -10, 3, 3, 33, -12, - -35, -1, -20, -23, -15, 24, 38, -22, - 0, 0, 0, 0, 0, 0, 0, 0, -}; - -int16_t eg_pawn_table[64] = { - 0, 0, 0, 0, 0, 0, 0, 0, - 178, 173, 158, 134, 147, 132, 165, 187, - 94, 100, 85, 67, 56, 53, 82, 84, - 32, 24, 13, 5, -2, 4, 17, 17, - 13, 9, -3, -7, -7, -8, 3, -1, - 4, 7, -6, 1, 0, -5, -1, -8, - 13, 8, 8, 10, 13, 0, 2, -7, - 0, 0, 0, 0, 0, 0, 0, 0, -}; - -int16_t mg_knight_table[64] = { - -167, -89, -34, -49, 61, -97, -15, -107, - -73, -41, 72, 36, 23, 62, 7, -17, - -47, 60, 37, 65, 84, 129, 73, 44, - -9, 17, 19, 53, 37, 69, 18, 22, - -13, 4, 16, 13, 28, 19, 21, -8, - -23, -9, 12, 10, 19, 17, 25, -16, - -29, -53, -12, -3, -1, 18, -14, -19, - -105, -21, -58, -33, -17, -28, -19, -23, -}; - -int16_t eg_knight_table[64] = { - -58, -38, -13, -28, -31, -27, -63, -99, - -25, -8, -25, -2, -9, -25, -24, -52, - -24, -20, 10, 9, -1, -9, -19, -41, - -17, 3, 22, 22, 22, 11, 8, -18, - -18, -6, 16, 25, 16, 17, 4, -18, - -23, -3, -1, 15, 10, -3, -20, -22, - -42, -20, -10, -5, -2, -20, -23, -44, - -29, -51, -23, -15, -22, -18, -50, -64, -}; - -int16_t mg_bishop_table[64] = { - -29, 4, -82, -37, -25, -42, 7, -8, - -26, 16, -18, -13, 30, 59, 18, -47, - -16, 37, 43, 40, 35, 50, 37, -2, - -4, 5, 19, 50, 37, 37, 7, -2, - -6, 13, 13, 26, 34, 12, 10, 4, - 0, 15, 15, 15, 14, 27, 18, 10, - 4, 15, 16, 0, 7, 21, 33, 1, - -33, -3, -14, -21, -13, -12, -39, -21, -}; - -int16_t eg_bishop_table[64] = { - -14, -21, -11, -8, -7, -9, -17, -24, - -8, -4, 7, -12, -3, -13, -4, -14, - 2, -8, 0, -1, -2, 6, 0, 4, - -3, 9, 12, 9, 14, 10, 3, 2, - -6, 3, 13, 19, 7, 10, -3, -9, - -12, -3, 8, 10, 13, 3, -7, -15, - -14, -18, -7, -1, 4, -9, -15, -27, - -23, -9, -23, -5, -9, -16, -5, -17, -}; - -int16_t mg_rook_table[64] = { - 32, 42, 32, 51, 63, 9, 31, 43, - 27, 32, 58, 62, 80, 67, 26, 44, - -5, 19, 26, 36, 17, 45, 61, 16, - -24, -11, 7, 26, 24, 35, -8, -20, - -36, -26, -12, -1, 9, -7, 6, -23, - -45, -25, -16, -17, 3, 0, -5, -33, - -44, -16, -20, -9, -1, 11, -6, -71, - -19, -13, 1, 17, 16, 7, -37, -26, -}; - -int16_t eg_rook_table[64] = { - 13, 10, 18, 15, 12, 12, 8, 5, - 11, 13, 13, 11, -3, 3, 8, 3, - 7, 7, 7, 5, 4, -3, -5, -3, - 4, 3, 13, 1, 2, 1, -1, 2, - 3, 5, 8, 4, -5, -6, -8, -11, - -4, 0, -5, -1, -7, -12, -8, -16, - -6, -6, 0, 2, -9, -9, -11, -3, - -9, 2, 3, -1, -5, -13, 4, -20, -}; - -int16_t mg_queen_table[64] = { - -28, 0, 29, 12, 59, 44, 43, 45, - -24, -39, -5, 1, -16, 57, 28, 54, - -13, -17, 7, 8, 29, 56, 47, 57, - -27, -27, -16, -16, -1, 17, -2, 1, - -9, -26, -9, -10, -2, -4, 3, -3, - -14, 2, -11, -2, -5, 2, 14, 5, - -35, -8, 11, 2, 8, 15, -3, 1, - -1, -18, -9, 10, -15, -25, -31, -50, -}; - -int16_t eg_queen_table[64] = { - -9, 22, 22, 27, 27, 19, 10, 20, - -17, 20, 32, 41, 58, 25, 30, 0, - -20, 6, 9, 49, 47, 35, 19, 9, - 3, 22, 24, 45, 57, 40, 57, 36, - -18, 28, 19, 47, 31, 34, 39, 23, - -16, -27, 15, 6, 9, 17, 10, 5, - -22, -23, -30, -16, -16, -23, -36, -32, - -33, -28, -22, -43, -5, -32, -20, -41, -}; - -int16_t mg_king_table[64] = { - -65, 23, 16, -15, -56, -34, 2, 13, - 29, -1, -20, -7, -8, -4, -38, -29, - -9, 24, 2, -16, -20, 6, 22, -22, - -17, -20, -12, -27, -30, -25, -14, -36, - -49, -1, -27, -39, -46, -44, -33, -51, - -14, -14, -22, -46, -44, -30, -15, -27, - 1, 7, -8, -64, -43, -16, 9, 8, - -15, 36, 12, -54, 8, -28, 24, 14, -}; - -int16_t eg_king_table[64] = { - -74, -35, -18, -18, -11, 15, 4, -17, - -12, 17, 14, 17, 17, 38, 23, 11, - 10, 17, 23, 15, 20, 45, 44, 13, - -8, 22, 24, 27, 26, 33, 26, 3, - -18, -4, 21, 24, 27, 23, 9, -11, - -19, -3, 11, 21, 23, 16, 7, -9, - -27, -11, 4, 13, 14, 4, -5, -17, - -53, -34, -21, -11, -28, -14, -24, -43 -}; - -int16_t* mg_pesto_table[6] = -{ - mg_pawn_table, - mg_knight_table, - mg_bishop_table, - mg_rook_table, - mg_queen_table, - mg_king_table -}; - -int16_t* eg_pesto_table[6] = -{ - eg_pawn_table, - eg_knight_table, - eg_bishop_table, - eg_rook_table, - eg_queen_table, - eg_king_table -}; - -// clang-format on -// 2. Evaluation functions -// 2.1. Phase -Evaluation int phase(const chess::Board &pos) -{ - constexpr int KnightPhase = 1; - constexpr int BishopPhase = 1; - constexpr int RookPhase = 2; - constexpr int QueenPhase = 4; - constexpr int TotalPhase = KnightPhase * 4 + BishopPhase * 4 + RookPhase * 4 + QueenPhase * 2; - - int phase = (pos.pieces(chess::PieceType::KNIGHT, chess::Color::WHITE).count() + - pos.pieces(chess::PieceType::KNIGHT, chess::Color::BLACK).count()) * - KnightPhase + - (pos.pieces(chess::PieceType::BISHOP, chess::Color::WHITE).count() + - pos.pieces(chess::PieceType::BISHOP, chess::Color::BLACK).count()) * - BishopPhase + - (pos.pieces(chess::PieceType::ROOK, chess::Color::WHITE).count() + - pos.pieces(chess::PieceType::ROOK, chess::Color::BLACK).count()) * - RookPhase + - (pos.pieces(chess::PieceType::QUEEN, chess::Color::WHITE).count() + - pos.pieces(chess::PieceType::QUEEN, chess::Color::BLACK).count()) * - QueenPhase; - - return wEval.phase = (phase * 256 + TotalPhase / 2) / TotalPhase; -} -// 2.2. Material (one-line) (all phases) -AllPhases inline int16_t material(const chess::Board &pos) -{ // Precompute piece counts (avoid multiple function calls) - int pieceCount[10] = { - pos.pieces(chess::PieceType::PAWN, chess::Color::WHITE).count(), - pos.pieces(chess::PieceType::KNIGHT, chess::Color::WHITE).count(), - pos.pieces(chess::PieceType::BISHOP, chess::Color::WHITE).count(), - pos.pieces(chess::PieceType::ROOK, chess::Color::WHITE).count(), - pos.pieces(chess::PieceType::QUEEN, chess::Color::WHITE).count(), - pos.pieces(chess::PieceType::PAWN, chess::Color::BLACK).count(), - pos.pieces(chess::PieceType::KNIGHT, chess::Color::BLACK).count(), - pos.pieces(chess::PieceType::BISHOP, chess::Color::BLACK).count(), - pos.pieces(chess::PieceType::ROOK, chess::Color::BLACK).count(), - pos.pieces(chess::PieceType::QUEEN, chess::Color::BLACK).count(), - }; - wEval.Material = pieceCount[0] * PawnValue + pieceCount[1] * KnightValue + pieceCount[2] * BishopValue + - pieceCount[3] * RookValue + pieceCount[4] * QueenValue; - bEval.Material = pieceCount[5] * PawnValue + pieceCount[6] * KnightValue + pieceCount[7] * BishopValue + - pieceCount[8] * RookValue + pieceCount[9] * QueenValue; - return wEval.Material - bEval.Material; -} -// 2.3. Pawn structure -// 2.3.1. Doubled pawns -Middlegame int16_t doubled(const chess::Board &pos) -{ - wEval.Doubled = bEval.Doubled = 0; - chess::Bitboard P = pos.pieces(chess::PieceType::PAWN, chess::Color::WHITE), - p = pos.pieces(chess::PieceType::PAWN, chess::Color::BLACK); - // We use bitwise tricks to avoid conditionals - for (int i = 0; i < 8; ++i) - { - chess::Bitboard white = P & chess::attacks::MASK_FILE[i], black = p & chess::attacks::MASK_FILE[i]; - if (white.count() > 1) - wEval.Doubled += (white.count() - 1) * Doubled; - if (black.count() > 1) - bEval.Doubled += (black.count() - 1) * Doubled; - } - return wEval.Doubled - bEval.Doubled; -} -// 2.3.2. Isolated pawns -int16_t isolated(const chess::Board &pos) -{ - chess::Bitboard P = pos.pieces(chess::PieceType::PAWN, chess::Color::WHITE); - chess::Bitboard p = pos.pieces(chess::PieceType::PAWN, chess::Color::BLACK); - - constexpr chess::Bitboard noAFile = 0xFEFEFEFEFEFEFEFEULL; // mask out file A - constexpr chess::Bitboard noHFile = 0x7F7F7F7F7F7F7F7FULL; // mask out file H - - chess::Bitboard adjP = ((P & noHFile) << 1) | ((P & noAFile) >> 1); - chess::Bitboard adjp = ((p & noHFile) << 1) | ((p & noAFile) >> 1); - - chess::Bitboard Pi = P & ~adjP; - chess::Bitboard pi = p & ~adjp; - - wEval.Isolated = Pi.count() * Isolated; - bEval.Isolated = pi.count() * Isolated; - - return wEval.Isolated - bEval.Isolated; -} - -// 2.3.3. Backward (https://www.chessprogramming.org/Backward_Pawns_(Bitboards)) -Middlegame inline int16_t backward(const chess::Board &pos) -{ - chess::Bitboard P = pos.pieces(chess::PieceType::PAWN, chess::Color::WHITE), - p = pos.pieces(chess::PieceType::PAWN, chess::Color::BLACK), wstop = P << 8, bstop = p << 8, - wAttacks = chess::attacks::pawnLeftAttacks(P) | - chess::attacks::pawnRightAttacks(P), - bAttacks = chess::attacks::pawnLeftAttacks(p) | - chess::attacks::pawnRightAttacks(p), - wback = (wstop & bAttacks & ~wAttacks) >> 8, bback = (bstop & wAttacks & ~bAttacks) >> 8; - wEval.Backward = wback.count() * Backward; - bEval.Backward = bback.count() * Backward; - return wEval.Backward - bEval.Backward; -} -// 2.3.4. Passed pawns (including candidate ones) (considered in both middlegame and endgame) - -AllPhases int16_t passed(const chess::Board &pos) -{ - chess::Bitboard whitePawns = pos.pieces(chess::PieceType::PAWN, chess::Color::WHITE); - chess::Bitboard blackPawns = pos.pieces(chess::PieceType::PAWN, chess::Color::BLACK); - - auto compute_score = [](chess::Bitboard P, chess::Bitboard p, bool isWhite) -> int16_t - { - chess::Bitboard mask = p; - mask |= (p << 1 | p >> 1) & 0x6F6F6F6F6F6F6F6F; // ~FILE_A&~FILE_H - - if (isWhite) - { - mask |= (mask << 8); - mask |= (mask << 16); - mask |= (mask << 32); - } - else - { - mask |= (mask >> 8); - mask |= (mask >> 16); - mask |= (mask >> 32); - } - - chess::Bitboard passed = P & ~mask; - chess::Bitboard candidate = P & mask; - - return wpassed * passed.count() + cpassed * candidate.count(); - }; - - wEval.Passed = compute_score(whitePawns, blackPawns, true); - bEval.Passed = compute_score(blackPawns, whitePawns, false); - - return wEval.Passed - bEval.Passed; -} - -Middlegame int16_t center(const chess::Board &pos) -{ - const chess::Square centerSquares[] = {chess::Square::SQ_E4, chess::Square::SQ_D4, chess::Square::SQ_E5, - chess::Square::SQ_D5}; - - int w = 0, b = 0; - - for (chess::Square sq : centerSquares) - { - // Count attackers - w += chess::attacks::attackers(pos, chess::Color::WHITE, sq).count(); - b += chess::attacks::attackers(pos, chess::Color::BLACK, sq).count(); - auto p = pos.at(sq); - if (p.color() == chess::Color::WHITE) - ++w; - if (p.color() == chess::Color::BLACK) - ++b; - } - - wEval.Center = w * Center; - bEval.Center = b * Center; - return wEval.Center - bEval.Center; -} -/* Pawn cache */ -struct PawnEntry -{ - uint64_t key; // Full pawn hash key (Zobrist) - int16_t evalWhite; // Pawn eval score for White - int16_t evalBlack; // Pawn eval score for Black -}; - -std::vector pawnHashTable(1 << 17); -template -AllPhases int pawn(const chess::Board &pos) -{ - if (trace) - { - return doubled(pos) + isolated(pos) + backward(pos) + center(pos); - } - else - { - // what? Nah. - auto a = pos.hash(); - auto entry = pawnHashTable[a & 131071]; - if (entry.key == a) - return entry.evalWhite - entry.evalBlack; - else - { - int16_t score = doubled(pos) + isolated(pos) + backward(pos) + center(pos); - PawnEntry entry = {a, - (int16_t)(wEval.Doubled + wEval.Isolated + wEval.Backward + wEval.Center), - (int16_t)(bEval.Doubled + bEval.Isolated + bEval.Backward + bEval.Center)}; - pawnHashTable[a & 131071] = entry; - return score; - } - } -} -// 2.4. Mobility -AllPhases int16_t mobility(const chess::Board &board) -{ - using namespace chess; - Bitboard occupied = board.occ(); - int mobilityScore[2] = {0, 0}; - const Color sides[2] = {Color::WHITE, Color::BLACK}; - - // Lookup table for attack functions per piece type - auto attackFuncs = [](Square sq, Bitboard occ, PieceType pt) -> Bitboard - { - switch (pt) - { - case (int)PieceType::KNIGHT: - return attacks::knight(sq); - case (int)PieceType::BISHOP: - return attacks::bishop(sq, occ); - case (int)PieceType::ROOK: - return attacks::rook(sq, occ); - case (int)PieceType::QUEEN: - return attacks::queen(sq, occ); - default: - return 0; - } - }; - - for (Color side : sides) - { - Bitboard us = board.us(side); - - for (PieceType pt : {PieceType::KNIGHT, PieceType::BISHOP, PieceType::ROOK, PieceType::QUEEN}) - { - int ptIndex = static_cast(pt); - Bitboard pieces = board.pieces(pt) & us; - while (pieces) - { - Square sq = pieces.pop(); - Bitboard attacks = attackFuncs(sq, occupied, pt); - attacks &= ~us; // remove friendly squares - mobilityScore[static_cast(side)] += MOBILITY_WEIGHTS[ptIndex] * attacks.count(); - } - } - } - - wEval.Mobility = mobilityScore[0]; - bEval.Mobility = mobilityScore[1]; - return mobilityScore[0] - mobilityScore[1]; -} - -// 2.5. King safety -// 2.5.1. Pawn shield -Middlegame int16_t pawn_shield(const chess::Board &board) -{ - constexpr chess::Bitboard SHIELD = 0x00ff00000000ff00ULL; // rank 2 and 7 for pawn shield - constexpr chess::Bitboard SHIELD2 = 0x0000ff0000ff0000ULL; // rank 3 and 6 for isolated pawns - chess::Bitboard wkingAttacksBB = chess::attacks::king(board.kingSq(chess::Color::WHITE)), - bKingAttacksBB = chess::attacks::king(board.kingSq(chess::Color::BLACK)); - int wshieldCount1 = (wkingAttacksBB & SHIELD).count(), wshieldCount2 = (wkingAttacksBB & SHIELD2).count(); - int bshieldCount1 = (bKingAttacksBB & SHIELD).count(), bshieldCount2 = (bKingAttacksBB & SHIELD2).count(); - wEval.Shield = (!wshieldCount1 * wshieldCount2 + wshieldCount1) * pawnShield; - bEval.Shield = (!bshieldCount1 * bshieldCount2 + bshieldCount1) * pawnShield; - return wEval.Shield - bEval.Shield; -} - -Middlegame int16_t king_tropism(const chess::Board &board) -{ - using namespace chess; - const Square wKing = board.kingSq(Color::WHITE); - const Square bKing = board.kingSq(Color::BLACK); - - int wScore = 0, bScore = 0; - - // Consider only attacking pieces - for (PieceType pt : {PieceType::QUEEN, PieceType::ROOK, PieceType::BISHOP, PieceType::KNIGHT}) - { - Bitboard whiteAttackers = board.pieces(pt, Color::WHITE); - Bitboard blackAttackers = board.pieces(pt, Color::BLACK); - - while (whiteAttackers) - { - Square sq = whiteAttackers.pop(); - wScore += 7 - chess::Square::value_distance(sq, bKing); // closer = higher danger - } - - while (blackAttackers) - { - Square sq = blackAttackers.pop(); - bScore += 7 - chess::Square::value_distance(sq, wKing); - } - } - - // Scale the result - wEval.KingTropism = wScore * KingTropism; - bEval.KingTropism = bScore * KingTropism; - - return wEval.KingTropism - bEval.KingTropism; -} - -Middlegame inline int16_t king_safety(const chess::Board &board) { return pawn_shield(board) + king_tropism(board); } -// 2.6. Space -AllPhases int space(const chess::Board &board) -{ - int spaceS[2] = {0, 0}; // [WHITE, BLACK] - - // Evaluate space control for both sides - for (chess::Color color : {chess::Color::WHITE, chess::Color::BLACK}) - { - chess::Color opponent = ~color; - - // Space Evaluation: Count controlled squares in the opponent's half - chess::Bitboard my_pawns = board.pieces(chess::PieceType::PAWN, color); - - while (my_pawns) - { - chess::Square sq = my_pawns.pop(); - chess::Bitboard pawn_attacks = chess::attacks::pawn(color, sq); - - while (pawn_attacks) - { - chess::Square target = pawn_attacks.pop(); - // Check if square is in opponent's half - bool in_opponent_half = - (color == chess::Color::WHITE && target >= 32) || (color == chess::Color::BLACK && target < 32); - - if (in_opponent_half && !board.isAttacked(target, opponent) && !board.at(target)) - { - spaceS[static_cast(color)]++; - } - } - } - } - wEval.Space = spaceS[0] * Space; - bEval.Space = spaceS[1] * Space; - return wEval.Space - bEval.Space; -} -// 2.7. Tempo (simple) (all phases) -AllPhases inline int16_t tempo(const chess::Board &board) -{ - return (board.sideToMove() == chess::Color::WHITE) ? Tempo : -Tempo; -} -// 2.8. Endgame -Endgame bool draw(const chess::Board &board) -{ - // KPvK with Rule of the Square - chess::Square wk = board.kingSq(chess::Color::WHITE), bk = board.kingSq(chess::Color::BLACK); - chess::Bitboard p = board.pieces(chess::PieceType::PAWN); - if (!board.hasNonPawnMaterial(chess::Color::WHITE) && !board.hasNonPawnMaterial(chess::Color::BLACK) && p.count() == 1) - { - chess::Square pawn = p.pop(); - auto c = board.at(pawn).color(); - chess::Square promoSq(pawn.file(), chess::Rank::rank(chess::Rank::RANK_8, c)); - if (chess::Square::value_distance((c ? bk : wk), promoSq) <= chess::Square::value_distance(pawn, promoSq)) - return 1; - } - return board.isInsufficientMaterial(); -} -template -AllPhases int16_t psqt_eval(const chess::Board &board) -{ - auto pieces = board.us(chess::Color::WHITE); - - while (pieces) - { - int sq = pieces.pop(); - chess::PieceType piece = board.at(sq); - if constexpr (Midgame) - wEval.MGPSQT += mg_pesto_table[(int)piece][sq]; - else - wEval.EGPSQT += eg_pesto_table[(int)piece][sq]; - } - pieces = board.us(chess::Color::BLACK); - - while (pieces) - { - chess::Square sq = pieces.pop(); - chess::PieceType piece = board.at(sq); - sq.flip(); - if constexpr (Midgame) - bEval.MGPSQT += mg_pesto_table[(int)piece][sq.index()]; - else - bEval.EGPSQT += eg_pesto_table[(int)piece][sq.index()]; - } - if constexpr (Midgame) - return wEval.MGPSQT - bEval.MGPSQT; // Negate for Black - else - return wEval.EGPSQT - bEval.EGPSQT; // Negate for Black -} -// huge table for distances, idk but it gives O(1) access -static const unsigned char chebyshev_distance[64][64] = { - {0, 1, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 4, 4, 4, 4, 4, 5, 6, 7, 5, 5, 5, 5, 5, 5, 6, 7, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7}, - {1, 0, 1, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 4, 4, 4, 4, 4, 4, 5, 6, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7}, - {2, 1, 0, 1, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7}, - {3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7}, - {4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7}, - {5, 4, 3, 2, 1, 0, 1, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7}, - {6, 5, 4, 3, 2, 1, 0, 1, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 4, 4, 4, 4, 4, 6, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7}, - {7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 4, 4, 4, 4, 7, 6, 5, 5, 5, 5, 5, 5, 7, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7}, - {1, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 4, 4, 4, 4, 4, 5, 6, 7, 5, 5, 5, 5, 5, 5, 6, 7, 6, 6, 6, 6, 6, 6, 6, 7}, - {1, 1, 1, 2, 3, 4, 5, 6, 1, 0, 1, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 4, 4, 4, 4, 4, 4, 5, 6, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6}, - {2, 1, 1, 1, 2, 3, 4, 5, 2, 1, 0, 1, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6}, - {3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6}, - {4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6}, - {5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 1, 0, 1, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6}, - {6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 1, 0, 1, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 4, 4, 4, 4, 4, 6, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6}, - {7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 4, 4, 4, 4, 7, 6, 5, 5, 5, 5, 5, 5, 7, 6, 6, 6, 6, 6, 6, 6}, - {2, 2, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 4, 4, 4, 4, 4, 5, 6, 7, 5, 5, 5, 5, 5, 5, 6, 7}, - {2, 2, 2, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 1, 0, 1, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 4, 4, 4, 4, 4, 4, 5, 6, 5, 5, 5, 5, 5, 5, 5, 6}, - {2, 2, 2, 2, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 1, 0, 1, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5}, - {3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5}, - {4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5}, - {5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 1, 0, 1, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5}, - {6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 1, 0, 1, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 4, 4, 4, 4, 4, 6, 5, 5, 5, 5, 5, 5, 5}, - {7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 4, 4, 4, 4, 7, 6, 5, 5, 5, 5, 5, 5}, - {3, 3, 3, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 4, 4, 4, 4, 4, 5, 6, 7}, - {3, 3, 3, 3, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 1, 0, 1, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 4, 4, 4, 4, 4, 4, 5, 6}, - {3, 3, 3, 3, 3, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 1, 0, 1, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 4, 4, 4, 4, 4, 4, 4, 5}, - {3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4}, - {4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4}, - {5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 1, 0, 1, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 4, 4, 4, 4, 4, 4}, - {6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 1, 0, 1, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 4, 4, 4, 4, 4}, - {7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 4, 4, 4, 4}, - {4, 4, 4, 4, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7}, - {4, 4, 4, 4, 4, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 1, 0, 1, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6}, - {4, 4, 4, 4, 4, 4, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 1, 0, 1, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5}, - {4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3, 4}, - {4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 3, 3, 3, 3, 3, 3}, - {5, 4, 4, 4, 4, 4, 4, 4, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 1, 0, 1, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 3, 3, 3, 3, 3}, - {6, 5, 4, 4, 4, 4, 4, 4, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 1, 0, 1, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 3, 3, 3, 3}, - {7, 6, 5, 4, 4, 4, 4, 4, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 3, 3, 3}, - {5, 5, 5, 5, 5, 5, 6, 7, 4, 4, 4, 4, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7}, - {5, 5, 5, 5, 5, 5, 5, 6, 4, 4, 4, 4, 4, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 1, 0, 1, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6}, - {5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 1, 0, 1, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5}, - {5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4}, - {5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 2, 2, 2, 2, 3}, - {5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 1, 0, 1, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 2, 2, 2, 2}, - {6, 5, 5, 5, 5, 5, 5, 5, 6, 5, 4, 4, 4, 4, 4, 4, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 1, 0, 1, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 2, 2, 2}, - {7, 6, 5, 5, 5, 5, 5, 5, 7, 6, 5, 4, 4, 4, 4, 4, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 2, 2}, - {6, 6, 6, 6, 6, 6, 6, 7, 5, 5, 5, 5, 5, 5, 6, 7, 4, 4, 4, 4, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7}, - {6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 6, 4, 4, 4, 4, 4, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 1, 0, 1, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6}, - {6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 1, 0, 1, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5}, - {6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4}, - {6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3}, - {6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 1, 0, 1, 2, 5, 4, 3, 2, 1, 1, 1, 2}, - {6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 6, 5, 4, 4, 4, 4, 4, 4, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 1, 0, 1, 6, 5, 4, 3, 2, 1, 1, 1}, - {7, 6, 6, 6, 6, 6, 6, 6, 7, 6, 5, 5, 5, 5, 5, 5, 7, 6, 5, 4, 4, 4, 4, 4, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 1, 0, 7, 6, 5, 4, 3, 2, 1, 1}, - {7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 7, 5, 5, 5, 5, 5, 5, 6, 7, 4, 4, 4, 4, 4, 5, 6, 7, 3, 3, 3, 3, 4, 5, 6, 7, 2, 2, 2, 3, 4, 5, 6, 7, 1, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7}, - {7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 6, 4, 4, 4, 4, 4, 4, 5, 6, 3, 3, 3, 3, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 1, 1, 1, 2, 3, 4, 5, 6, 1, 0, 1, 2, 3, 4, 5, 6}, - {7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 5, 3, 3, 3, 3, 3, 3, 4, 5, 2, 2, 2, 2, 2, 3, 4, 5, 2, 1, 1, 1, 2, 3, 4, 5, 2, 1, 0, 1, 2, 3, 4, 5}, - {7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3, 4}, - {7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 4, 3, 2, 2, 2, 2, 2, 3, 4, 3, 2, 1, 1, 1, 2, 3, 4, 3, 2, 1, 0, 1, 2, 3}, - {7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 3, 2, 2, 2, 2, 2, 5, 4, 3, 2, 1, 1, 1, 2, 5, 4, 3, 2, 1, 0, 1, 2}, - {7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 6, 5, 4, 4, 4, 4, 4, 4, 6, 5, 4, 3, 3, 3, 3, 3, 6, 5, 4, 3, 2, 2, 2, 2, 6, 5, 4, 3, 2, 1, 1, 1, 6, 5, 4, 3, 2, 1, 0, 1}, - {7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 7, 6, 5, 5, 5, 5, 5, 5, 7, 6, 5, 4, 4, 4, 4, 4, 7, 6, 5, 4, 3, 3, 3, 3, 7, 6, 5, 4, 3, 2, 2, 2, 7, 6, 5, 4, 3, 2, 1, 1, 7, 6, 5, 4, 3, 2, 1, 0} -}; -inline int distance_to_corner(uint8_t sq){ - return std::min({chebyshev_distance[sq][0], // A1 - chebyshev_distance[sq][56], // A8 - chebyshev_distance[sq][7], // H1 - chebyshev_distance[sq][63]});// H8 -} -inline int distance_from_center(uint8_t sq){ - return std::min({chebyshev_distance[sq][34], // E4 - chebyshev_distance[sq][35], // D4 - chebyshev_distance[sq][42], // E5 - chebyshev_distance[sq][43]});// D5 -} -int mopUp(const chess::Board& pos) { - chess::Square whiteKingSq = pos.kingSq(chess::Color::WHITE); - chess::Square blackKingSq = pos.kingSq(chess::Color::BLACK); - - int whiteMaterial = wEval.Material; - int blackMaterial = bEval.Material; - int materialAdvantage = whiteMaterial - blackMaterial; - - // If neither side has a big material advantage, skip mop-up - if (std::abs(materialAdvantage) < 300) return 0; - - if (wEval.phase>64) return 0; - - // Distance to corner for both kings - int whiteCornerDist = distance_to_corner(whiteKingSq.index()); - int blackCornerDist = distance_to_corner(blackKingSq.index()); - - // Distance from center for both kings - int whiteCenterDist = distance_from_center(whiteKingSq.index()); - int blackCenterDist = distance_from_center(blackKingSq.index()); - - // White mop-up score (push black king to corner, bring white king to center) - int whiteMopUp = blackCornerDist * 4 - whiteCenterDist * 3; - - // Black mop-up score (push white king to corner, bring black king to center) - int blackMopUp = whiteCornerDist * 4 - blackCenterDist * 3; - - // Return net mop-up score favoring White (+ means White advantage) - return whiteMopUp - blackMopUp; -} - -// 3. Main evaluation -inline int16_t eg(const chess::Board &pos) -{ - if (draw(pos)) - return 0; - return space(pos) + tempo(pos) + material(pos) + passed(pos) + psqt_eval(pos) + mobility(pos) + king_safety(pos) + mopUp(pos); -} -template -inline int16_t mg(const chess::Board &pos) -{ - return material(pos) + space(pos) + tempo(pos) + mobility(pos) + king_safety(pos) + psqt_eval(pos) + pawn(pos); -} -struct CachedEvalEntry -{ - uint64_t key; // Full pawn hash key (Zobrist) - int16_t eval; -}; - -std::vector evalCache(1 << 17); -template -int16_t eval(const chess::Board &pos) -{ - auto hash = pos.hash(); - auto &entry = evalCache[hash & 131071]; - if constexpr (!trace) - if (entry.key == hash) - return entry.eval; - memset(&wEval, 0, sizeof(wEval)); - memset(&bEval, 0, sizeof(bEval)); - const int phase = ::phase(pos); - const int sign = pos.sideToMove() == chess::Color::WHITE ? 1 : -1; - - int mgScore = mg(pos); - int egScore = eg(pos); - int finalScore = ((mgScore * phase) + (egScore * (256 - phase))) / 256 * sign; - - return finalScore; -} -int16_t eval(const chess::Board &pos) -{ - return eval(pos); -} -void traceEvaluationResults(const char *label = nullptr, int16_t _eval = 0) -{ - if (label) - printf("\n[Trace: %s]\n", label); - - printf("+----------------+-------+-------+\n"); - printf("| Factor | White | Black |\n"); - printf("+----------------+-------+-------+\n"); - printf("| Phase | %13d |\n", wEval.phase); - printf("| Material | %5d | %5d |\n", wEval.Material, bEval.Material); - printf("| Doubled | %5d | %5d |\n", wEval.Doubled, bEval.Doubled); - printf("| Isolated | %5d | %5d |\n", wEval.Isolated, bEval.Isolated); - printf("| Backward | %5d | %5d |\n", wEval.Backward, bEval.Backward); - printf("| Passed | %5d | %5d |\n", wEval.Passed, bEval.Passed); - printf("| King tropism | %5d | %5d |\n", wEval.KingTropism, bEval.KingTropism); - printf("| Center control | %5d | %5d |\n", wEval.Center, bEval.Center); - printf("| Mobility | %5d | %5d |\n", wEval.Mobility, bEval.Mobility); - printf("| Shield | %5d | %5d |\n", wEval.Shield, bEval.Shield); - printf("| Space | %5d | %5d |\n", wEval.Space, bEval.Space); - printf("| MiddlegamePSQT | %5d | %5d |\n", wEval.MGPSQT, bEval.MGPSQT); - printf("| EndgamePSQT | %5d | %5d |\n", wEval.EGPSQT, bEval.EGPSQT); - printf("| Total | %13d |\n", _eval); - printf("+----------------+-------+-------+\n"); -} -void trace(const chess::Board &pos) { traceEvaluationResults("Handcrafted", eval(pos)); } -int16_t piece_value(chess::PieceType p) -{ - switch ((int)p) - { - case (int)chess::PieceType::PAWN: - return PawnValue; - case (int)chess::PieceType::KNIGHT: - return KnightValue; - case (int)chess::PieceType::BISHOP: - return BishopValue; - case (int)chess::PieceType::ROOK: - return RookValue; - case (int)chess::PieceType::QUEEN: - return QueenValue; - default: - return 0; - } -} diff --git a/eval.hpp b/eval.hpp deleted file mode 100644 index 08f98db..0000000 --- a/eval.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include "chess.hpp" -#include -#include - -constexpr int MAX_PLY = 64; -constexpr int MAX_MATE = 32'000; -constexpr int MAX_SCORE_CP = 31'000; -inline int16_t MATE(int i) { return MAX_MATE - i; } -inline int16_t MATE_DISTANCE(int16_t i) { return MAX_MATE - abs(i); } -int16_t eval(const chess::Board &board); -void trace(const chess::Board &board); -int16_t piece_value(chess::PieceType); -// some clarity for documentation -#define Evaluation [[nodiscard]] -#define AllPhases Evaluation -#define Middlegame Evaluation -#define Endgame Evaluation diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 1ec23e2..0000000 --- a/main.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "chess.hpp" -#include "search.hpp" -#include "ucioptions.hpp" -#include "uci.hpp" -#include -#include // for setbuf -int main() { - // Some quirks competition programming only for flushing buffers - setbuf(stdout, NULL); - UCIOptions::addSpin("Hash", 16, 1, 1024, [](const UCIOptions::Option& opt)->void { - search::tt.resize(std::get(opt.value)); - }); - uci_loop(); - return 0; -} diff --git a/movepick.cpp b/movepick.cpp deleted file mode 100644 index 74452c0..0000000 --- a/movepick.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "movepick.hpp" - -namespace movepick -{ - // 1. SEE - - // Return type changed to chess::Square for consistency - chess::Square select_least_valuable(chess::Board &board, chess::Bitboard bb) - { - if (bb.empty()) - return chess::Square::NO_SQ; // Use NO_SQ sentinel for no square - - int least_value = 5000; - chess::Square least_valuable_square = chess::Square::NO_SQ; - - while (bb) - { - int idx = bb.pop(); // idx is int index of bitboard square - chess::Square sq(idx); - int value = piece_value(board.at(sq)); - if (value < least_value) - { - least_value = value; - least_valuable_square = sq; - } - } - return least_valuable_square; - } - - int SEE(chess::Board &board, chess::Move move) - { - constexpr int max_gain_size = MAX_PLY + 1; // To avoid overflow in gain[d + 1] - std::array gain{}; - int d = 0; - chess::Color side = board.sideToMove(); - chess::Bitboard occ = board.occ(); - - chess::Square target_sq = move.to(); - - // Initial gain setup: - if (move.typeOf() & chess::Move::PROMOTION) - { - gain[0] = piece_value(move.promotionType()); - if (board.isCapture(move)) - gain[0] += piece_value(board.at(target_sq)); - } - else if (move.typeOf() == chess::Move::ENPASSANT) - { - // We'll treat it normally below, so just assign captured pawn value now: - gain[0] = piece_value(chess::PieceType::PAWN); - } - else - { - gain[0] = piece_value(board.at(target_sq)); - } - - chess::Square from_sq = move.from(); - - // Remove the moving piece from occupancy: - occ.clear(from_sq.index()); - - // Special case for en passant: remove the captured pawn square here: - if (move.typeOf() == chess::Move::ENPASSANT) - { - chess::Square ep_captured_sq(target_sq.file(), from_sq.rank()); // square behind target - occ.clear(ep_captured_sq.index()); - } - - chess::Square next_attacker_sq = from_sq; - - while (++d < MAX_PLY) - { - side = ~side; - - // Find all attackers of target square from side to move: - chess::Bitboard attackers = chess::attacks::attackers(board, side, target_sq) & occ; - - // Include x-ray attacks for sliding pieces: - chess::Bitboard rook_attacks = chess::attacks::rook(target_sq, occ); - chess::Bitboard bishop_attacks = chess::attacks::bishop(target_sq, occ); - - attackers |= rook_attacks & (board.pieces(chess::PieceType::ROOK, side) | board.pieces(chess::PieceType::QUEEN, side)); - attackers |= bishop_attacks & (board.pieces(chess::PieceType::BISHOP, side) | board.pieces(chess::PieceType::QUEEN, side)); - - if (attackers.empty()) - break; - - next_attacker_sq = select_least_valuable(board, attackers); - if (next_attacker_sq == chess::Square::NO_SQ) - break; - - int captured_value = piece_value(board.at(next_attacker_sq)); - gain[d] = -gain[d - 1] + captured_value; - - if (std::max(-gain[d - 1], gain[d]) < 0) - break; - - occ.clear(next_attacker_sq.index()); - } - - while (--d > 0) - { - gain[d] = std::max(-gain[d + 1], gain[d]); - } - - return gain[0]; - } - - // 2. Killer moves - chess::Move killerMoves[MAX_PLY][2] = {}; - - void updateKillerMoves(chess::Move m, int ply) - { - if (killerMoves[ply][0] != m) - { - killerMoves[ply][1] = killerMoves[ply][0]; - killerMoves[ply][0] = m; - } - } - - // 3. History heuristic - int historyHeuristic[64][64] = {}; // from-square to to-square - - void updateHistoryHeuristic(chess::Move m, int depth) - { - historyHeuristic[m.from().index()][m.to().index()] += depth * depth; - } - - void orderMoves(chess::Board &board, chess::Movelist &moves, chess::Move ttMove, chess::Move pvMove, int ply) - { - for (auto &move : moves) - { - if (move == ttMove) - move.setScore(10000); - else if (move == pvMove) - move.setScore(9000); - else if (board.isCapture(move)) - move.setScore(SEE(board, move)); - else if (move == killerMoves[ply][0]) - move.setScore(8500); - else if (move == killerMoves[ply][1]) - move.setScore(8000); - else - move.setScore(historyHeuristic[move.from().index()][move.to().index()]); - } - // Sort moves by score in descending order - std::stable_sort(moves.begin(), moves.end(), - [](const chess::Move &a, const chess::Move &b) - { return a.score() > b.score(); }); - } - - void qOrderMoves(chess::Board &board, chess::Movelist &moves) - { - // clang-format off - int16_t promotion_table[4][8] = { - {-58, -38, -13, -28, -31, -27, -63, -99}, // N - {-14, -21, -11, -8, -7, -9, -17, -24}, // B - { 13, 10, 18, 15, 12, 12, 8, 5}, // R - { -9, 22, 22, 27, 27, 19, 10, 20}, // Q - }; - // clang-format on - - for (auto &move : moves) - { - if (board.isCapture(move)) // Simple MVV-LVA - { - move.setScore(SEE(board, move)); - } - else if (move.typeOf() & chess::Move::PROMOTION) - { - int promoValue = piece_value(move.promotionType()); - int captureValue = board.isCapture(move) ? piece_value(board.at(move.to())) : 0; - move.setScore(promoValue + captureValue + promotion_table[move.promotionType() - 1][move.to().file()]); - } - else - { - move.setScore(historyHeuristic[move.from().index()][move.to().index()]); - } - } - // Sort moves by score in descending order - std::stable_sort(moves.begin(), moves.end(), - [](const chess::Move &a, const chess::Move &b) - { return a.score() > b.score(); }); - } -} diff --git a/movepick.hpp b/movepick.hpp deleted file mode 100644 index ce46757..0000000 --- a/movepick.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include "chess.hpp" -#include "eval.hpp" -namespace movepick -{ - // Static Exchange Evaluation - evaluates material exchange on a square - int SEE(chess::Board &, chess::Move); - // Updates killer moves table for move ordering - void updateKillerMoves(chess::Move, int); - // Updates history heuristic table for move ordering - void updateHistoryHeuristic(chess::Move, int); - // Orders moves for regular search (with TT move, killers at given ply) - void orderMoves(chess::Board &, chess::Movelist &, chess::Move, chess::Move, int); - // Orders moves for quiescence search (captures/promotions) - void qOrderMoves(chess::Board &, chess::Movelist &); - // Killer moves table: [ply][slot] for move ordering heuristic - extern chess::Move killerMoves[MAX_PLY][2]; -} \ No newline at end of file diff --git a/scripts/.gitattributes b/scripts/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/scripts/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/scripts/get_native_properties.sh b/scripts/get_native_properties.sh new file mode 100755 index 0000000..773d6c2 --- /dev/null +++ b/scripts/get_native_properties.sh @@ -0,0 +1,159 @@ +#!/bin/sh + +# +# Returns properties of the native system. +# best architecture as supported by the CPU +# filename of the best binary uploaded as an artifact during CI +# + +# Check if all the given flags are present in the CPU flags list +check_flags() { + for flag; do + printf '%s\n' "$flags" | grep -q -w "$flag" || return 1 + done +} + +# Set the CPU flags list +# remove underscores and points from flags, e.g. gcc uses avx512vnni, while some cpuinfo can have avx512_vnni, some systems use sse4_1 others sse4.1 +get_flags() { + flags=$(awk '/^flags[ \t]*:|^Features[ \t]*:/{gsub(/^flags[ \t]*:[ \t]*|^Features[ \t]*:[ \t]*|[_.]/, ""); line=$0} END{print line}' /proc/cpuinfo) +} + +# Check for gcc march "znver1" or "znver2" https://en.wikichip.org/wiki/amd/cpuid +check_znver_1_2() { + vendor_id=$(awk '/^vendor_id/{print $3; exit}' /proc/cpuinfo) + cpu_family=$(awk '/^cpu family/{print $4; exit}' /proc/cpuinfo) + [ "$vendor_id" = "AuthenticAMD" ] && [ "$cpu_family" = "23" ] && znver_1_2=true +} + +# Set the file CPU loongarch64 architecture +set_arch_loongarch64() { + if check_flags 'lasx'; then + true_arch='loongarch64-lasx' + elif check_flags 'lsx'; then + true_arch='lonngarch64-lsx' + else + true_arch='loongarch64' + fi +} + +# Set the file CPU x86_64 architecture +set_arch_x86_64() { + if check_flags 'avx512vnni' 'avx512dq' 'avx512f' 'avx512bw' 'avx512vl'; then + true_arch='x86-64-vnni256' + elif check_flags 'avx512f' 'avx512bw'; then + true_arch='x86-64-avx512' + elif [ -z "${znver_1_2+1}" ] && check_flags 'bmi2'; then + true_arch='x86-64-bmi2' + elif check_flags 'avx2'; then + true_arch='x86-64-avx2' + elif check_flags 'sse41' && check_flags 'popcnt'; then + true_arch='x86-64-sse41-popcnt' + else + true_arch='x86-64' + fi +} + +set_arch_ppc_64() { + if $(grep -q -w "altivec" /proc/cpuinfo); then + power=$(grep -oP -m 1 'cpu\t+: POWER\K\d+' /proc/cpuinfo) + if [ "0$power" -gt 7 ]; then + # VSX started with POWER8 + true_arch='ppc-64-vsx' + else + true_arch='ppc-64-altivec' + fi + else + true_arch='ppc-64' + fi +} + +# Check the system type +uname_s=$(uname -s) +uname_m=$(uname -m) +case $uname_s in + 'Darwin') # Mac OSX system + case $uname_m in + 'arm64') + true_arch='apple-silicon' + file_arch='m1-apple-silicon' + ;; + 'x86_64') + flags=$(sysctl -n machdep.cpu.features machdep.cpu.leaf7_features | tr '\n' ' ' | tr '[:upper:]' '[:lower:]' | tr -d '_.') + set_arch_x86_64 + if [ "$true_arch" = 'x86-64-vnni256' ] || [ "$true_arch" = 'x86-64-avx512' ]; then + file_arch='x86-64-bmi2' + fi + ;; + esac + file_os='macos' + file_ext='tar' + ;; + 'Linux') # Linux system + get_flags + case $uname_m in + 'x86_64') + file_os='ubuntu' + check_znver_1_2 + set_arch_x86_64 + ;; + 'i686') + file_os='ubuntu' + true_arch='x86-32' + ;; + 'ppc64'*) + file_os='ubuntu' + set_arch_ppc_64 + ;; + 'aarch64') + file_os='android' + true_arch='armv8' + if check_flags 'asimddp'; then + true_arch="$true_arch-dotprod" + fi + ;; + 'armv7'*) + file_os='android' + true_arch='armv7' + if check_flags 'neon'; then + true_arch="$true_arch-neon" + fi + ;; + 'loongarch64'*) + file_os='linux' + set_arch_loongarch64 + ;; + *) # Unsupported machine type, exit with error + printf 'Unsupported machine type: %s\n' "$uname_m" + exit 1 + ;; + esac + file_ext='tar' + ;; + 'MINGW'*'ARM64'*) # Windows ARM64 system with POSIX compatibility layer + # TODO: older chips might be armv8, but we have no good way to detect, /proc/cpuinfo shows x86 info + file_os='windows' + true_arch='armv8-dotprod' + file_ext='zip' + ;; + 'CYGWIN'*|'MINGW'*|'MSYS'*) # Windows x86_64system with POSIX compatibility layer + get_flags + check_znver_1_2 + set_arch_x86_64 + file_os='windows' + file_ext='zip' + ;; + *) + # Unknown system type, exit with error + printf 'Unsupported system type: %s\n' "$uname_s" + exit 1 + ;; +esac + +if [ -z "$file_arch" ]; then + file_arch=$true_arch +fi + +file_name="stockfish-$file_os-$file_arch.$file_ext" + +printf '%s %s\n' "$true_arch" "$file_name" diff --git a/scripts/net.sh b/scripts/net.sh new file mode 100755 index 0000000..1aa1fbf --- /dev/null +++ b/scripts/net.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +wget_or_curl=$( (command -v wget > /dev/null 2>&1 && echo "wget -qO-") || \ + (command -v curl > /dev/null 2>&1 && echo "curl -skL")) + + +sha256sum=$( (command -v shasum > /dev/null 2>&1 && echo "shasum -a 256") || \ + (command -v sha256sum > /dev/null 2>&1 && echo "sha256sum")) + +if [ -z "$sha256sum" ]; then + >&2 echo "sha256sum not found, NNUE files will be assumed valid." +fi + +get_nnue_filename() { + grep "$1" evaluate.h | grep "#define" | sed "s/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/" +} + +validate_network() { + # If no sha256sum command is available, assume the file is always valid. + if [ -n "$sha256sum" ] && [ -f "$1" ]; then + if [ "$1" != "nn-$($sha256sum "$1" | cut -c 1-12).nnue" ]; then + rm -f "$1" + return 1 + fi + fi +} + +fetch_network() { + _filename="$(get_nnue_filename "$1")" + + if [ -z "$_filename" ]; then + >&2 echo "NNUE file name not found for: $1" + return 1 + fi + + if [ -f "$_filename" ]; then + if validate_network "$_filename"; then + echo "Existing $_filename validated, skipping download" + return + else + echo "Removing invalid NNUE file: $_filename" + fi + fi + + if [ -z "$wget_or_curl" ]; then + >&2 printf "%s\n" "Neither wget or curl is installed." \ + "Install one of these tools to download NNUE files automatically." + exit 1 + fi + + for url in \ + "https://tests.stockfishchess.org/api/nn/$_filename" \ + "https://github.com/official-stockfish/networks/raw/master/$_filename"; do + echo "Downloading from $url ..." + if $wget_or_curl "$url" > "$_filename"; then + if validate_network "$_filename"; then + echo "Successfully validated $_filename" + else + echo "Downloaded $_filename is invalid" + continue + fi + else + echo "Failed to download from $url" + fi + if [ -f "$_filename" ]; then + return + fi + done + + # Download was not successful in the loop, return false. + >&2 echo "Failed to download $_filename" + return 1 +} + +fetch_network EvalFileDefaultNameBig && \ +fetch_network EvalFileDefaultNameSmall diff --git a/search.cpp b/search.cpp deleted file mode 100644 index 7ca18ff..0000000 --- a/search.cpp +++ /dev/null @@ -1,355 +0,0 @@ -#include "search.hpp" -#include "timeman.hpp" -#include -#include -#include -#include -#include - -namespace search -{ - struct PV - { - int8_t cmove; - chess::Move pv[MAX_PLY]; - }; - - constexpr int LMR_BONUS = 2; - std::atomic stop_requested(false); - PV stablePV; - TranspositionTable tt; - int seldepth = 0; - - struct SearchAbortException : public std::exception - { - SearchAbortException() = default; - }; - - inline bool isMateScore(int16_t score) - { - return std::abs(score) > MAX_SCORE_CP; - } - - int16_t adjustMateScore(int16_t score, int ply) - { - return isMateScore(score) ? (score > 0 ? score - ply : score + ply) : score; - } - - int16_t restoreMateScore(int16_t score, int ply) - { - return isMateScore(score) ? (score > 0 ? score + ply : score - ply) : score; - } - - int16_t quiescence(chess::Board &board, int16_t alpha, int16_t beta, int ply) - { - seldepth = std::max(seldepth, ply); - if (timeman::check_time() || stop_requested.load(std::memory_order_relaxed)) - throw SearchAbortException(); - - ++nodes; - - int16_t stand_pat = eval(board); - if (stand_pat >= beta) - return beta; - if (alpha < stand_pat) - alpha = stand_pat; - - chess::Movelist moves; - chess::movegen::legalmoves(moves, board); - if (moves.empty()) - return board.inCheck() ? -MATE(ply) : 0; - - chess::Movelist tacticalMoves; - for (auto mv : moves) - { - if (board.isCapture(mv) || (mv.typeOf() & chess::Move::PROMOTION) || board.givesCheck(mv) != chess::CheckType::NO_CHECK) - tacticalMoves.add(mv); - } - - movepick::qOrderMoves(board, tacticalMoves); - for (auto mv : tacticalMoves) - { - // --- BEGIN SEE FILTER --- - int gain = mv.score(); // Or use precomputed SEE - if (board.isCapture(mv) && gain < 0) - continue; - // --- END SEE FILTER --- - - // --- BEGIN DELTA PRUNING --- - int margin = 50; - if (stand_pat + gain + margin < alpha) - continue; - // --- END DELTA PRUNING --- - - board.makeMove(mv); - int16_t score = -quiescence(board, -beta, -alpha, ply + 1); - board.unmakeMove(mv); - - if (score >= beta) - return beta; - if (score > alpha) - alpha = score; - } - return alpha; - } - - int16_t alpha_beta(chess::Board &board, int depth, int16_t alpha, int16_t beta, int ply, PV *pv, int ext = 0) - { - pv->cmove = 0; - if (timeman::check_time() || stop_requested.load(std::memory_order_relaxed)) - throw SearchAbortException(); - - uint64_t h = board.hash(); - TTEntry *entry = tt.lookup(h); - chess::Move hash_move = entry ? entry->bestMove : chess::Move::NULL_MOVE; - - if (entry && entry->hash == h && entry->depth() >= depth) - { - int16_t score = adjustMateScore(entry->score(), ply); - if (entry->flag() == TTFlag::LOWERBOUND && score >= beta) - return score; - if (entry->flag() == TTFlag::UPPERBOUND && score <= alpha) - return score; - if (entry->flag() == TTFlag::EXACT) - return score; - } - - chess::Movelist moves; - chess::movegen::legalmoves(moves, board); - if (moves.empty()) - return board.inCheck() ? -MATE(ply) : 0; - - if (depth <= 0){ - ++nodes; - return quiescence(board, alpha, beta, ply); - } - - bool in_check = board.inCheck(); - int16_t evalScore = eval(board); - - // --- BEGIN RAZORING --- - if (depth <= 2 && !in_check) - { - if (evalScore + 150 < alpha) - return quiescence(board, alpha, beta, ply); - } - // --- END RAZORING --- - - bool can_do_null = (!in_check) && (depth >= 3); - if (can_do_null) - { - board.makeNullMove(); - int R = depth / 6; - int16_t score = -alpha_beta(board, depth - 1 - R, -beta, -beta + 1, ply + 1, pv); - board.unmakeNullMove(); - if (score >= beta) - { - tt.store(h, chess::Move::NULL_MOVE, restoreMateScore(score, ply), depth, TTFlag::LOWERBOUND); - return score; - } - } - - bool has_hash = (hash_move != chess::Move::NULL_MOVE); - if (!has_hash && depth >= 4) - { - PV temp; - temp.cmove = 0; - alpha_beta(board, depth - 2, alpha, beta, ply, &temp); - TTEntry *iid = tt.lookup(board.hash()); - if (iid && iid->bestMove != chess::Move::NULL_MOVE) - hash_move = iid->bestMove; - } - - movepick::orderMoves(board, moves, hash_move, stablePV.pv[0], ply); - - chess::Move best_move = chess::Move(); - int16_t best_score = -MATE(0); - - for (int i = 0; i < moves.size(); ++i) - { - chess::Move mv = moves[i]; - PV sub_pv; - sub_pv.cmove = 0; - - bool isCap = board.isCapture(mv); - bool do_lmr = (i > 0) && !isCap && !in_check; - bool givesCheck = board.givesCheck(mv) != chess::CheckType::NO_CHECK; - bool isPromo = mv.typeOf() & chess::Move::PROMOTION; - - // --- BEGIN FUTILITY PRUNING --- - if (depth <= 3 && !in_check && !isCap && !isPromo && !givesCheck) - { - const int margin = 100 * depth; - if (evalScore + margin <= alpha) - continue; - } - // --- END FUTILITY PRUNING --- - - // --- BEGIN EXTENSIONS --- - int extension = 0; - if (givesCheck) - extension = 1; - - if (board.at(mv.from()) == chess::PieceType::PAWN && (mv.to().rank() >= chess::Rank::RANK_6)) - { - int rank = board.sideToMove() == chess::Color::WHITE ? (int)mv.to().rank() : 7 - mv.to().rank(); - if (rank >= 5) - extension += 1; - } - - if (moves.size() == 1) - extension += 1; - - if (ext + extension > 16) - extension = 0; - - int new_depth = depth - 1 + extension; - // --- END EXTENSIONS --- - - board.makeMove(mv); - int16_t score; - - if (do_lmr && !extension && depth >= 3) - { - score = -alpha_beta(board, new_depth - LMR_BONUS, -alpha - 1, -alpha, ply + 1, &sub_pv, ext); - if (score > alpha) - score = -alpha_beta(board, new_depth, -beta, -alpha, ply + 1, &sub_pv, ext); - } - else - { - if (i == 0) - score = -alpha_beta(board, new_depth, -beta, -alpha, ply + 1, &sub_pv, ext + extension); - else - { - score = -alpha_beta(board, new_depth, -alpha - 1, -alpha, ply + 1, &sub_pv, ext + extension); - if (score > alpha) - score = -alpha_beta(board, new_depth, -beta, -alpha, ply + 1, &sub_pv, ext + extension); - } - } - - board.unmakeMove(mv); - - if (score > best_score) - { - best_score = score; - best_move = mv; - if (score > alpha) - { - alpha = score; - pv->pv[0] = mv; - std::memcpy(&pv->pv[1], sub_pv.pv, sizeof(chess::Move) * sub_pv.cmove); - pv->cmove = sub_pv.cmove + 1; - } - } - - if (score >= beta) - { - if (!isCap) - { - movepick::updateKillerMoves(mv, ply); - movepick::updateHistoryHeuristic(mv, depth); - } - break; - } - } - - if (best_score != -MATE(0)) - { - TTFlag flag = (best_score >= beta) ? TTFlag::LOWERBOUND - : (best_score > alpha) ? TTFlag::EXACT - : TTFlag::UPPERBOUND; - tt.store(h, best_move, restoreMateScore(best_score, ply), depth, flag); - } - - return best_score; - } - - void run_search(const chess::Board &board, const TimeControl &tc) - { - nodes = 0, seldepth = 0; - stop_requested.store(false, std::memory_order_relaxed); - timeman::setLimits(tc); - - PV child_pv; - child_pv.cmove = 0; - stablePV.cmove = 0; - - chess::Movelist rootMoves; - chess::movegen::legalmoves(rootMoves, board); - - int16_t last_score = 0; - int max_iter = std::min(tc.infinite ? MAX_PLY : tc.depth, (int)MAX_PLY); - - for (int current_depth = 1; current_depth <= max_iter; ++current_depth) - { - chess::Board _board = board; - child_pv.cmove = 0; - int16_t alpha = last_score - 30; - int16_t beta = last_score + 30; - - try - { - last_score = alpha_beta(_board, current_depth, alpha, beta, 0, &child_pv); - - if (last_score <= alpha || last_score >= beta) - { - alpha = -MATE(0); - beta = MATE(0); - last_score = alpha_beta(_board, current_depth, alpha, beta, 0, &child_pv); - } - - stablePV = child_pv; - auto elapsed = std::chrono::duration_cast( - std::chrono::high_resolution_clock::now() - timeman::start_time) - .count(); - - if (isMateScore(last_score)){ - // Confusement: break - printf("info depth %d seldepth %d nodes %" PRIu64 " nps %" PRIu64 " time %u score mate %d pv ", - current_depth, - seldepth, - nodes.load(), - (elapsed == 0 ? 0 : nodes.load() / elapsed * 1000), - elapsed, - last_score > 0 ? (MAX_MATE - last_score) : -(MAX_MATE + last_score)); - } - else - printf("info depth %d seldepth %d nodes %" PRIu64 " nps %" PRIu64 " time %u score cp %d pv ", - current_depth, - seldepth, - nodes.load(), - (elapsed == 0 ? 0 : nodes.load() / elapsed * 1000), - elapsed, - last_score); - - for (int j = 0; j < child_pv.cmove; ++j) - printf("%s ", chess::uci::moveToUci(child_pv.pv[j], board.chess960()).c_str()); - printf("\n"); - - // [DEBUG] - if (isMateScore(last_score)) break; - if (timeman::check_time()) - break; - } - catch (SearchAbortException &) - { - break; - } - } - - if (stablePV.cmove > 0) - { - printf("bestmove %s\n", - chess::uci::moveToUci(stablePV.pv[0], board.chess960()).c_str()); - } - else if (!rootMoves.empty()) - { - printf("bestmove %s\n", - chess::uci::moveToUci(rootMoves[0], board.chess960()).c_str()); - } - else - { - printf("bestmove 0000\n"); - } - } -} diff --git a/search.hpp b/search.hpp deleted file mode 100644 index 0733daf..0000000 --- a/search.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "chess.hpp" -#include "timeman.hpp" -#include "tt.hpp" -#include "movepick.hpp" -#include "eval.hpp" -#include "ucioptions.hpp" -#include -#include -#include -namespace search -{ - inline std::atomic nodes{0}; - void run_search(const chess::Board& board, const TimeControl& tc); - extern std::atomic stop_requested; - extern TranspositionTable tt; -} diff --git a/src/.depend b/src/.depend new file mode 100644 index 0000000..2dfa530 --- /dev/null +++ b/src/.depend @@ -0,0 +1,105 @@ +half_ka_v2_hm.o: nnue/features/half_ka_v2_hm.cpp \ + nnue/features/half_ka_v2_hm.h nnue/features/../../misc.h \ + nnue/features/../../types.h nnue/features/../nnue_common.h \ + nnue/features/../../misc.h nnue/features/../../bitboard.h \ + nnue/features/../../types.h nnue/features/../../position.h \ + nnue/features/../../chess.hpp \ + nnue/features/../../nnue/nnue_accumulator.h \ + nnue/features/../../nnue/../types.h \ + nnue/features/../../nnue/nnue_architecture.h \ + nnue/features/../../nnue/features/half_ka_v2_hm.h \ + nnue/features/../../nnue/layers/affine_transform.h \ + nnue/features/../../nnue/layers/../nnue_common.h \ + nnue/features/../../nnue/layers/../simd.h \ + nnue/features/../../nnue/layers/../../types.h \ + nnue/features/../../nnue/layers/../nnue_common.h \ + nnue/features/../../nnue/layers/affine_transform_sparse_input.h \ + nnue/features/../../nnue/layers/../../bitboard.h \ + nnue/features/../../nnue/layers/clipped_relu.h \ + nnue/features/../../nnue/layers/sqr_clipped_relu.h \ + nnue/features/../../nnue/nnue_common.h +evaluate.o: evaluate.cpp evaluate.h types.h nnue/network.h \ + nnue/../memory.h nnue/../types.h nnue/nnue_accumulator.h \ + nnue/nnue_architecture.h nnue/features/half_ka_v2_hm.h \ + nnue/features/../../misc.h nnue/features/../../types.h \ + nnue/features/../nnue_common.h nnue/features/../../misc.h \ + nnue/layers/affine_transform.h nnue/layers/../nnue_common.h \ + nnue/layers/../simd.h nnue/layers/../../types.h \ + nnue/layers/../nnue_common.h nnue/layers/affine_transform_sparse_input.h \ + nnue/layers/../../bitboard.h nnue/layers/../../types.h \ + nnue/layers/clipped_relu.h nnue/layers/sqr_clipped_relu.h \ + nnue/nnue_common.h nnue/nnue_feature_transformer.h nnue/../position.h \ + nnue/../chess.hpp nnue/../types.h nnue/../nnue/nnue_accumulator.h \ + nnue/simd.h nnue/nnue_misc.h nnue/nnue_misc.h position.h uci.hpp \ + chess.hpp search.h movepick.hpp timeman.hpp tt.hpp memory.h \ + ucioptions.hpp nnue/nnue_accumulator.h +main.o: main.cpp search.h evaluate.h types.h movepick.hpp chess.hpp \ + position.h nnue/nnue_accumulator.h nnue/../types.h \ + nnue/nnue_architecture.h nnue/features/half_ka_v2_hm.h \ + nnue/features/../../misc.h nnue/features/../../types.h \ + nnue/features/../nnue_common.h nnue/features/../../misc.h \ + nnue/layers/affine_transform.h nnue/layers/../nnue_common.h \ + nnue/layers/../simd.h nnue/layers/../../types.h \ + nnue/layers/../nnue_common.h nnue/layers/affine_transform_sparse_input.h \ + nnue/layers/../../bitboard.h nnue/layers/../../types.h \ + nnue/layers/clipped_relu.h nnue/layers/sqr_clipped_relu.h \ + nnue/nnue_common.h timeman.hpp tt.hpp memory.h nnue/network.h \ + nnue/../memory.h nnue/nnue_accumulator.h nnue/nnue_feature_transformer.h \ + nnue/../position.h nnue/simd.h nnue/nnue_misc.h uci.hpp ucioptions.hpp +search.o: search.cpp search.h evaluate.h types.h movepick.hpp chess.hpp \ + position.h nnue/nnue_accumulator.h nnue/../types.h \ + nnue/nnue_architecture.h nnue/features/half_ka_v2_hm.h \ + nnue/features/../../misc.h nnue/features/../../types.h \ + nnue/features/../nnue_common.h nnue/features/../../misc.h \ + nnue/layers/affine_transform.h nnue/layers/../nnue_common.h \ + nnue/layers/../simd.h nnue/layers/../../types.h \ + nnue/layers/../nnue_common.h nnue/layers/affine_transform_sparse_input.h \ + nnue/layers/../../bitboard.h nnue/layers/../../types.h \ + nnue/layers/clipped_relu.h nnue/layers/sqr_clipped_relu.h \ + nnue/nnue_common.h timeman.hpp tt.hpp memory.h nnue/network.h \ + nnue/../memory.h nnue/nnue_accumulator.h nnue/nnue_feature_transformer.h \ + nnue/../position.h nnue/simd.h nnue/nnue_misc.h +timeman.o: timeman.cpp timeman.hpp types.h +tt.o: tt.cpp tt.hpp chess.hpp types.h memory.h +uci.o: uci.cpp uci.hpp chess.hpp search.h evaluate.h types.h movepick.hpp \ + position.h nnue/nnue_accumulator.h nnue/../types.h \ + nnue/nnue_architecture.h nnue/features/half_ka_v2_hm.h \ + nnue/features/../../misc.h nnue/features/../../types.h \ + nnue/features/../nnue_common.h nnue/features/../../misc.h \ + nnue/layers/affine_transform.h nnue/layers/../nnue_common.h \ + nnue/layers/../simd.h nnue/layers/../../types.h \ + nnue/layers/../nnue_common.h nnue/layers/affine_transform_sparse_input.h \ + nnue/layers/../../bitboard.h nnue/layers/../../types.h \ + nnue/layers/clipped_relu.h nnue/layers/sqr_clipped_relu.h \ + nnue/nnue_common.h timeman.hpp tt.hpp memory.h nnue/network.h \ + nnue/../memory.h nnue/nnue_accumulator.h nnue/nnue_feature_transformer.h \ + nnue/../position.h nnue/simd.h nnue/nnue_misc.h ucioptions.hpp +ucioptions.o: ucioptions.cpp ucioptions.hpp +nnue_accumulator.o: nnue/nnue_accumulator.cpp nnue/nnue_accumulator.h \ + nnue/../types.h nnue/nnue_architecture.h nnue/features/half_ka_v2_hm.h \ + nnue/features/../../misc.h nnue/features/../../types.h \ + nnue/features/../nnue_common.h nnue/features/../../misc.h \ + nnue/layers/affine_transform.h nnue/layers/../nnue_common.h \ + nnue/layers/../simd.h nnue/layers/../../types.h \ + nnue/layers/../nnue_common.h nnue/layers/affine_transform_sparse_input.h \ + nnue/layers/../../bitboard.h nnue/layers/../../types.h \ + nnue/layers/clipped_relu.h nnue/layers/sqr_clipped_relu.h \ + nnue/nnue_common.h nnue/../bitboard.h nnue/../misc.h nnue/../position.h \ + nnue/../chess.hpp nnue/../types.h nnue/../nnue/nnue_accumulator.h \ + nnue/nnue_feature_transformer.h nnue/simd.h +network.o: nnue/network.cpp nnue/network.h nnue/../memory.h \ + nnue/../types.h nnue/nnue_accumulator.h nnue/nnue_architecture.h \ + nnue/features/half_ka_v2_hm.h nnue/features/../../misc.h \ + nnue/features/../../types.h nnue/features/../nnue_common.h \ + nnue/features/../../misc.h nnue/layers/affine_transform.h \ + nnue/layers/../nnue_common.h nnue/layers/../simd.h \ + nnue/layers/../../types.h nnue/layers/../nnue_common.h \ + nnue/layers/affine_transform_sparse_input.h nnue/layers/../../bitboard.h \ + nnue/layers/../../types.h nnue/layers/clipped_relu.h \ + nnue/layers/sqr_clipped_relu.h nnue/nnue_common.h \ + nnue/nnue_feature_transformer.h nnue/../position.h nnue/../chess.hpp \ + nnue/../types.h nnue/../nnue/nnue_accumulator.h nnue/simd.h \ + nnue/nnue_misc.h nnue/../incbin/incbin.h nnue/../evaluate.h \ + nnue/../misc.h +memory.o: memory.cpp memory.h +movepick.o: movepick.cpp movepick.hpp chess.hpp types.h diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..4e18577 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,1128 @@ +#makefile of stockfish, but for cppchess_engine + +#Stockfish, a UCI chess playing engine derived from Glaurung 2.1 +#Copyright(C) 2004 - 2025 The Stockfish developers(see AUTHORS file) +# +#Stockfish is free software : you can redistribute it and / or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#Stockfish is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the +#GNU General Public License for more details. +# +#You should have received a copy of the GNU General Public License +#along with this program.If not, see < http: //www.gnu.org/licenses/>. + + +### ========================================================================== +### Section 1. General Configuration +### ========================================================================== + +### Establish the operating system name +KERNEL := $(shell uname -s) +ifeq ($(KERNEL),Linux) + OS := $(shell uname -o) +endif + +### Target Windows OS +ifeq ($(OS),Windows_NT) + ifneq ($(COMP),ndk) + target_windows = yes + endif +else ifeq ($(COMP),mingw) + target_windows = yes + ifeq ($(WINE_PATH),) + WINE_PATH := $(shell which wine) + endif +endif + +### Executable name +ifeq ($(target_windows),yes) + EXE = cppchess_engine.exe +else + EXE = cppchess_engine +endif + +### Installation dir definitions +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin + +### Built-in benchmark for pgo-builds +PGOBENCH = $(WINE_PATH) ./$(EXE) bench + +### Source and object files +SRCS = nnue/features/half_ka_v2_hm.cpp evaluate.cpp \ + main.cpp search.cpp timeman.cpp tt.cpp uci.cpp \ + ucioptions.cpp nnue/nnue_accumulator.cpp nnue/network.cpp \ + memory.cpp movepick.cpp + +HEADERS = evaluate.h types.h movepick.hpp nnue/features/half_ka_v2_hm.h misc.h \ + nnue/layers/affine_transform.h nnue/layers/affine_transform_sparse_input.h nnue/layers/clipped_relu.h \ + nnue/layers/sqr_clipped_relu.h nnue/nnue_accumulator.h nnue/nnue_architecture.h \ + nnue/nnue_common.h nnue/nnue_feature_transformer.h nnue/simd.h position.h search.h \ + timeman.hpp tt.hpp types.h uci.hpp ucioptions.hpp nnue/network.h memory.h + +OBJS = $(SRCS:.cpp=.o) + +### ========================================================================== +### Section 2. High-level Configuration +### ========================================================================== +# +#flag-- - Comp switch -- -Description +#-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# +#debug = yes / no-- - -DNDEBUG-- - Enable / Disable debug mode +#sanitize = none / < sanitizer>...(-fsanitize) +#-- -(undefined) -- - enable undefined behavior checks +#-- -(thread) -- - enable threading error checks +#-- -(address) -- - enable memory access checks +#-- -... etc... -- - see compiler documentation for supported sanitizers +#optimize = yes / no-- - (-O3 / -fast etc.)-- - Enable / Disable optimizations +#arch = (name)-- - (-arch)-- - Target architecture +#bits = 64 / 32 -- - -DIS_64BIT-- - 64 - / 32 - bit operating system +#prefetch = yes / no-- - -DUSE_PREFETCH-- - Use prefetch asm - instruction +#popcnt = yes / no-- - -DUSE_POPCNT-- - Use popcnt asm - instruction +#pext = yes / no-- - -DUSE_PEXT-- - Use pext x86_64 asm - instruction +#sse = yes / no-- - -msse-- - Use Intel Streaming SIMD Extensions +#mmx = yes / no-- - -mmmx-- - Use Intel MMX instructions +#sse2 = yes / no-- - -msse2-- - Use Intel Streaming SIMD Extensions 2 +#ssse3 = yes / no-- - -mssse3-- - Use Intel Supplemental Streaming SIMD Extensions 3 +#sse41 = yes / no-- - -msse4.1 -- -Use Intel Streaming SIMD Extensions 4.1 +#avx2 = yes / no-- - -mavx2-- - Use Intel Advanced Vector Extensions 2 +#avxvnni = yes / no-- - -mavxvnni-- - Use Intel Vector Neural Network Instructions AVX +#avx512 = yes / no-- - -mavx512bw-- - Use Intel Advanced Vector Extensions 512 +#vnni256 = yes / no-- - -mavx256vnni-- \ + - Use Intel Vector Neural Network Instructions 512 with 256bit operands +#vnni512 = yes / no-- - -mavx512vnni-- - Use Intel Vector Neural Network Instructions 512 +#altivec = yes / no-- - -maltivec-- - Use PowerPC Altivec SIMD extension +#vsx = yes / no-- - -mvsx-- - Use POWER VSX SIMD extension +#neon = yes / no-- - -DUSE_NEON-- - Use ARM SIMD architecture +#dotprod = yes / no-- - -DUSE_NEON_DOTPROD-- - Use ARM advanced SIMD Int8 dot product instructions +#lsx = yes / no-- - -mlsx-- - Use Loongson SIMD eXtension +#lasx = yes / no-- - -mlasx-- - use Loongson Advanced SIMD eXtension +# +#Note that Makefile is space sensitive, so when adding new architectures +# or modifying existing flags, you have to make sure there are no extra spaces +#at the end of the line for flag values. +# +#Example of use for these flags: +#make build ARCH = x86 - 64 - avx512 debug = yes sanitize = "address undefined" + + +### 2.1. General and architecture defaults + +ifeq ($(ARCH),) + ARCH = native +endif + +ifeq ($(ARCH), native) + override ARCH := $(shell $(SHELL) ../scripts/get_native_properties.sh | cut -d " " -f 1) +endif + +#explicitly check for the list of supported architectures(as listed with make help), +#the user can override with `make ARCH = x86 - 32 - vnni256 SUPPORTED_ARCH = true` +ifeq ($(ARCH), $(filter $(ARCH), \ + x86-64-vnni512 x86-64-vnni256 x86-64-avx512 x86-64-avxvnni x86-64-bmi2 \ + x86-64-avx2 x86-64-sse41-popcnt x86-64-modern x86-64-ssse3 x86-64-sse3-popcnt \ + x86-64 x86-32-sse41-popcnt x86-32-sse2 x86-32 ppc-64 ppc-64-altivec ppc-64-vsx ppc-32 e2k \ + armv7 armv7-neon armv8 armv8-dotprod apple-silicon general-64 general-32 riscv64 \ + loongarch64 loongarch64-lsx loongarch64-lasx)) + SUPPORTED_ARCH=true +else + SUPPORTED_ARCH=false +endif + +optimize = yes +debug = no +sanitize = none +bits = 64 +prefetch = no +popcnt = no +pext = no +sse = no +mmx = no +sse2 = no +ssse3 = no +sse41 = no +avx2 = no +avxvnni = no +avx512 = no +vnni256 = no +vnni512 = no +altivec = no +vsx = no +neon = no +dotprod = no +arm_version = 0 +lsx = no +lasx = no +STRIP = strip + +ifneq ($(shell which clang-format-20 2> /dev/null),) + CLANG-FORMAT = clang-format-20 +else + CLANG-FORMAT = clang-format +endif + +### 2.2 Architecture specific + +ifeq ($(findstring x86,$(ARCH)),x86) + +#x86 - 32 / 64 + +ifeq ($(findstring x86-32,$(ARCH)),x86-32) + arch = i386 + bits = 32 + sse = no + mmx = yes +else + arch = x86_64 + sse = yes + sse2 = yes +endif + +ifeq ($(findstring -sse,$(ARCH)),-sse) + sse = yes +endif + +ifeq ($(findstring -popcnt,$(ARCH)),-popcnt) + popcnt = yes +endif + +ifeq ($(findstring -mmx,$(ARCH)),-mmx) + mmx = yes +endif + +ifeq ($(findstring -sse2,$(ARCH)),-sse2) + sse = yes + sse2 = yes +endif + +ifeq ($(findstring -ssse3,$(ARCH)),-ssse3) + sse = yes + sse2 = yes + ssse3 = yes +endif + +ifeq ($(findstring -sse41,$(ARCH)),-sse41) + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes +endif + +ifeq ($(findstring -modern,$(ARCH)),-modern) + $(warning *** ARCH=$(ARCH) is deprecated, defaulting to ARCH=x86-64-sse41-popcnt. Execute `make help` for a list of available architectures. ***) + $(shell sleep 5) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes +endif + +ifeq ($(findstring -avx2,$(ARCH)),-avx2) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes +endif + +ifeq ($(findstring -avxvnni,$(ARCH)),-avxvnni) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + avxvnni = yes + pext = yes +endif + +ifeq ($(findstring -bmi2,$(ARCH)),-bmi2) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes +endif + +ifeq ($(findstring -avx512,$(ARCH)),-avx512) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes + avx512 = yes +endif + +ifeq ($(findstring -vnni256,$(ARCH)),-vnni256) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes + vnni256 = yes +endif + +ifeq ($(findstring -vnni512,$(ARCH)),-vnni512) + popcnt = yes + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + avx2 = yes + pext = yes + avx512 = yes + vnni512 = yes +endif + +ifeq ($(sse),yes) + prefetch = yes +endif + +# 64 - bit pext is not available on x86 - 32 +ifeq ($(bits),32) + pext = no +endif + +else + +#all other architectures + +ifeq ($(ARCH),general-32) + arch = any + bits = 32 +endif + +ifeq ($(ARCH),general-64) + arch = any +endif + +ifeq ($(ARCH),armv7) + arch = armv7 + prefetch = yes + bits = 32 + arm_version = 7 +endif + +ifeq ($(ARCH),armv7-neon) + arch = armv7 + prefetch = yes + popcnt = yes + neon = yes + bits = 32 + arm_version = 7 +endif + +ifeq ($(ARCH),armv8) + arch = armv8 + prefetch = yes + popcnt = yes + neon = yes + arm_version = 8 +endif + +ifeq ($(ARCH),armv8-dotprod) + arch = armv8 + prefetch = yes + popcnt = yes + neon = yes + dotprod = yes + arm_version = 8 +endif + +ifeq ($(ARCH),apple-silicon) + arch = arm64 + prefetch = yes + popcnt = yes + neon = yes + dotprod = yes + arm_version = 8 +endif + +ifeq ($(ARCH),ppc-32) + arch = ppc + bits = 32 +endif + +ifeq ($(ARCH),ppc-64) + arch = ppc64 + popcnt = yes + prefetch = yes +endif + +ifeq ($(ARCH),ppc-64-altivec) + arch = ppc64 + popcnt = yes + prefetch = yes + altivec = yes +endif + +ifeq ($(ARCH),ppc-64-vsx) + arch = ppc64 + popcnt = yes + prefetch = yes + vsx = yes +endif + +ifeq ($(findstring e2k,$(ARCH)),e2k) + arch = e2k + mmx = yes + bits = 64 + sse = yes + sse2 = yes + ssse3 = yes + sse41 = yes + popcnt = yes +endif + +ifeq ($(ARCH),riscv64) + arch = riscv64 +endif + +ifeq ($(findstring loongarch64,$(ARCH)),loongarch64) + arch = loongarch64 + prefetch = yes + +ifeq ($(findstring -lasx,$(ARCH)),-lasx) + lsx = yes + lasx = yes +endif + +ifeq ($(findstring -lsx,$(ARCH)),-lsx) + lsx = yes +endif + +endif +endif + + +### ========================================================================== +### Section 3. Low-level Configuration +### ========================================================================== + +### 3.1 Selecting compiler (default = gcc) +ifeq ($(MAKELEVEL),0) + export ENV_CXXFLAGS := $(CXXFLAGS) + export ENV_DEPENDFLAGS := $(DEPENDFLAGS) + export ENV_LDFLAGS := $(LDFLAGS) +endif + +CXXFLAGS = $(ENV_CXXFLAGS) -Wall -Wcast-qual -fno-exceptions -std=c++17 $(EXTRACXXFLAGS) +DEPENDFLAGS = $(ENV_DEPENDFLAGS) -std=c++17 +LDFLAGS = $(ENV_LDFLAGS) $(EXTRALDFLAGS) + +ifeq ($(COMP),) + COMP=gcc +endif + +ifeq ($(COMP),gcc) + comp=gcc + CXX=g++ + CXXFLAGS += -pedantic -Wextra -Wshadow -Wmissing-declarations + + ifeq ($(arch),$(filter $(arch),armv7 armv8 riscv64)) + ifeq ($(OS),Android) + CXXFLAGS += -m$(bits) + LDFLAGS += -m$(bits) + endif + ifeq ($(ARCH),riscv64) + CXXFLAGS += -latomic + endif + else ifeq ($(arch),loongarch64) + CXXFLAGS += -latomic + else + CXXFLAGS += -m$(bits) + LDFLAGS += -m$(bits) + endif + + ifeq ($(arch),$(filter $(arch),armv7)) + LDFLAGS += -latomic + endif + + ifneq ($(KERNEL),Darwin) + LDFLAGS += -Wl,--no-as-needed + endif +endif + +ifeq ($(target_windows),yes) + LDFLAGS += -static +endif + +ifeq ($(COMP),mingw) + comp=mingw + + ifeq ($(bits),64) + ifeq ($(shell which x86_64-w64-mingw32-c++-posix 2> /dev/null),) + CXX=x86_64-w64-mingw32-c++ + else + CXX=x86_64-w64-mingw32-c++-posix + endif + else + ifeq ($(shell which i686-w64-mingw32-c++-posix 2> /dev/null),) + CXX=i686-w64-mingw32-c++ + else + CXX=i686-w64-mingw32-c++-posix + endif + endif + CXXFLAGS += -pedantic -Wextra -Wshadow -Wmissing-declarations +endif + +ifeq ($(COMP),icx) + comp=icx + CXX=icpx + CXXFLAGS += --intel -pedantic -Wextra -Wshadow -Wmissing-prototypes \ + -Wconditional-uninitialized -Wabi -Wdeprecated +endif + +ifeq ($(COMP),clang) + comp=clang + CXX=clang++ + ifeq ($(target_windows),yes) + CXX=x86_64-w64-mingw32-clang++ + endif + + CXXFLAGS += -pedantic -Wextra -Wshadow -Wmissing-prototypes \ + -Wconditional-uninitialized + + ifeq ($(filter $(KERNEL),Darwin OpenBSD FreeBSD),) + ifeq ($(target_windows),) + ifneq ($(RTLIB),compiler-rt) + LDFLAGS += -latomic + endif + endif + endif + + ifeq ($(arch),$(filter $(arch),armv7 armv8 riscv64)) + ifeq ($(OS),Android) + CXXFLAGS += -m$(bits) + LDFLAGS += -m$(bits) + endif + ifeq ($(ARCH),riscv64) + CXXFLAGS += -latomic + endif + else ifeq ($(arch),loongarch64) + CXXFLAGS += -latomic + else + CXXFLAGS += -m$(bits) + LDFLAGS += -m$(bits) + endif +endif + +ifeq ($(KERNEL),Darwin) + CXXFLAGS += -mmacosx-version-min=10.15 + LDFLAGS += -mmacosx-version-min=10.15 + ifneq ($(arch),any) + CXXFLAGS += -arch $(arch) + LDFLAGS += -arch $(arch) + endif + XCRUN = xcrun +endif + +#To cross - compile for Android, use NDK version r27c or later. +ifeq ($(COMP),ndk) + CXXFLAGS += -stdlib=libc++ + comp=clang + ifeq ($(arch),armv7) + CXX=armv7a-linux-androideabi29-clang++ + CXXFLAGS += -mthumb -march=armv7-a -mfloat-abi=softfp -mfpu=neon + ifneq ($(shell which arm-linux-androideabi-strip 2>/dev/null),) + STRIP=arm-linux-androideabi-strip + else + STRIP=llvm-strip + endif + endif + ifeq ($(arch),armv8) + CXX=aarch64-linux-android29-clang++ + ifneq ($(shell which aarch64-linux-android-strip 2>/dev/null),) + STRIP=aarch64-linux-android-strip + else + STRIP=llvm-strip + endif + endif + ifeq ($(arch),x86_64) + CXX=x86_64-linux-android29-clang++ + ifneq ($(shell which x86_64-linux-android-strip 2>/dev/null),) + STRIP=x86_64-linux-android-strip + else + STRIP=llvm-strip + endif + endif + LDFLAGS += -static-libstdc++ +endif + +### Allow overwriting CXX from command line +ifdef COMPCXX + CXX=$(COMPCXX) +endif + +#llvm - profdata must be version compatible with the specified CXX(be it clang, or the gcc alias) +#make - j profile - build CXX = clang++ - 20 COMP = clang +#Locate the version in the same directory as the compiler used, +#with fallback to a generic one if it can't be located + LLVM_PROFDATA := $(dir $(realpath $(shell which $(CXX) 2> /dev/null)))llvm-profdata +ifeq ($(wildcard $(LLVM_PROFDATA)),) + LLVM_PROFDATA := llvm-profdata +endif + +ifeq ($(comp),icx) + profile_make = icx-profile-make + profile_use = icx-profile-use +else ifeq ($(comp),clang) + profile_make = clang-profile-make + profile_use = clang-profile-use +else + profile_make = gcc-profile-make + profile_use = gcc-profile-use + ifeq ($(KERNEL),Darwin) + EXTRAPROFILEFLAGS = -fvisibility=hidden + endif +endif + +### Sometimes gcc is really clang +ifeq ($(COMP),gcc) + gccversion := $(shell $(CXX) --version 2>/dev/null) + gccisclang := $(findstring clang,$(gccversion)) + ifneq ($(gccisclang),) + profile_make = clang-profile-make + profile_use = clang-profile-use + endif +endif + +### On mingw use Windows threads, otherwise POSIX +ifneq ($(comp),mingw) + CXXFLAGS += -DUSE_PTHREADS +#On Android Bionic's C library comes with its own pthread implementation bundled in + ifneq ($(OS),Android) +#Haiku has pthreads in its libroot, so only link it in on other platforms + ifneq ($(KERNEL),Haiku) + ifneq ($(COMP),ndk) + LDFLAGS += -lpthread + endif + endif + endif +endif + +### 3.2.1 Debugging +ifeq ($(debug),no) + CXXFLAGS += -DNDEBUG +else + CXXFLAGS += -g +endif + +### 3.2.2 Debugging with undefined behavior sanitizers +ifneq ($(sanitize),none) + CXXFLAGS += -g3 $(addprefix -fsanitize=,$(sanitize)) + LDFLAGS += $(addprefix -fsanitize=,$(sanitize)) +endif + +### 3.3 Optimization +ifeq ($(optimize),yes) + + CXXFLAGS += -O3 -funroll-loops + + ifeq ($(comp),gcc) + ifeq ($(OS), Android) + CXXFLAGS += -fno-gcse -mthumb -march=armv7-a -mfloat-abi=softfp + endif + endif + + ifeq ($(KERNEL),Darwin) + ifeq ($(comp),$(filter $(comp),clang icx)) + CXXFLAGS += -mdynamic-no-pic + endif + + ifeq ($(comp),gcc) + ifneq ($(arch),arm64) + CXXFLAGS += -mdynamic-no-pic + endif + endif + endif + + ifeq ($(comp),clang) + clangmajorversion := $(shell $(CXX) -dumpversion 2>/dev/null | cut -f1 -d.) + ifeq ($(shell expr $(clangmajorversion) \< 16),1) + CXXFLAGS += -fexperimental-new-pass-manager + endif + endif +endif + +### 3.4 Bits +ifeq ($(bits),64) + CXXFLAGS += -DIS_64BIT +endif + +### 3.5 prefetch and popcount +ifeq ($(prefetch),yes) + ifeq ($(sse),yes) + CXXFLAGS += -msse + endif +else + CXXFLAGS += -DNO_PREFETCH +endif + +ifeq ($(popcnt),yes) + ifeq ($(arch),$(filter $(arch),ppc64 ppc64-altivec ppc64-vsx armv7 armv8 arm64)) + CXXFLAGS += -DUSE_POPCNT + else + CXXFLAGS += -msse3 -mpopcnt -DUSE_POPCNT + endif +endif + +### 3.6 SIMD architectures +ifeq ($(avx2),yes) + CXXFLAGS += -DUSE_AVX2 + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mavx2 -mbmi + endif +endif + +ifeq ($(avxvnni),yes) + CXXFLAGS += -DUSE_VNNI -DUSE_AVXVNNI + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mavxvnni + endif +endif + +ifeq ($(avx512),yes) + CXXFLAGS += -DUSE_AVX512 + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mavx512f -mavx512bw + endif +endif + +ifeq ($(vnni256),yes) + CXXFLAGS += -DUSE_VNNI + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mavx512f -mavx512bw -mavx512vnni -mavx512dq -mavx512vl -mprefer-vector-width=256 + endif +endif + +ifeq ($(vnni512),yes) + CXXFLAGS += -DUSE_VNNI + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mavx512f -mavx512bw -mavx512vnni -mavx512dq -mavx512vl -mprefer-vector-width=512 + endif +endif + +ifeq ($(sse41),yes) + CXXFLAGS += -DUSE_SSE41 + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -msse4.1 + endif +endif + +ifeq ($(ssse3),yes) + CXXFLAGS += -DUSE_SSSE3 + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mssse3 + endif +endif + +ifeq ($(sse2),yes) + CXXFLAGS += -DUSE_SSE2 + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -msse2 + endif +endif + +ifeq ($(mmx),yes) + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mmmx + endif +endif + +ifeq ($(altivec),yes) + CXXFLAGS += -maltivec + ifeq ($(COMP),gcc) + CXXFLAGS += -mabi=altivec + endif +endif + +ifeq ($(vsx),yes) + CXXFLAGS += -mvsx + ifeq ($(COMP),gcc) + CXXFLAGS += -DNO_WARN_X86_INTRINSICS -DUSE_SSE2 + endif +endif + +ifeq ($(neon),yes) + CXXFLAGS += -DUSE_NEON=$(arm_version) + ifeq ($(KERNEL),Linux) + ifneq ($(COMP),ndk) + ifneq ($(arch),armv8) + CXXFLAGS += -mfpu=neon + endif + endif + endif +endif + +ifeq ($(dotprod),yes) + CXXFLAGS += -march=armv8.2-a+dotprod -DUSE_NEON_DOTPROD +endif + +ifeq ($(lasx),yes) + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mlasx + endif +endif + +ifeq ($(lsx),yes) + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mlsx + endif +endif + +### 3.7 pext +ifeq ($(pext),yes) + CXXFLAGS += -DUSE_PEXT + ifeq ($(comp),$(filter $(comp),gcc clang mingw icx)) + CXXFLAGS += -mbmi2 + endif +endif + +### 3.8.1 Try to include git commit sha for versioning +GIT_SHA := $(shell git rev-parse HEAD 2>/dev/null | cut -c 1-8) +ifneq ($(GIT_SHA), ) + CXXFLAGS += -DGIT_SHA=$(GIT_SHA) +endif + +### 3.8.2 Try to include git commit date for versioning +GIT_DATE := $(shell git show -s --date=format:'%Y%m%d' --format=%cd HEAD 2>/dev/null) +ifneq ($(GIT_DATE), ) + CXXFLAGS += -DGIT_DATE=$(GIT_DATE) +endif + +### 3.8.3 Try to include architecture +ifneq ($(ARCH), ) + CXXFLAGS += -DARCH=$(ARCH) +endif + +### 3.9 Link Time Optimization +### This is a mix of compile and link time options because the lto link phase +### needs access to the optimization flags. +ifeq ($(optimize),yes) +ifeq ($(debug), no) + ifeq ($(comp),$(filter $(comp),clang icx)) + CXXFLAGS += -flto=full + ifeq ($(comp),icx) + CXXFLAGS += -fwhole-program-vtables + endif + ifeq ($(target_windows),yes) + CXXFLAGS += -fuse-ld=lld + endif + LDFLAGS += $(CXXFLAGS) + +#GCC and CLANG use different methods for parallelizing LTO and CLANG pretends to be +#GCC on some systems. + else ifeq ($(comp),gcc) + ifeq ($(gccisclang),) + CXXFLAGS += -flto -flto-partition=one + LDFLAGS += $(CXXFLAGS) -flto=jobserver + else + CXXFLAGS += -flto=full + LDFLAGS += $(CXXFLAGS) + endif + +#To use LTO and static linking on Windows, +#the tool chain requires gcc version 10.1 or later. + else ifeq ($(comp),mingw) + CXXFLAGS += -flto -flto-partition=one + LDFLAGS += $(CXXFLAGS) -save-temps + endif +endif +endif + +### 3.10 Android 5 can only run position independent executables. Note that this +### breaks Android 4.0 and earlier. +ifeq ($(OS), Android) + CXXFLAGS += -fPIE + LDFLAGS += -fPIE -pie +endif + +### ========================================================================== +### Section 4. Public Targets +### ========================================================================== + +help: + @echo "" && \ + echo "To compile cppchess_engine, type: " && \ + echo "" && \ + echo "make -j target [ARCH=arch] [COMP=compiler] [COMPCXX=cxx]" && \ + echo "" && \ + echo "Supported targets:" && \ + echo "" && \ + echo "help > Display architecture details" && \ + echo "profile-build > standard build with profile-guided optimization" && \ + echo "build > skip profile-guided optimization" && \ + echo "net > Download the default nnue nets" && \ + echo "strip > Strip executable" && \ + echo "install > Install executable" && \ + echo "clean > Clean up" && \ + echo "" && \ + echo "Supported archs:" && \ + echo "" && \ + echo "native > select the best architecture for the host processor (default)" && \ + echo "x86-64-vnni512 > x86 64-bit with vnni 512bit support" && \ + echo "x86-64-vnni256 > x86 64-bit with vnni 512bit support, limit operands to 256bit wide" && \ + echo "x86-64-avx512 > x86 64-bit with avx512 support" && \ + echo "x86-64-avxvnni > x86 64-bit with vnni 256bit support" && \ + echo "x86-64-bmi2 > x86 64-bit with bmi2 support" && \ + echo "x86-64-avx2 > x86 64-bit with avx2 support" && \ + echo "x86-64-sse41-popcnt > x86 64-bit with sse41 and popcnt support" && \ + echo "x86-64-modern > deprecated, currently x86-64-sse41-popcnt" && \ + echo "x86-64-ssse3 > x86 64-bit with ssse3 support" && \ + echo "x86-64-sse3-popcnt > x86 64-bit with sse3 compile and popcnt support" && \ + echo "x86-64 > x86 64-bit generic (with sse2 support)" && \ + echo "x86-32-sse41-popcnt > x86 32-bit with sse41 and popcnt support" && \ + echo "x86-32-sse2 > x86 32-bit with sse2 support" && \ + echo "x86-32 > x86 32-bit generic (with mmx compile support)" && \ + echo "ppc-64 > PPC 64-bit" && \ + echo "ppc-64-altivec > PPC 64-bit with altivec support" && \ + echo "ppc-64-vsx > PPC 64-bit with vsx support" && \ + echo "ppc-32 > PPC 32-bit" && \ + echo "armv7 > ARMv7 32-bit" && \ + echo "armv7-neon > ARMv7 32-bit with popcnt and neon" && \ + echo "armv8 > ARMv8 64-bit with popcnt and neon" && \ + echo "armv8-dotprod > ARMv8 64-bit with popcnt, neon and dot product support" && \ + echo "e2k > Elbrus 2000" && \ + echo "apple-silicon > Apple silicon ARM64" && \ + echo "general-64 > unspecified 64-bit" && \ + echo "general-32 > unspecified 32-bit" && \ + echo "riscv64 > RISC-V 64-bit" && \ + echo "loongarch64 > LoongArch 64-bit" && \ + echo "loongarch64-lsx > LoongArch 64-bit with SIMD eXtension" && \ + echo "loongarch64-lasx > LoongArch 64-bit with Advanced SIMD eXtension" && \ + echo "" && \ + echo "Supported compilers:" && \ + echo "" && \ + echo "gcc > GNU compiler (default)" && \ + echo "mingw > GNU compiler with MinGW under Windows" && \ + echo "clang > LLVM Clang compiler" && \ + echo "icx > Intel oneAPI DPC++/C++ Compiler" && \ + echo "ndk > Google NDK to cross-compile for Android" && \ + echo "" && \ + echo "Simple examples. If you don't know what to do, you likely want to run one of: " && \ + echo "" && \ + echo "make -j profile-build ARCH=x86-64-avx2 # typically a fast compile for common systems " && \ + echo "make -j profile-build ARCH=x86-64-sse41-popcnt # A more portable compile for 64-bit systems " && \ + echo "make -j profile-build ARCH=x86-64 # A portable compile for 64-bit systems " && \ + echo "" && \ + echo "Advanced examples, for experienced users: " && \ + echo "" && \ + echo "make -j profile-build ARCH=x86-64-avxvnni" && \ + echo "make -j profile-build ARCH=x86-64-avxvnni COMP=gcc COMPCXX=g++-12.0" && \ + echo "make -j build ARCH=x86-64-ssse3 COMP=clang" && \ + echo "" +ifneq ($(SUPPORTED_ARCH), true) + @echo "Specify a supported architecture with the ARCH option for more details" + @echo "" +endif + + +.PHONY: help analyze build profile-build strip install clean net \ + objclean profileclean config-sanity \ + icx-profile-use icx-profile-make \ + gcc-profile-use gcc-profile-make \ + clang-profile-use clang-profile-make FORCE \ + format analyze + +analyze: net config-sanity objclean + $(MAKE) -k ARCH=$(ARCH) COMP=$(COMP) $(OBJS) + +build: net config-sanity + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) all + +profile-build: net config-sanity objclean profileclean + @echo "" + @echo "Step 1/4. Building instrumented executable ..." + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) $(profile_make) + @echo "" + @echo "Step 2/4. Running benchmark for pgo-build ..." + $(PGOBENCH) > PGOBENCH.out 2>&1 + tail -n 4 PGOBENCH.out + @echo "" + @echo "Step 3/4. Building optimized executable ..." + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) objclean + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) $(profile_use) + @echo "" + @echo "Step 4/4. Deleting profile data ..." + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) profileclean + +strip: + $(STRIP) $(EXE) + +install: + -mkdir -p -m 755 $(BINDIR) + -cp $(EXE) $(BINDIR) + $(STRIP) $(BINDIR)/$(EXE) + +#clean all +clean: objclean profileclean + @rm -f .depend *~ core + +#clean binaries and objects +objclean: + @rm -f cppchess_engine cppchess_engine.exe *.o ./nnue/features/*.o ./nnue/*.o + +# clean auxiliary profiling files +profileclean: + @rm -rf profdir + @rm -f bench.txt *.gcda *.gcno ./nnue/*.gcda ./nnue/features/*.gcda *.s PGOBENCH.out + @rm -f cppchess_engine.profdata *.profraw + @rm -f cppchess_engine.*args* + @rm -f cppchess_engine.*lt* + @rm -f cppchess_engine.res + @rm -f ./-lstdc++.res + +# evaluation network (nnue) +net: + @$(SHELL) ../scripts/net.sh + +format: + $(CLANG-FORMAT) -i $(SRCS) $(HEADERS) -style=file + +# default target +default: + help + +### ========================================================================== +### Section 5. Private Targets +### ========================================================================== + +all: $(EXE) .depend + +config-sanity: net + @echo "" + @echo "Config:" && \ + echo "debug: '$(debug)'" && \ + echo "sanitize: '$(sanitize)'" && \ + echo "optimize: '$(optimize)'" && \ + echo "arch: '$(arch)'" && \ + echo "bits: '$(bits)'" && \ + echo "kernel: '$(KERNEL)'" && \ + echo "os: '$(OS)'" && \ + echo "prefetch: '$(prefetch)'" && \ + echo "popcnt: '$(popcnt)'" && \ + echo "pext: '$(pext)'" && \ + echo "sse: '$(sse)'" && \ + echo "mmx: '$(mmx)'" && \ + echo "sse2: '$(sse2)'" && \ + echo "ssse3: '$(ssse3)'" && \ + echo "sse41: '$(sse41)'" && \ + echo "avx2: '$(avx2)'" && \ + echo "avxvnni: '$(avxvnni)'" && \ + echo "avx512: '$(avx512)'" && \ + echo "vnni256: '$(vnni256)'" && \ + echo "vnni512: '$(vnni512)'" && \ + echo "altivec: '$(altivec)'" && \ + echo "vsx: '$(vsx)'" && \ + echo "neon: '$(neon)'" && \ + echo "dotprod: '$(dotprod)'" && \ + echo "arm_version: '$(arm_version)'" && \ + echo "lsx: '$(lsx)'" && \ + echo "lasx: '$(lasx)'" && \ + echo "target_windows: '$(target_windows)'" && \ + echo "" && \ + echo "Flags:" && \ + echo "CXX: $(CXX)" && \ + echo "CXXFLAGS: $(CXXFLAGS)" && \ + echo "LDFLAGS: $(LDFLAGS)" && \ + echo "" && \ + echo "Testing config sanity. If this fails, try 'make help' ..." && \ + echo "" && \ + (test "$(debug)" = "yes" || test "$(debug)" = "no") && \ + (test "$(optimize)" = "yes" || test "$(optimize)" = "no") && \ + (test "$(SUPPORTED_ARCH)" = "true") && \ + (test "$(arch)" = "any" || test "$(arch)" = "x86_64" || test "$(arch)" = "i386" || \ + test "$(arch)" = "ppc64" || test "$(arch)" = "ppc" || test "$(arch)" = "e2k" || \ + test "$(arch)" = "armv7" || test "$(arch)" = "armv8" || test "$(arch)" = "arm64" || \ + test "$(arch)" = "riscv64" || test "$(arch)" = "loongarch64") && \ + (test "$(bits)" = "32" || test "$(bits)" = "64") && \ + (test "$(prefetch)" = "yes" || test "$(prefetch)" = "no") && \ + (test "$(popcnt)" = "yes" || test "$(popcnt)" = "no") && \ + (test "$(pext)" = "yes" || test "$(pext)" = "no") && \ + (test "$(sse)" = "yes" || test "$(sse)" = "no") && \ + (test "$(mmx)" = "yes" || test "$(mmx)" = "no") && \ + (test "$(sse2)" = "yes" || test "$(sse2)" = "no") && \ + (test "$(ssse3)" = "yes" || test "$(ssse3)" = "no") && \ + (test "$(sse41)" = "yes" || test "$(sse41)" = "no") && \ + (test "$(avx2)" = "yes" || test "$(avx2)" = "no") && \ + (test "$(avx512)" = "yes" || test "$(avx512)" = "no") && \ + (test "$(vnni256)" = "yes" || test "$(vnni256)" = "no") && \ + (test "$(vnni512)" = "yes" || test "$(vnni512)" = "no") && \ + (test "$(altivec)" = "yes" || test "$(altivec)" = "no") && \ + (test "$(vsx)" = "yes" || test "$(vsx)" = "no") && \ + (test "$(neon)" = "yes" || test "$(neon)" = "no") && \ + (test "$(lsx)" = "yes" || test "$(lsx)" = "no") && \ + (test "$(lasx)" = "yes" || test "$(lasx)" = "no") && \ + (test "$(comp)" = "gcc" || test "$(comp)" = "icx" || test "$(comp)" = "mingw" || \ + test "$(comp)" = "clang" || test "$(comp)" = "armv7a-linux-androideabi16-clang" || \ + test "$(comp)" = "aarch64-linux-android21-clang") + +$(EXE): $(OBJS) + +$(CXX) -o $@ $(OBJS) $(LDFLAGS) + +# Force recompilation to ensure version info is up-to-date +misc.o: FORCE +FORCE: + +clang-profile-make: + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ + EXTRACXXFLAGS='-fprofile-generate ' \ + EXTRALDFLAGS=' -fprofile-generate' \ + all + +clang-profile-use: + $(XCRUN) $(LLVM_PROFDATA) merge -output=cppchess_engine.profdata *.profraw + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ + EXTRACXXFLAGS='-fprofile-use=cppchess_engine.profdata' \ + EXTRALDFLAGS='-fprofile-use ' \ + all + +gcc-profile-make: + @mkdir -p profdir + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ + EXTRACXXFLAGS='-fprofile-generate=profdir' \ + EXTRACXXFLAGS+=$(EXTRAPROFILEFLAGS) \ + EXTRALDFLAGS='-lgcov' \ + all + +gcc-profile-use: + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ + EXTRACXXFLAGS='-fprofile-use=profdir -fno-peel-loops -fno-tracer' \ + EXTRACXXFLAGS+=$(EXTRAPROFILEFLAGS) \ + EXTRALDFLAGS='-lgcov' \ + all + +icx-profile-make: + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ + EXTRACXXFLAGS='-fprofile-instr-generate ' \ + EXTRALDFLAGS=' -fprofile-instr-generate' \ + all + +icx-profile-use: + $(XCRUN) llvm-profdata merge -output=cppchess_engine.profdata *.profraw + $(MAKE) ARCH=$(ARCH) COMP=$(COMP) \ + EXTRACXXFLAGS='-fprofile-instr-use=cppchess_engine.profdata' \ + EXTRALDFLAGS='-fprofile-use ' \ + all + +.depend: $(SRCS) + -@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null + +ifeq (, $(filter $(MAKECMDGOALS), help strip install clean net objclean profileclean config-sanity)) +-include .depend +endif diff --git a/README.md b/src/README.md similarity index 88% rename from README.md rename to src/README.md index 1841726..d1bf29e 100644 --- a/README.md +++ b/src/README.md @@ -1,4 +1,4 @@ -# cppchess_engine +#cppchess_engine A minimal UCI-compilant (no time control now) chess engine diff --git a/src/bitboard.h b/src/bitboard.h new file mode 100644 index 0000000..d0de72a --- /dev/null +++ b/src/bitboard.h @@ -0,0 +1,141 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef BITBOARD_H_INCLUDED +#define BITBOARD_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include + +#include "types.h" + +namespace Stockfish { + +// Counts the number of non-zero bits in a bitboard. +inline int popcount(Bitboard b) { + +#ifndef USE_POPCNT + + b = b - ((b >> 1) & 0x5555555555555555ULL); + b = (b & 0x3333333333333333ULL) + ((b >> 2) & 0x3333333333333333ULL); + b = (b + (b >> 4)) & 0x0F0F0F0F0F0F0F0FULL; + return (b * 0x0101010101010101ULL) >> 56; + +#elif defined(_MSC_VER) + + return int(_mm_popcnt_u64(b)); + +#else // Assumed gcc or compatible compiler + + return __builtin_popcountll(b); + +#endif +} + +// Returns the least significant bit in a non-zero bitboard. +inline Square lsb(Bitboard b) { + assert(b); + +#if defined(__GNUC__) // GCC, Clang, ICX + + return Square(__builtin_ctzll(b)); + +#elif defined(_MSC_VER) + #ifdef _WIN64 // MSVC, WIN64 + + unsigned long idx; + _BitScanForward64(&idx, b); + return Square(idx); + + #else // MSVC, WIN32 + unsigned long idx; + + if (b & 0xffffffff) + { + _BitScanForward(&idx, int32_t(b)); + return Square(idx); + } + else + { + _BitScanForward(&idx, int32_t(b >> 32)); + return Square(idx + 32); + } + #endif +#else // Compiler is neither GCC nor MSVC compatible + #error "Compiler not supported." +#endif +} + +// Returns the most significant bit in a non-zero bitboard. +inline Square msb(Bitboard b) { + assert(b); + +#if defined(__GNUC__) // GCC, Clang, ICX + + return Square(63 ^ __builtin_clzll(b)); + +#elif defined(_MSC_VER) + #ifdef _WIN64 // MSVC, WIN64 + + unsigned long idx; + _BitScanReverse64(&idx, b); + return Square(idx); + + #else // MSVC, WIN32 + + unsigned long idx; + + if (b >> 32) + { + _BitScanReverse(&idx, int32_t(b >> 32)); + return Square(idx + 32); + } + else + { + _BitScanReverse(&idx, int32_t(b)); + return Square(idx); + } + #endif +#else // Compiler is neither GCC nor MSVC compatible + #error "Compiler not supported." +#endif +} + +// Returns the bitboard of the least significant +// square of a non-zero bitboard. It is equivalent to square_bb(lsb(bb)). +inline Bitboard least_significant_square_bb(Bitboard b) { + assert(b); + return b & -b; +} + +// Finds and clears the least significant bit in a non-zero bitboard. +inline Square pop_lsb(Bitboard& b) { + assert(b); + const Square s = lsb(b); + b &= b - 1; + return s; +} + +} // namespace Stockfish + +#endif // #ifndef BITBOARD_H_INCLUDED diff --git a/chess.hpp b/src/chess.hpp similarity index 60% rename from chess.hpp rename to src/chess.hpp index c2c7698..98ab643 100644 --- a/chess.hpp +++ b/src/chess.hpp @@ -25,12 +25,15 @@ THIS FILE IS AUTO GENERATED DO NOT CHANGE MANUALLY. Source: https://github.com/Disservin/chess-library -VERSION: 0.8.13 +VERSION: 0.8.2 */ #ifndef CHESS_HPP #define CHESS_HPP - +#ifndef NDEBUG + #define NDEBUG + #define CHESS_NO_EXCEPTIONS +#endif #include #include @@ -38,12 +41,12 @@ VERSION: 0.8.13 #include #ifdef CHESS_USE_PEXT -# include + #include #endif #if __cpp_lib_bitops >= 201907L -# include + #include #endif #include #include @@ -52,8 +55,8 @@ VERSION: 0.8.13 #include #if defined(_MSC_VER) -# include -# include + #include + #include #endif @@ -66,15 +69,26 @@ namespace chess { class Color { public: - enum class underlying : std::int8_t { WHITE = 0, BLACK = 1, NONE = -1 }; + enum class underlying : std::int8_t { + WHITE = 0, + BLACK = 1, + NONE = -1 + }; - constexpr Color() : color(underlying::NONE) {} - constexpr Color(underlying c) : color(c) { assert(isValid(static_cast(c))); } - constexpr Color(int c) : Color(static_cast(c)) { assert(isValid(c)); } - constexpr Color(std::string_view str) - : color(str == "w" ? underlying::WHITE - : str == "b" ? underlying::BLACK - : underlying::NONE) {} + constexpr Color() : + color(underlying::NONE) {} + constexpr Color(underlying c) : + color(c) { + assert(isValid(static_cast(c))); + } + constexpr Color(int c) : + Color(static_cast(c)) { + assert(isValid(c)); + } + constexpr Color(std::string_view str) : + color(str == "w" ? underlying::WHITE + : str == "b" ? underlying::BLACK + : underlying::NONE) {} /** * @brief Gets the long string representation of the color @@ -82,13 +96,14 @@ class Color { * "None" for NONE */ [[nodiscard]] std::string longStr() const { - switch (color) { - case underlying::WHITE: - return "White"; - case underlying::BLACK: - return "Black"; - default: - return "None"; + switch (color) + { + case underlying::WHITE : + return "White"; + case underlying::BLACK : + return "Black"; + default : + return "None"; } } @@ -108,8 +123,8 @@ class Color { [[nodiscard]] constexpr underlying internal() const noexcept { return color; } - friend std::ostream& operator<<(std::ostream& os, const Color& color) { - return os << static_cast(color); + friend std::ostream& operator<<(std::ostream& os, const Color& _color) { + return os << static_cast(_color); } static constexpr underlying WHITE = underlying::WHITE; @@ -123,9 +138,9 @@ class Color { }; constexpr Color::underlying operator~(Color::underlying color) { - return color == Color::underlying::WHITE ? Color::underlying::BLACK - : color == Color::underlying::BLACK ? Color::underlying::WHITE - : Color::underlying::NONE; + return color == Color::underlying::WHITE ? Color::underlying::BLACK + : color == Color::underlying::BLACK ? Color::underlying::WHITE + : Color::underlying::NONE; } } // namespace chess @@ -136,12 +151,14 @@ namespace chess { namespace utils { // Split a string by a delimiter -[[nodiscard]] inline std::vector splitString(std::string_view string, const char &delimiter) { +[[nodiscard]] inline std::vector splitString(std::string_view string, + const char& delimiter) { std::vector result; - size_t start = 0; - size_t end = string.find(delimiter); + size_t start = 0; + size_t end = string.find(delimiter); - while (end != std::string_view::npos) { + while (end != std::string_view::npos) + { result.push_back(string.substr(start, end - start)); start = end + 1; end = string.find(delimiter, start); @@ -161,7 +178,7 @@ constexpr char tolower(char c) { return (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') namespace chess { -#define CHESS_DECLARE_RANK(N) \ +#define CHESS_DECLARE_RANK(N) \ static constexpr auto SQ_A##N = underlying::SQ_A##N; \ static constexpr auto SQ_B##N = underlying::SQ_B##N; \ static constexpr auto SQ_C##N = underlying::SQ_C##N; \ @@ -173,13 +190,29 @@ namespace chess { class File { public: - enum class underlying : std::uint8_t { FILE_A, FILE_B, FILE_C, FILE_D, FILE_E, FILE_F, FILE_G, FILE_H, NO_FILE }; + enum class underlying : std::uint8_t { + FILE_A, + FILE_B, + FILE_C, + FILE_D, + FILE_E, + FILE_F, + FILE_G, + FILE_H, + NO_FILE + }; - constexpr File() : file(underlying::NO_FILE) {} - constexpr File(underlying file) : file(file) {} - constexpr File(int file) : file(static_cast(file)) {} - constexpr File(std::string_view sw) - : file(static_cast(static_cast(utils::tolower(static_cast(sw[0]))) - 'a')) {} + constexpr File() : + file(underlying::NO_FILE) {} + constexpr File(underlying file) : + file(file) {} + constexpr File(int file) : + file(static_cast(file)) {} + constexpr File(std::string_view sw) : + file(sw.empty() + ? underlying::NO_FILE + : static_cast( + static_cast(utils::tolower(static_cast(sw[0]))) - 'a')) {} [[nodiscard]] constexpr underlying internal() const noexcept { return file; } @@ -211,7 +244,9 @@ class File { constexpr operator int() const noexcept { return static_cast(file); } - explicit operator std::string() const { return std::string(1, static_cast(static_cast(file) + 'a')); } + explicit operator std::string() const { + return std::string(1, static_cast(static_cast(file) + 'a')); + } static constexpr underlying FILE_A = underlying::FILE_A; static constexpr underlying FILE_B = underlying::FILE_B; @@ -229,13 +264,27 @@ class File { class Rank { public: - enum class underlying { RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8, NO_RANK }; + enum class underlying { + RANK_1, + RANK_2, + RANK_3, + RANK_4, + RANK_5, + RANK_6, + RANK_7, + RANK_8, + NO_RANK + }; - constexpr Rank() : rank_(underlying::NO_RANK) {} - constexpr Rank(underlying rank) : rank_(rank) {} - constexpr Rank(int rank) : rank_(static_cast(rank)) {} - constexpr Rank(std::string_view sw) - : rank_(static_cast(static_cast(utils::tolower(static_cast(sw[0]))) - '1')) {} + constexpr Rank() : + rank_(underlying::NO_RANK) {} + constexpr Rank(underlying rank) : + rank_(rank) {} + constexpr Rank(int rank) : + rank_(static_cast(rank)) {} + constexpr Rank(std::string_view sw) : + rank_(static_cast( + static_cast(utils::tolower(static_cast(sw[0]))) - '1')) {} [[nodiscard]] constexpr underlying internal() const noexcept { return rank_; } @@ -257,11 +306,15 @@ class Rank { return *this; } - operator std::string() const { return std::string(1, static_cast(static_cast(rank_) + '1')); } + operator std::string() const { + return std::string(1, static_cast(static_cast(rank_) + '1')); + } constexpr operator int() const noexcept { return static_cast(rank_); } - [[nodiscard]] constexpr std::uint64_t bb() const noexcept { return 0xffULL << (8 * static_cast(rank_)); } + [[nodiscard]] constexpr std::uint64_t bb() const noexcept { + return 0xffULL << (8 * static_cast(rank_)); + } [[nodiscard]] static constexpr bool back_rank(Rank r, Color color) noexcept { return r == Rank(static_cast(color) * 7); @@ -319,13 +372,21 @@ class Square { #endif - constexpr Square() : sq(underlying::NO_SQ) {} - - constexpr Square(int sq) : sq(static_cast(sq)) { assert(sq <= 64 && sq >= 0); } - constexpr Square(File file, Rank rank) : sq(static_cast(file + rank * 8)) {} - constexpr Square(Rank rank, File file) : sq(static_cast(file + rank * 8)) {} - constexpr Square(underlying sq) : sq(sq) {} - constexpr Square(std::string_view str) : sq(static_cast((str[0] - 'a') + (str[1] - '1') * 8)) { + constexpr Square() : + sq(underlying::NO_SQ) {} + + constexpr Square(int sq) : + sq(static_cast(sq)) { + assert(sq <= 64 && sq >= 0); + } + constexpr Square(File file, Rank rank) : + sq(static_cast(file + rank * 8)) {} + constexpr Square(Rank rank, File file) : + sq(static_cast(file + rank * 8)) {} + constexpr Square(underlying sq) : + sq(sq) {} + constexpr Square(std::string_view str) : + sq(static_cast((str[0] - 'a') + (str[1] - '1') * 8)) { assert(str.size() >= 2); } @@ -402,8 +463,7 @@ class Square { * @brief Check if the square is light. * @return */ - [[nodiscard]] constexpr bool is_light() const noexcept { return (file() + rank()) & 1; - } + [[nodiscard]] constexpr bool is_light() const noexcept { return (file() + rank()) & 1; } /** * @brief Check if the square is dark. @@ -412,10 +472,12 @@ class Square { [[nodiscard]] constexpr bool is_dark() const noexcept { return !is_light(); } /** - * @brief Check if the square is vali.d + * @brief Check if the square is valid * @return */ - [[nodiscard]] constexpr bool is_valid() const noexcept { return static_cast(sq) < 64; } + [[nodiscard]] constexpr bool is_valid() const noexcept { + return static_cast(sq) < 64; + } /** * @brief Check if the square is valid. @@ -508,7 +570,8 @@ class Square { * @param c * @return */ - [[nodiscard]] static constexpr Square castling_king_square(bool is_king_side, Color c) noexcept { + [[nodiscard]] static constexpr Square castling_king_square(bool is_king_side, + Color c) noexcept { return Square(is_king_side ? Square::SQ_G1 : Square::SQ_C1).relative_square(c); } @@ -518,7 +581,8 @@ class Square { * @param c * @return */ - [[nodiscard]] static constexpr Square castling_rook_square(bool is_king_side, Color c) noexcept { + [[nodiscard]] static constexpr Square castling_rook_square(bool is_king_side, + Color c) noexcept { return Square(is_king_side ? Square::SQ_F1 : Square::SQ_D1).relative_square(c); } @@ -555,7 +619,8 @@ enum class Direction : std::int8_t { }; [[nodiscard]] constexpr Direction make_direction(Direction dir, Color c) noexcept { - if (c == Color::BLACK) return static_cast(-static_cast(dir)); + if (c == Color::BLACK) + return static_cast(-static_cast(dir)); return dir; } @@ -571,13 +636,17 @@ namespace chess { class Bitboard { public: - constexpr Bitboard() : bits(0) {} - constexpr Bitboard(std::uint64_t bits) : bits(bits) {} - constexpr Bitboard(File file) : bits(0) { + constexpr Bitboard() : + bits(0) {} + constexpr Bitboard(std::uint64_t bits) : + bits(bits) {} + constexpr Bitboard(File file) : + bits(0) { assert(file != File::NO_FILE); bits = 0x0101010101010101ULL << static_cast(file.internal()); } - constexpr Bitboard(Rank rank) : bits(0) { + constexpr Bitboard(Rank rank) : + bits(0) { assert(rank != Rank::NO_RANK); bits = 0xFFULL << (8 * static_cast(rank.internal())); } @@ -586,11 +655,12 @@ class Bitboard { explicit operator std::string() const { std::bitset<64> b(bits); - std::string str_bitset = b.to_string(); + std::string str_bitset = b.to_string(); std::string str; - for (int i = 0; i < 64; i += 8) { + for (int i = 0; i < 64; i += 8) + { std::string x = str_bitset.substr(i, 8); std::reverse(x.begin(), x.end()); str += x + '\n'; @@ -603,14 +673,24 @@ class Bitboard { constexpr Bitboard operator&(std::uint64_t rhs) const noexcept { return Bitboard(bits & rhs); } constexpr Bitboard operator|(std::uint64_t rhs) const noexcept { return Bitboard(bits | rhs); } constexpr Bitboard operator^(std::uint64_t rhs) const noexcept { return Bitboard(bits ^ rhs); } - constexpr Bitboard operator<<(std::uint64_t rhs) const noexcept { return Bitboard(bits << rhs); } - constexpr Bitboard operator>>(std::uint64_t rhs) const noexcept { return Bitboard(bits >> rhs); } + constexpr Bitboard operator<<(std::uint64_t rhs) const noexcept { + return Bitboard(bits << rhs); + } + constexpr Bitboard operator>>(std::uint64_t rhs) const noexcept { + return Bitboard(bits >> rhs); + } constexpr bool operator==(std::uint64_t rhs) const noexcept { return bits == rhs; } constexpr bool operator!=(std::uint64_t rhs) const noexcept { return bits != rhs; } - constexpr Bitboard operator&(const Bitboard& rhs) const noexcept { return Bitboard(bits & rhs.bits); } - constexpr Bitboard operator|(const Bitboard& rhs) const noexcept { return Bitboard(bits | rhs.bits); } - constexpr Bitboard operator^(const Bitboard& rhs) const noexcept { return Bitboard(bits ^ rhs.bits); } + constexpr Bitboard operator&(const Bitboard& rhs) const noexcept { + return Bitboard(bits & rhs.bits); + } + constexpr Bitboard operator|(const Bitboard& rhs) const noexcept { + return Bitboard(bits | rhs.bits); + } + constexpr Bitboard operator^(const Bitboard& rhs) const noexcept { + return Bitboard(bits ^ rhs.bits); + } constexpr Bitboard operator~() const noexcept { return Bitboard(~bits); } constexpr Bitboard& operator&=(const Bitboard& rhs) noexcept { @@ -671,20 +751,20 @@ class Bitboard { #if __cpp_lib_bitops >= 201907L constexpr #endif - int lsb() const noexcept { + int lsb() const noexcept { assert(bits != 0); #if __cpp_lib_bitops >= 201907L return std::countr_zero(bits); #else -# if defined(__GNUC__) + #if defined(__GNUC__) return __builtin_ctzll(bits); -# elif defined(_MSC_VER) + #elif defined(_MSC_VER) unsigned long idx; _BitScanForward64(&idx, bits); return static_cast(idx); -# else -# error "Compiler not supported." -# endif + #else + #error "Compiler not supported." + #endif #endif } @@ -692,21 +772,21 @@ class Bitboard { #if __cpp_lib_bitops >= 201907L constexpr #endif - int msb() const noexcept { + int msb() const noexcept { assert(bits != 0); #if __cpp_lib_bitops >= 201907L return std::countl_zero(bits) ^ 63; #else -# if defined(__GNUC__) + #if defined(__GNUC__) return 63 ^ __builtin_clzll(bits); -# elif defined(_MSC_VER) + #elif defined(_MSC_VER) unsigned long idx; _BitScanReverse64(&idx, bits); return static_cast(idx); -# else -# error "Compiler not supported." -# endif + #else + #error "Compiler not supported." + #endif #endif } @@ -714,15 +794,15 @@ class Bitboard { #if __cpp_lib_bitops >= 201907L constexpr #endif - int count() const noexcept { + int count() const noexcept { #if __cpp_lib_bitops >= 201907L return std::popcount(bits); #else -# if defined(_MSC_VER) || defined(__INTEL_COMPILER) + #if defined(_MSC_VER) || defined(__INTEL_COMPILER) return static_cast(_mm_popcnt_u64(bits)); -# else + #else return __builtin_popcountll(bits); -# endif + #endif #endif } @@ -730,7 +810,7 @@ class Bitboard { #if __cpp_lib_bitops >= 201907L constexpr #endif - std::uint8_t pop() noexcept { + std::uint8_t pop() noexcept { assert(bits != 0); std::uint8_t index = lsb(); bits &= bits - 1; @@ -759,7 +839,6 @@ class Board; } // namespace chess - namespace chess { class PieceType { @@ -774,9 +853,12 @@ class PieceType { NONE, }; - constexpr PieceType() : pt(underlying::NONE) {} - constexpr PieceType(underlying pt) : pt(pt) {} - constexpr explicit PieceType(std::string_view type) : pt(underlying::NONE) { + constexpr PieceType() : + pt(underlying::NONE) {} + constexpr PieceType(underlying pt) : + pt(pt) {} + constexpr explicit PieceType(std::string_view type) : + pt(underlying::NONE) { assert(type.size() > 0); char c = type[0]; @@ -798,7 +880,8 @@ class PieceType { } explicit operator std::string() const { - if (pt == underlying::NONE) return " "; + if (pt == underlying::NONE) + return " "; constexpr static const char* pieceTypeStr[] = {"p", "n", "b", "r", "q", "k"}; return pieceTypeStr[static_cast(pt)]; } @@ -845,17 +928,24 @@ class Piece { NONE }; - constexpr Piece() : piece(underlying::NONE) {} - constexpr Piece(underlying piece) : piece(piece) {} - constexpr Piece(PieceType type, Color color) - : piece(color == Color::NONE ? Piece::NONE - : type == PieceType::NONE ? Piece::NONE - : static_cast(static_cast(color.internal()) * 6 + type)) {} - constexpr Piece(Color color, PieceType type) - : piece(color == Color::NONE ? Piece::NONE - : type == PieceType::NONE ? Piece::NONE - : static_cast(static_cast(color.internal()) * 6 + type)) {} - constexpr Piece(std::string_view p) : piece(underlying::NONE) { piece = convertCharToUnderlying(p[0]); } + constexpr Piece() : + piece(underlying::NONE) {} + constexpr Piece(underlying piece) : + piece(piece) {} + constexpr Piece(PieceType type, Color color) : + piece(color == Color::NONE ? Piece::NONE + : type == PieceType::NONE + ? Piece::NONE + : static_cast(static_cast(color.internal()) * 6 + type)) {} + constexpr Piece(Color color, PieceType type) : + piece(color == Color::NONE ? Piece::NONE + : type == PieceType::NONE + ? Piece::NONE + : static_cast(static_cast(color.internal()) * 6 + type)) {} + constexpr Piece(std::string_view p) : + piece(underlying::NONE) { + piece = convertCharToUnderlying(p[0]); + } constexpr bool operator<(const Piece& rhs) const noexcept { return piece < rhs.piece; } constexpr bool operator>(const Piece& rhs) const noexcept { return piece > rhs.piece; } @@ -871,21 +961,24 @@ class Piece { explicit operator std::string() const { constexpr static const char* pieceStr[] = {"P", "N", "B", "R", "Q", "K", // "p", "n", "b", "r", "q", "k"}; - if (piece == NONE) return "."; + if (piece == NONE) + return "."; return pieceStr[static_cast(piece)]; } constexpr operator int() const noexcept { return static_cast(piece); } [[nodiscard]] constexpr PieceType type() const noexcept { - if (piece == NONE) return PieceType::NONE; + if (piece == NONE) + return PieceType::NONE; // return static_cast(int(piece) % 6); - return static_cast(static_cast(piece) > 5 ? static_cast(piece) - 6 - : static_cast(piece)); + return static_cast( + static_cast(piece) > 5 ? static_cast(piece) - 6 : static_cast(piece)); } [[nodiscard]] constexpr Color color() const noexcept { - if (piece == NONE) return Color::NONE; + if (piece == NONE) + return Color::NONE; return static_cast(static_cast(piece) / 6); } @@ -909,33 +1002,34 @@ class Piece { underlying piece; [[nodiscard]] constexpr static underlying convertCharToUnderlying(char c) { - switch (c) { - case 'P': - return WHITEPAWN; - case 'N': - return WHITEKNIGHT; - case 'B': - return WHITEBISHOP; - case 'R': - return WHITEROOK; - case 'Q': - return WHITEQUEEN; - case 'K': - return WHITEKING; - case 'p': - return BLACKPAWN; - case 'n': - return BLACKKNIGHT; - case 'b': - return BLACKBISHOP; - case 'r': - return BLACKROOK; - case 'q': - return BLACKQUEEN; - case 'k': - return BLACKKING; - default: - return NONE; + switch (c) + { + case 'P' : + return WHITEPAWN; + case 'N' : + return WHITEKNIGHT; + case 'B' : + return WHITEBISHOP; + case 'R' : + return WHITEROOK; + case 'Q' : + return WHITEQUEEN; + case 'K' : + return WHITEKING; + case 'p' : + return BLACKPAWN; + case 'n' : + return BLACKKNIGHT; + case 'b' : + return BLACKBISHOP; + case 'r' : + return BLACKROOK; + case 'q' : + return BLACKQUEEN; + case 'k' : + return BLACKKING; + default : + return NONE; } } }; @@ -947,27 +1041,31 @@ class attacks { #ifdef CHESS_USE_PEXT struct Magic { - U64 mask; - Bitboard *attacks; - U64 operator()(Bitboard b) const noexcept { return _pext_u64(b.getBits(), mask); } + U64 mask; + Bitboard* attacks; + U64 operator()(Bitboard b) const noexcept { return _pext_u64(b.getBits(), mask); } }; #else struct Magic { - U64 mask; - U64 magic; - Bitboard *attacks; - U64 shift; - U64 operator()(Bitboard b) const noexcept { return (((b & mask)).getBits() * magic) >> shift; } + U64 mask; + U64 magic; + Bitboard* attacks; + U64 shift; + U64 operator()(Bitboard b) const noexcept { + return (((b & mask)).getBits() * magic) >> shift; + } }; #endif // Slow function to calculate bishop and rook attacks - template + template [[nodiscard]] static Bitboard sliderAttacks(Square sq, Bitboard occupied) noexcept; // Initializes the magic bitboard tables for sliding pieces - static void initSliders(Square sq, Magic table[], U64 magic, - const std::function &attacks); + static void initSliders(Square sq, + Magic table[], + U64 magic, + const std::function& attacks); // clang-format off // pre-calculated lookup table for pawn attacks @@ -1014,71 +1112,77 @@ class attacks { // pre-calculated lookup table for knight attacks static constexpr Bitboard KnightAttacks[64] = { - 0x0000000000020400, 0x0000000000050800, 0x00000000000A1100, 0x0000000000142200, 0x0000000000284400, - 0x0000000000508800, 0x0000000000A01000, 0x0000000000402000, 0x0000000002040004, 0x0000000005080008, - 0x000000000A110011, 0x0000000014220022, 0x0000000028440044, 0x0000000050880088, 0x00000000A0100010, - 0x0000000040200020, 0x0000000204000402, 0x0000000508000805, 0x0000000A1100110A, 0x0000001422002214, - 0x0000002844004428, 0x0000005088008850, 0x000000A0100010A0, 0x0000004020002040, 0x0000020400040200, - 0x0000050800080500, 0x00000A1100110A00, 0x0000142200221400, 0x0000284400442800, 0x0000508800885000, - 0x0000A0100010A000, 0x0000402000204000, 0x0002040004020000, 0x0005080008050000, 0x000A1100110A0000, - 0x0014220022140000, 0x0028440044280000, 0x0050880088500000, 0x00A0100010A00000, 0x0040200020400000, - 0x0204000402000000, 0x0508000805000000, 0x0A1100110A000000, 0x1422002214000000, 0x2844004428000000, - 0x5088008850000000, 0xA0100010A0000000, 0x4020002040000000, 0x0400040200000000, 0x0800080500000000, - 0x1100110A00000000, 0x2200221400000000, 0x4400442800000000, 0x8800885000000000, 0x100010A000000000, - 0x2000204000000000, 0x0004020000000000, 0x0008050000000000, 0x00110A0000000000, 0x0022140000000000, - 0x0044280000000000, 0x0088500000000000, 0x0010A00000000000, 0x0020400000000000}; + 0x0000000000020400, 0x0000000000050800, 0x00000000000A1100, 0x0000000000142200, + 0x0000000000284400, 0x0000000000508800, 0x0000000000A01000, 0x0000000000402000, + 0x0000000002040004, 0x0000000005080008, 0x000000000A110011, 0x0000000014220022, + 0x0000000028440044, 0x0000000050880088, 0x00000000A0100010, 0x0000000040200020, + 0x0000000204000402, 0x0000000508000805, 0x0000000A1100110A, 0x0000001422002214, + 0x0000002844004428, 0x0000005088008850, 0x000000A0100010A0, 0x0000004020002040, + 0x0000020400040200, 0x0000050800080500, 0x00000A1100110A00, 0x0000142200221400, + 0x0000284400442800, 0x0000508800885000, 0x0000A0100010A000, 0x0000402000204000, + 0x0002040004020000, 0x0005080008050000, 0x000A1100110A0000, 0x0014220022140000, + 0x0028440044280000, 0x0050880088500000, 0x00A0100010A00000, 0x0040200020400000, + 0x0204000402000000, 0x0508000805000000, 0x0A1100110A000000, 0x1422002214000000, + 0x2844004428000000, 0x5088008850000000, 0xA0100010A0000000, 0x4020002040000000, + 0x0400040200000000, 0x0800080500000000, 0x1100110A00000000, 0x2200221400000000, + 0x4400442800000000, 0x8800885000000000, 0x100010A000000000, 0x2000204000000000, + 0x0004020000000000, 0x0008050000000000, 0x00110A0000000000, 0x0022140000000000, + 0x0044280000000000, 0x0088500000000000, 0x0010A00000000000, 0x0020400000000000}; // pre-calculated lookup table for king attacks static constexpr Bitboard KingAttacks[64] = { - 0x0000000000000302, 0x0000000000000705, 0x0000000000000E0A, 0x0000000000001C14, 0x0000000000003828, - 0x0000000000007050, 0x000000000000E0A0, 0x000000000000C040, 0x0000000000030203, 0x0000000000070507, - 0x00000000000E0A0E, 0x00000000001C141C, 0x0000000000382838, 0x0000000000705070, 0x0000000000E0A0E0, - 0x0000000000C040C0, 0x0000000003020300, 0x0000000007050700, 0x000000000E0A0E00, 0x000000001C141C00, - 0x0000000038283800, 0x0000000070507000, 0x00000000E0A0E000, 0x00000000C040C000, 0x0000000302030000, - 0x0000000705070000, 0x0000000E0A0E0000, 0x0000001C141C0000, 0x0000003828380000, 0x0000007050700000, - 0x000000E0A0E00000, 0x000000C040C00000, 0x0000030203000000, 0x0000070507000000, 0x00000E0A0E000000, - 0x00001C141C000000, 0x0000382838000000, 0x0000705070000000, 0x0000E0A0E0000000, 0x0000C040C0000000, - 0x0003020300000000, 0x0007050700000000, 0x000E0A0E00000000, 0x001C141C00000000, 0x0038283800000000, - 0x0070507000000000, 0x00E0A0E000000000, 0x00C040C000000000, 0x0302030000000000, 0x0705070000000000, - 0x0E0A0E0000000000, 0x1C141C0000000000, 0x3828380000000000, 0x7050700000000000, 0xE0A0E00000000000, - 0xC040C00000000000, 0x0203000000000000, 0x0507000000000000, 0x0A0E000000000000, 0x141C000000000000, - 0x2838000000000000, 0x5070000000000000, 0xA0E0000000000000, 0x40C0000000000000}; + 0x0000000000000302, 0x0000000000000705, 0x0000000000000E0A, 0x0000000000001C14, + 0x0000000000003828, 0x0000000000007050, 0x000000000000E0A0, 0x000000000000C040, + 0x0000000000030203, 0x0000000000070507, 0x00000000000E0A0E, 0x00000000001C141C, + 0x0000000000382838, 0x0000000000705070, 0x0000000000E0A0E0, 0x0000000000C040C0, + 0x0000000003020300, 0x0000000007050700, 0x000000000E0A0E00, 0x000000001C141C00, + 0x0000000038283800, 0x0000000070507000, 0x00000000E0A0E000, 0x00000000C040C000, + 0x0000000302030000, 0x0000000705070000, 0x0000000E0A0E0000, 0x0000001C141C0000, + 0x0000003828380000, 0x0000007050700000, 0x000000E0A0E00000, 0x000000C040C00000, + 0x0000030203000000, 0x0000070507000000, 0x00000E0A0E000000, 0x00001C141C000000, + 0x0000382838000000, 0x0000705070000000, 0x0000E0A0E0000000, 0x0000C040C0000000, + 0x0003020300000000, 0x0007050700000000, 0x000E0A0E00000000, 0x001C141C00000000, + 0x0038283800000000, 0x0070507000000000, 0x00E0A0E000000000, 0x00C040C000000000, + 0x0302030000000000, 0x0705070000000000, 0x0E0A0E0000000000, 0x1C141C0000000000, + 0x3828380000000000, 0x7050700000000000, 0xE0A0E00000000000, 0xC040C00000000000, + 0x0203000000000000, 0x0507000000000000, 0x0A0E000000000000, 0x141C000000000000, + 0x2838000000000000, 0x5070000000000000, 0xA0E0000000000000, 0x40C0000000000000}; static constexpr U64 RookMagics[64] = { - 0x8a80104000800020ULL, 0x140002000100040ULL, 0x2801880a0017001ULL, 0x100081001000420ULL, - 0x200020010080420ULL, 0x3001c0002010008ULL, 0x8480008002000100ULL, 0x2080088004402900ULL, - 0x800098204000ULL, 0x2024401000200040ULL, 0x100802000801000ULL, 0x120800800801000ULL, - 0x208808088000400ULL, 0x2802200800400ULL, 0x2200800100020080ULL, 0x801000060821100ULL, - 0x80044006422000ULL, 0x100808020004000ULL, 0x12108a0010204200ULL, 0x140848010000802ULL, - 0x481828014002800ULL, 0x8094004002004100ULL, 0x4010040010010802ULL, 0x20008806104ULL, - 0x100400080208000ULL, 0x2040002120081000ULL, 0x21200680100081ULL, 0x20100080080080ULL, - 0x2000a00200410ULL, 0x20080800400ULL, 0x80088400100102ULL, 0x80004600042881ULL, - 0x4040008040800020ULL, 0x440003000200801ULL, 0x4200011004500ULL, 0x188020010100100ULL, - 0x14800401802800ULL, 0x2080040080800200ULL, 0x124080204001001ULL, 0x200046502000484ULL, - 0x480400080088020ULL, 0x1000422010034000ULL, 0x30200100110040ULL, 0x100021010009ULL, - 0x2002080100110004ULL, 0x202008004008002ULL, 0x20020004010100ULL, 0x2048440040820001ULL, - 0x101002200408200ULL, 0x40802000401080ULL, 0x4008142004410100ULL, 0x2060820c0120200ULL, - 0x1001004080100ULL, 0x20c020080040080ULL, 0x2935610830022400ULL, 0x44440041009200ULL, - 0x280001040802101ULL, 0x2100190040002085ULL, 0x80c0084100102001ULL, 0x4024081001000421ULL, - 0x20030a0244872ULL, 0x12001008414402ULL, 0x2006104900a0804ULL, 0x1004081002402ULL}; + 0x8a80104000800020ULL, 0x140002000100040ULL, 0x2801880a0017001ULL, 0x100081001000420ULL, + 0x200020010080420ULL, 0x3001c0002010008ULL, 0x8480008002000100ULL, 0x2080088004402900ULL, + 0x800098204000ULL, 0x2024401000200040ULL, 0x100802000801000ULL, 0x120800800801000ULL, + 0x208808088000400ULL, 0x2802200800400ULL, 0x2200800100020080ULL, 0x801000060821100ULL, + 0x80044006422000ULL, 0x100808020004000ULL, 0x12108a0010204200ULL, 0x140848010000802ULL, + 0x481828014002800ULL, 0x8094004002004100ULL, 0x4010040010010802ULL, 0x20008806104ULL, + 0x100400080208000ULL, 0x2040002120081000ULL, 0x21200680100081ULL, 0x20100080080080ULL, + 0x2000a00200410ULL, 0x20080800400ULL, 0x80088400100102ULL, 0x80004600042881ULL, + 0x4040008040800020ULL, 0x440003000200801ULL, 0x4200011004500ULL, 0x188020010100100ULL, + 0x14800401802800ULL, 0x2080040080800200ULL, 0x124080204001001ULL, 0x200046502000484ULL, + 0x480400080088020ULL, 0x1000422010034000ULL, 0x30200100110040ULL, 0x100021010009ULL, + 0x2002080100110004ULL, 0x202008004008002ULL, 0x20020004010100ULL, 0x2048440040820001ULL, + 0x101002200408200ULL, 0x40802000401080ULL, 0x4008142004410100ULL, 0x2060820c0120200ULL, + 0x1001004080100ULL, 0x20c020080040080ULL, 0x2935610830022400ULL, 0x44440041009200ULL, + 0x280001040802101ULL, 0x2100190040002085ULL, 0x80c0084100102001ULL, 0x4024081001000421ULL, + 0x20030a0244872ULL, 0x12001008414402ULL, 0x2006104900a0804ULL, 0x1004081002402ULL}; static constexpr U64 BishopMagics[64] = { - 0x40040844404084ULL, 0x2004208a004208ULL, 0x10190041080202ULL, 0x108060845042010ULL, - 0x581104180800210ULL, 0x2112080446200010ULL, 0x1080820820060210ULL, 0x3c0808410220200ULL, - 0x4050404440404ULL, 0x21001420088ULL, 0x24d0080801082102ULL, 0x1020a0a020400ULL, - 0x40308200402ULL, 0x4011002100800ULL, 0x401484104104005ULL, 0x801010402020200ULL, - 0x400210c3880100ULL, 0x404022024108200ULL, 0x810018200204102ULL, 0x4002801a02003ULL, - 0x85040820080400ULL, 0x810102c808880400ULL, 0xe900410884800ULL, 0x8002020480840102ULL, - 0x220200865090201ULL, 0x2010100a02021202ULL, 0x152048408022401ULL, 0x20080002081110ULL, - 0x4001001021004000ULL, 0x800040400a011002ULL, 0xe4004081011002ULL, 0x1c004001012080ULL, - 0x8004200962a00220ULL, 0x8422100208500202ULL, 0x2000402200300c08ULL, 0x8646020080080080ULL, - 0x80020a0200100808ULL, 0x2010004880111000ULL, 0x623000a080011400ULL, 0x42008c0340209202ULL, - 0x209188240001000ULL, 0x400408a884001800ULL, 0x110400a6080400ULL, 0x1840060a44020800ULL, - 0x90080104000041ULL, 0x201011000808101ULL, 0x1a2208080504f080ULL, 0x8012020600211212ULL, - 0x500861011240000ULL, 0x180806108200800ULL, 0x4000020e01040044ULL, 0x300000261044000aULL, - 0x802241102020002ULL, 0x20906061210001ULL, 0x5a84841004010310ULL, 0x4010801011c04ULL, - 0xa010109502200ULL, 0x4a02012000ULL, 0x500201010098b028ULL, 0x8040002811040900ULL, - 0x28000010020204ULL, 0x6000020202d0240ULL, 0x8918844842082200ULL, 0x4010011029020020ULL}; + 0x40040844404084ULL, 0x2004208a004208ULL, 0x10190041080202ULL, 0x108060845042010ULL, + 0x581104180800210ULL, 0x2112080446200010ULL, 0x1080820820060210ULL, 0x3c0808410220200ULL, + 0x4050404440404ULL, 0x21001420088ULL, 0x24d0080801082102ULL, 0x1020a0a020400ULL, + 0x40308200402ULL, 0x4011002100800ULL, 0x401484104104005ULL, 0x801010402020200ULL, + 0x400210c3880100ULL, 0x404022024108200ULL, 0x810018200204102ULL, 0x4002801a02003ULL, + 0x85040820080400ULL, 0x810102c808880400ULL, 0xe900410884800ULL, 0x8002020480840102ULL, + 0x220200865090201ULL, 0x2010100a02021202ULL, 0x152048408022401ULL, 0x20080002081110ULL, + 0x4001001021004000ULL, 0x800040400a011002ULL, 0xe4004081011002ULL, 0x1c004001012080ULL, + 0x8004200962a00220ULL, 0x8422100208500202ULL, 0x2000402200300c08ULL, 0x8646020080080080ULL, + 0x80020a0200100808ULL, 0x2010004880111000ULL, 0x623000a080011400ULL, 0x42008c0340209202ULL, + 0x209188240001000ULL, 0x400408a884001800ULL, 0x110400a6080400ULL, 0x1840060a44020800ULL, + 0x90080104000041ULL, 0x201011000808101ULL, 0x1a2208080504f080ULL, 0x8012020600211212ULL, + 0x500861011240000ULL, 0x180806108200800ULL, 0x4000020e01040044ULL, 0x300000261044000aULL, + 0x802241102020002ULL, 0x20906061210001ULL, 0x5a84841004010310ULL, 0x4010801011c04ULL, + 0xa010109502200ULL, 0x4a02012000ULL, 0x500201010098b028ULL, 0x8040002811040900ULL, + 0x28000010020204ULL, 0x6000020202d0240ULL, 0x8918844842082200ULL, 0x4010011029020020ULL}; static inline Bitboard RookAttacks[0x19000] = {}; static inline Bitboard BishopAttacks[0x1480] = {}; @@ -1087,12 +1191,13 @@ class attacks { static inline Magic BishopTable[64] = {}; public: - static constexpr Bitboard MASK_RANK[8] = {0xff, 0xff00, 0xff0000, 0xff000000, - 0xff00000000, 0xff0000000000, 0xff000000000000, 0xff00000000000000}; + static constexpr Bitboard MASK_RANK[8] = { + 0xff, 0xff00, 0xff0000, 0xff000000, + 0xff00000000, 0xff0000000000, 0xff000000000000, 0xff00000000000000}; static constexpr Bitboard MASK_FILE[8] = { - 0x101010101010101, 0x202020202020202, 0x404040404040404, 0x808080808080808, - 0x1010101010101010, 0x2020202020202020, 0x4040404040404040, 0x8080808080808080, + 0x101010101010101, 0x202020202020202, 0x404040404040404, 0x808080808080808, + 0x1010101010101010, 0x2020202020202020, 0x4040404040404040, 0x8080808080808080, }; /** @@ -1101,7 +1206,7 @@ class attacks { * @param b * @return */ - template + template [[nodiscard]] static constexpr Bitboard shift(const Bitboard b); /** @@ -1110,7 +1215,7 @@ class attacks { * @param pawns * @return */ - template + template [[nodiscard]] static Bitboard pawnLeftAttacks(const Bitboard pawns); /** @@ -1119,7 +1224,7 @@ class attacks { * @param pawns * @return */ - template + template [[nodiscard]] static Bitboard pawnRightAttacks(const Bitboard pawns); /** @@ -1169,13 +1274,14 @@ class attacks { [[nodiscard]] static Bitboard king(Square sq) noexcept; /** - * @brief Returns the attacks for a given piece on a given square + * @brief Returns the origin squares of pieces of a given color attacking a target square * @param board - * @param color - * @param square + * @param color Attacker Color + * @param square Attacked Square * @return */ - [[nodiscard]] static Bitboard attackers(const Board &board, Color color, Square square) noexcept; + [[nodiscard]] static Bitboard + attackers(const Board& board, Color color, Square square) noexcept; /** * @brief Returns the slider attacks for a given square @@ -1184,7 +1290,7 @@ class attacks { * @tparam pt * @return */ - template + template [[nodiscard]] static Bitboard slider(Square sq, Bitboard occupied) noexcept; /** @@ -1200,29 +1306,29 @@ class attacks { // check if charconv header is available #if __has_include() -# define CHESS_USE_CHARCONV -# include + #define CHESS_USE_CHARCONV + #include #else -# include + #include #endif - namespace chess::constants { constexpr Bitboard DEFAULT_CHECKMASK = Bitboard(0xFFFFFFFFFFFFFFFFull); -constexpr auto STARTPOS = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; -constexpr auto MAX_MOVES = 256; +constexpr auto STARTPOS = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +constexpr auto MAX_MOVES = 256; } // namespace chess::constants - namespace chess { class Move { public: Move() = default; - constexpr Move(std::uint16_t move) : move_(move), score_(0) {} + constexpr Move(std::uint16_t move) : + move_(move), + score_(0) {} /** * @brief Creates a move from a source and target square. @@ -1232,11 +1338,13 @@ class Move { * @param pt leave this empty if it is not a promotion move, otherwise pass the piece type of the new piece. * @return */ - template - [[nodiscard]] static constexpr Move make(Square source, Square target, PieceType pt = PieceType::KNIGHT) noexcept { + template + [[nodiscard]] static constexpr Move + make(Square source, Square target, PieceType pt = PieceType::KNIGHT) noexcept { assert(pt >= PieceType(PieceType::KNIGHT) && pt <= PieceType(PieceType::QUEEN)); - std::uint16_t bits_promotion = static_cast(pt - PieceType(PieceType::KNIGHT)); + std::uint16_t bits_promotion = + static_cast(pt - PieceType(PieceType::KNIGHT)); return Move(MoveType + (bits_promotion << 12) + (source.index() << 6) + target.index()); } @@ -1245,7 +1353,9 @@ class Move { * @brief Get the source square of the move. * @return */ - [[nodiscard]] constexpr Square from() const noexcept { return static_cast((move_ >> 6) & 0x3F); } + [[nodiscard]] constexpr Square from() const noexcept { + return static_cast((move_ >> 6) & 0x3F); + } /** * @brief Get the target square of the move. @@ -1266,7 +1376,8 @@ class Move { * @return */ [[nodiscard]] constexpr PieceType promotionType() const noexcept { - return static_cast(((move_ >> 12) & 3) + PieceType(PieceType::KNIGHT)); + return static_cast(((move_ >> 12) & 3) + + PieceType(PieceType::KNIGHT)); } /** @@ -1276,10 +1387,10 @@ class Move { constexpr void setScore(std::int16_t score) noexcept { score_ = score; } [[nodiscard]] constexpr std::uint16_t move() const noexcept { return move_; } - [[nodiscard]] constexpr std::int16_t score() const noexcept { return score_; } + [[nodiscard]] constexpr std::int16_t score() const noexcept { return score_; } - constexpr bool operator==(const Move &rhs) const noexcept { return move_ == rhs.move_; } - constexpr bool operator!=(const Move &rhs) const noexcept { return move_ != rhs.move_; } + constexpr bool operator==(const Move& rhs) const noexcept { return move_ == rhs.move_; } + constexpr bool operator!=(const Move& rhs) const noexcept { return move_ != rhs.move_; } static constexpr std::uint16_t NO_MOVE = 0; static constexpr std::uint16_t NULL_MOVE = 65; @@ -1290,25 +1401,12 @@ class Move { private: std::uint16_t move_; - std::int16_t score_; + std::int16_t score_; }; -inline std::ostream &operator<<(std::ostream &os, const Move &move) { - Square from_sq = move.from(); - Square to_sq = move.to(); - - os << from_sq << to_sq; - - if (move.typeOf() == Move::PROMOTION) { - os << static_cast(move.promotionType()); - } - - return os; -} } // namespace chess - #include #include #include @@ -1334,40 +1432,32 @@ class Movelist { // Element access [[nodiscard]] constexpr reference at(size_type pos) { -#ifndef CHESS_NO_EXCEPTIONS - if (pos >= size_) { - throw std::out_of_range("Movelist::at: pos (which is " + std::to_string(pos) + ") >= size (which is " + - std::to_string(size_) + ")"); - } -#endif + // pos out-of-range ignored return moves_[pos]; } [[nodiscard]] constexpr const_reference at(size_type pos) const { -#ifndef CHESS_NO_EXCEPTIONS - if (pos >= size_) { - throw std::out_of_range("Movelist::at: pos (which is " + std::to_string(pos) + ") >= size (which is " + - std::to_string(size_) + ")"); - } -#endif + // pos out-of-range ignored return moves_[pos]; } [[nodiscard]] constexpr reference operator[](size_type pos) noexcept { return moves_[pos]; } - [[nodiscard]] constexpr const_reference operator[](size_type pos) const noexcept { return moves_[pos]; } + [[nodiscard]] constexpr const_reference operator[](size_type pos) const noexcept { + return moves_[pos]; + } - [[nodiscard]] constexpr reference front() noexcept { return moves_[0]; } + [[nodiscard]] constexpr reference front() noexcept { return moves_[0]; } [[nodiscard]] constexpr const_reference front() const noexcept { return moves_[0]; } - [[nodiscard]] constexpr reference back() noexcept { return moves_[size_ - 1]; } + [[nodiscard]] constexpr reference back() noexcept { return moves_[size_ - 1]; } [[nodiscard]] constexpr const_reference back() const noexcept { return moves_[size_ - 1]; } // Iterators - [[nodiscard]] constexpr iterator begin() noexcept { return &moves_[0]; } + [[nodiscard]] constexpr iterator begin() noexcept { return &moves_[0]; } [[nodiscard]] constexpr const_iterator begin() const noexcept { return &moves_[0]; } - [[nodiscard]] constexpr iterator end() noexcept { return &moves_[0] + size_; } + [[nodiscard]] constexpr iterator end() noexcept { return &moves_[0] + size_; } [[nodiscard]] constexpr const_iterator end() const noexcept { return &moves_[0] + size_; } // Capacity @@ -1416,9 +1506,12 @@ class Movelist { * @param move * @return */ - [[nodiscard]] [[deprecated("Use std::find() instead.")]] constexpr size_type find(value_type move) const noexcept { - for (size_type i = 0; i < size_; ++i) { - if (moves_[i] == move) { + [[nodiscard]] [[deprecated("Use std::find() instead.")]] constexpr size_type + find(value_type move) const noexcept { + for (size_type i = 0; i < size_; ++i) + { + if (moves_[i] == move) + { return i; } } @@ -1428,7 +1521,7 @@ class Movelist { private: std::array moves_; - size_type size_ = 0; + size_type size_ = 0; }; } // namespace chess @@ -1446,7 +1539,11 @@ class Board; class movegen { public: - enum class MoveGenType : std::uint8_t { ALL, CAPTURE, QUIET }; + enum class MoveGenType : std::uint8_t { + ALL, + CAPTURE, + QUIET + }; /** * @brief Generates all legal moves for a position. @@ -1455,36 +1552,47 @@ class movegen { * @param board * @param pieces */ - template - void static legalmoves(Movelist &movelist, const Board &board, - int pieces = PieceGenType::PAWN | PieceGenType::KNIGHT | PieceGenType::BISHOP | - PieceGenType::ROOK | PieceGenType::QUEEN | PieceGenType::KING); + template + void static legalmoves(Movelist& movelist, + const Board& board, + int pieces = PieceGenType::PAWN | PieceGenType::KNIGHT + | PieceGenType::BISHOP | PieceGenType::ROOK + | PieceGenType::QUEEN | PieceGenType::KING); private: - static auto init_squares_between(); + static auto init_squares_between(); static const std::array, 64> SQUARES_BETWEEN_BB; // Generate the checkmask. Returns a bitboard where the attacker path between the king and enemy piece is set. - template - [[nodiscard]] static std::pair checkMask(const Board &board, Square sq); + template + [[nodiscard]] static std::pair checkMask(const Board& board, Square sq); // Generate the pin mask for horizontal and vertical pins -> PieceType::ROOK // Generate the pin mask for diagonal pins. -> PieceType::BISHOP // Returns a bitboard where the ray between the king and the pinner is set. - template - [[nodiscard]] static Bitboard pinMask(const Board &board, Square sq, Bitboard occ_enemy, Bitboard occ_us) noexcept; + template + [[nodiscard]] static Bitboard + pinMask(const Board& board, Square sq, Bitboard occ_enemy, Bitboard occ_us) noexcept; // Returns the squares that are attacked by the enemy - template - [[nodiscard]] static Bitboard seenSquares(const Board &board, Bitboard enemy_empty); + template + [[nodiscard]] static Bitboard seenSquares(const Board& board, Bitboard enemy_empty); // Generate pawn moves. - template - static void generatePawnMoves(const Board &board, Movelist &moves, Bitboard pin_d, Bitboard pin_hv, - Bitboard checkmask, Bitboard occ_enemy); - - [[nodiscard]] static std::array generateEPMove(const Board &board, Bitboard checkmask, Bitboard pin_d, - Bitboard pawns_lr, Square ep, Color c); + template + static void generatePawnMoves(const Board& board, + Movelist& moves, + Bitboard pin_d, + Bitboard pin_hv, + Bitboard checkmask, + Bitboard occ_enemy); + + [[nodiscard]] static std::array generateEPMove(const Board& board, + Bitboard checkmask, + Bitboard pin_d, + Bitboard pawns_lr, + Square ep, + Color c); [[nodiscard]] static Bitboard generateKnightMoves(Square sq); @@ -1492,21 +1600,24 @@ class movegen { [[nodiscard]] static Bitboard generateRookMoves(Square sq, Bitboard pin_hv, Bitboard occ_all); - [[nodiscard]] static Bitboard generateQueenMoves(Square sq, Bitboard pin_d, Bitboard pin_hv, Bitboard occ_all); + [[nodiscard]] static Bitboard + generateQueenMoves(Square sq, Bitboard pin_d, Bitboard pin_hv, Bitboard occ_all); - [[nodiscard]] static Bitboard generateKingMoves(Square sq, Bitboard seen, Bitboard movable_square); + [[nodiscard]] static Bitboard + generateKingMoves(Square sq, Bitboard seen, Bitboard movable_square); - template - [[nodiscard]] static Bitboard generateCastleMoves(const Board &board, Square sq, Bitboard seen, Bitboard pinHV) noexcept; + template + [[nodiscard]] static Bitboard + generateCastleMoves(const Board& board, Square sq, Bitboard seen, Bitboard pinHV) noexcept; - template - static void whileBitboardAdd(Movelist &movelist, Bitboard mask, T func); + template + static void whileBitboardAdd(Movelist& movelist, Bitboard mask, T func); - template - static void legalmoves(Movelist &movelist, const Board &board, int pieces); + template + static void legalmoves(Movelist& movelist, const Board& board, int pieces); - template - static bool isEpSquareValid(const Board &board, Square ep); + template + static bool isEpSquareValid(const Board& board, Square ep); [[nodiscard]] static Bitboard between(Square sq1, Square sq2) noexcept; @@ -1516,168 +1627,206 @@ class movegen { } // namespace chess - namespace chess { class Zobrist { using U64 = std::uint64_t; static constexpr U64 RANDOM_ARRAY[781] = { - 0x9D39247E33776D41, 0x2AF7398005AAA5C7, 0x44DB015024623547, 0x9C15F73E62A76AE2, 0x75834465489C0C89, - 0x3290AC3A203001BF, 0x0FBBAD1F61042279, 0xE83A908FF2FB60CA, 0x0D7E765D58755C10, 0x1A083822CEAFE02D, - 0x9605D5F0E25EC3B0, 0xD021FF5CD13A2ED5, 0x40BDF15D4A672E32, 0x011355146FD56395, 0x5DB4832046F3D9E5, - 0x239F8B2D7FF719CC, 0x05D1A1AE85B49AA1, 0x679F848F6E8FC971, 0x7449BBFF801FED0B, 0x7D11CDB1C3B7ADF0, - 0x82C7709E781EB7CC, 0xF3218F1C9510786C, 0x331478F3AF51BBE6, 0x4BB38DE5E7219443, 0xAA649C6EBCFD50FC, - 0x8DBD98A352AFD40B, 0x87D2074B81D79217, 0x19F3C751D3E92AE1, 0xB4AB30F062B19ABF, 0x7B0500AC42047AC4, - 0xC9452CA81A09D85D, 0x24AA6C514DA27500, 0x4C9F34427501B447, 0x14A68FD73C910841, 0xA71B9B83461CBD93, - 0x03488B95B0F1850F, 0x637B2B34FF93C040, 0x09D1BC9A3DD90A94, 0x3575668334A1DD3B, 0x735E2B97A4C45A23, - 0x18727070F1BD400B, 0x1FCBACD259BF02E7, 0xD310A7C2CE9B6555, 0xBF983FE0FE5D8244, 0x9F74D14F7454A824, - 0x51EBDC4AB9BA3035, 0x5C82C505DB9AB0FA, 0xFCF7FE8A3430B241, 0x3253A729B9BA3DDE, 0x8C74C368081B3075, - 0xB9BC6C87167C33E7, 0x7EF48F2B83024E20, 0x11D505D4C351BD7F, 0x6568FCA92C76A243, 0x4DE0B0F40F32A7B8, - 0x96D693460CC37E5D, 0x42E240CB63689F2F, 0x6D2BDCDAE2919661, 0x42880B0236E4D951, 0x5F0F4A5898171BB6, - 0x39F890F579F92F88, 0x93C5B5F47356388B, 0x63DC359D8D231B78, 0xEC16CA8AEA98AD76, 0x5355F900C2A82DC7, - 0x07FB9F855A997142, 0x5093417AA8A7ED5E, 0x7BCBC38DA25A7F3C, 0x19FC8A768CF4B6D4, 0x637A7780DECFC0D9, - 0x8249A47AEE0E41F7, 0x79AD695501E7D1E8, 0x14ACBAF4777D5776, 0xF145B6BECCDEA195, 0xDABF2AC8201752FC, - 0x24C3C94DF9C8D3F6, 0xBB6E2924F03912EA, 0x0CE26C0B95C980D9, 0xA49CD132BFBF7CC4, 0xE99D662AF4243939, - 0x27E6AD7891165C3F, 0x8535F040B9744FF1, 0x54B3F4FA5F40D873, 0x72B12C32127FED2B, 0xEE954D3C7B411F47, - 0x9A85AC909A24EAA1, 0x70AC4CD9F04F21F5, 0xF9B89D3E99A075C2, 0x87B3E2B2B5C907B1, 0xA366E5B8C54F48B8, - 0xAE4A9346CC3F7CF2, 0x1920C04D47267BBD, 0x87BF02C6B49E2AE9, 0x092237AC237F3859, 0xFF07F64EF8ED14D0, - 0x8DE8DCA9F03CC54E, 0x9C1633264DB49C89, 0xB3F22C3D0B0B38ED, 0x390E5FB44D01144B, 0x5BFEA5B4712768E9, - 0x1E1032911FA78984, 0x9A74ACB964E78CB3, 0x4F80F7A035DAFB04, 0x6304D09A0B3738C4, 0x2171E64683023A08, - 0x5B9B63EB9CEFF80C, 0x506AACF489889342, 0x1881AFC9A3A701D6, 0x6503080440750644, 0xDFD395339CDBF4A7, - 0xEF927DBCF00C20F2, 0x7B32F7D1E03680EC, 0xB9FD7620E7316243, 0x05A7E8A57DB91B77, 0xB5889C6E15630A75, - 0x4A750A09CE9573F7, 0xCF464CEC899A2F8A, 0xF538639CE705B824, 0x3C79A0FF5580EF7F, 0xEDE6C87F8477609D, - 0x799E81F05BC93F31, 0x86536B8CF3428A8C, 0x97D7374C60087B73, 0xA246637CFF328532, 0x043FCAE60CC0EBA0, - 0x920E449535DD359E, 0x70EB093B15B290CC, 0x73A1921916591CBD, 0x56436C9FE1A1AA8D, 0xEFAC4B70633B8F81, - 0xBB215798D45DF7AF, 0x45F20042F24F1768, 0x930F80F4E8EB7462, 0xFF6712FFCFD75EA1, 0xAE623FD67468AA70, - 0xDD2C5BC84BC8D8FC, 0x7EED120D54CF2DD9, 0x22FE545401165F1C, 0xC91800E98FB99929, 0x808BD68E6AC10365, - 0xDEC468145B7605F6, 0x1BEDE3A3AEF53302, 0x43539603D6C55602, 0xAA969B5C691CCB7A, 0xA87832D392EFEE56, - 0x65942C7B3C7E11AE, 0xDED2D633CAD004F6, 0x21F08570F420E565, 0xB415938D7DA94E3C, 0x91B859E59ECB6350, - 0x10CFF333E0ED804A, 0x28AED140BE0BB7DD, 0xC5CC1D89724FA456, 0x5648F680F11A2741, 0x2D255069F0B7DAB3, - 0x9BC5A38EF729ABD4, 0xEF2F054308F6A2BC, 0xAF2042F5CC5C2858, 0x480412BAB7F5BE2A, 0xAEF3AF4A563DFE43, - 0x19AFE59AE451497F, 0x52593803DFF1E840, 0xF4F076E65F2CE6F0, 0x11379625747D5AF3, 0xBCE5D2248682C115, - 0x9DA4243DE836994F, 0x066F70B33FE09017, 0x4DC4DE189B671A1C, 0x51039AB7712457C3, 0xC07A3F80C31FB4B4, - 0xB46EE9C5E64A6E7C, 0xB3819A42ABE61C87, 0x21A007933A522A20, 0x2DF16F761598AA4F, 0x763C4A1371B368FD, - 0xF793C46702E086A0, 0xD7288E012AEB8D31, 0xDE336A2A4BC1C44B, 0x0BF692B38D079F23, 0x2C604A7A177326B3, - 0x4850E73E03EB6064, 0xCFC447F1E53C8E1B, 0xB05CA3F564268D99, 0x9AE182C8BC9474E8, 0xA4FC4BD4FC5558CA, - 0xE755178D58FC4E76, 0x69B97DB1A4C03DFE, 0xF9B5B7C4ACC67C96, 0xFC6A82D64B8655FB, 0x9C684CB6C4D24417, - 0x8EC97D2917456ED0, 0x6703DF9D2924E97E, 0xC547F57E42A7444E, 0x78E37644E7CAD29E, 0xFE9A44E9362F05FA, - 0x08BD35CC38336615, 0x9315E5EB3A129ACE, 0x94061B871E04DF75, 0xDF1D9F9D784BA010, 0x3BBA57B68871B59D, - 0xD2B7ADEEDED1F73F, 0xF7A255D83BC373F8, 0xD7F4F2448C0CEB81, 0xD95BE88CD210FFA7, 0x336F52F8FF4728E7, - 0xA74049DAC312AC71, 0xA2F61BB6E437FDB5, 0x4F2A5CB07F6A35B3, 0x87D380BDA5BF7859, 0x16B9F7E06C453A21, - 0x7BA2484C8A0FD54E, 0xF3A678CAD9A2E38C, 0x39B0BF7DDE437BA2, 0xFCAF55C1BF8A4424, 0x18FCF680573FA594, - 0x4C0563B89F495AC3, 0x40E087931A00930D, 0x8CFFA9412EB642C1, 0x68CA39053261169F, 0x7A1EE967D27579E2, - 0x9D1D60E5076F5B6F, 0x3810E399B6F65BA2, 0x32095B6D4AB5F9B1, 0x35CAB62109DD038A, 0xA90B24499FCFAFB1, - 0x77A225A07CC2C6BD, 0x513E5E634C70E331, 0x4361C0CA3F692F12, 0xD941ACA44B20A45B, 0x528F7C8602C5807B, - 0x52AB92BEB9613989, 0x9D1DFA2EFC557F73, 0x722FF175F572C348, 0x1D1260A51107FE97, 0x7A249A57EC0C9BA2, - 0x04208FE9E8F7F2D6, 0x5A110C6058B920A0, 0x0CD9A497658A5698, 0x56FD23C8F9715A4C, 0x284C847B9D887AAE, - 0x04FEABFBBDB619CB, 0x742E1E651C60BA83, 0x9A9632E65904AD3C, 0x881B82A13B51B9E2, 0x506E6744CD974924, - 0xB0183DB56FFC6A79, 0x0ED9B915C66ED37E, 0x5E11E86D5873D484, 0xF678647E3519AC6E, 0x1B85D488D0F20CC5, - 0xDAB9FE6525D89021, 0x0D151D86ADB73615, 0xA865A54EDCC0F019, 0x93C42566AEF98FFB, 0x99E7AFEABE000731, - 0x48CBFF086DDF285A, 0x7F9B6AF1EBF78BAF, 0x58627E1A149BBA21, 0x2CD16E2ABD791E33, 0xD363EFF5F0977996, - 0x0CE2A38C344A6EED, 0x1A804AADB9CFA741, 0x907F30421D78C5DE, 0x501F65EDB3034D07, 0x37624AE5A48FA6E9, - 0x957BAF61700CFF4E, 0x3A6C27934E31188A, 0xD49503536ABCA345, 0x088E049589C432E0, 0xF943AEE7FEBF21B8, - 0x6C3B8E3E336139D3, 0x364F6FFA464EE52E, 0xD60F6DCEDC314222, 0x56963B0DCA418FC0, 0x16F50EDF91E513AF, - 0xEF1955914B609F93, 0x565601C0364E3228, 0xECB53939887E8175, 0xBAC7A9A18531294B, 0xB344C470397BBA52, - 0x65D34954DAF3CEBD, 0xB4B81B3FA97511E2, 0xB422061193D6F6A7, 0x071582401C38434D, 0x7A13F18BBEDC4FF5, - 0xBC4097B116C524D2, 0x59B97885E2F2EA28, 0x99170A5DC3115544, 0x6F423357E7C6A9F9, 0x325928EE6E6F8794, - 0xD0E4366228B03343, 0x565C31F7DE89EA27, 0x30F5611484119414, 0xD873DB391292ED4F, 0x7BD94E1D8E17DEBC, - 0xC7D9F16864A76E94, 0x947AE053EE56E63C, 0xC8C93882F9475F5F, 0x3A9BF55BA91F81CA, 0xD9A11FBB3D9808E4, - 0x0FD22063EDC29FCA, 0xB3F256D8ACA0B0B9, 0xB03031A8B4516E84, 0x35DD37D5871448AF, 0xE9F6082B05542E4E, - 0xEBFAFA33D7254B59, 0x9255ABB50D532280, 0xB9AB4CE57F2D34F3, 0x693501D628297551, 0xC62C58F97DD949BF, - 0xCD454F8F19C5126A, 0xBBE83F4ECC2BDECB, 0xDC842B7E2819E230, 0xBA89142E007503B8, 0xA3BC941D0A5061CB, - 0xE9F6760E32CD8021, 0x09C7E552BC76492F, 0x852F54934DA55CC9, 0x8107FCCF064FCF56, 0x098954D51FFF6580, - 0x23B70EDB1955C4BF, 0xC330DE426430F69D, 0x4715ED43E8A45C0A, 0xA8D7E4DAB780A08D, 0x0572B974F03CE0BB, - 0xB57D2E985E1419C7, 0xE8D9ECBE2CF3D73F, 0x2FE4B17170E59750, 0x11317BA87905E790, 0x7FBF21EC8A1F45EC, - 0x1725CABFCB045B00, 0x964E915CD5E2B207, 0x3E2B8BCBF016D66D, 0xBE7444E39328A0AC, 0xF85B2B4FBCDE44B7, - 0x49353FEA39BA63B1, 0x1DD01AAFCD53486A, 0x1FCA8A92FD719F85, 0xFC7C95D827357AFA, 0x18A6A990C8B35EBD, - 0xCCCB7005C6B9C28D, 0x3BDBB92C43B17F26, 0xAA70B5B4F89695A2, 0xE94C39A54A98307F, 0xB7A0B174CFF6F36E, - 0xD4DBA84729AF48AD, 0x2E18BC1AD9704A68, 0x2DE0966DAF2F8B1C, 0xB9C11D5B1E43A07E, 0x64972D68DEE33360, - 0x94628D38D0C20584, 0xDBC0D2B6AB90A559, 0xD2733C4335C6A72F, 0x7E75D99D94A70F4D, 0x6CED1983376FA72B, - 0x97FCAACBF030BC24, 0x7B77497B32503B12, 0x8547EDDFB81CCB94, 0x79999CDFF70902CB, 0xCFFE1939438E9B24, - 0x829626E3892D95D7, 0x92FAE24291F2B3F1, 0x63E22C147B9C3403, 0xC678B6D860284A1C, 0x5873888850659AE7, - 0x0981DCD296A8736D, 0x9F65789A6509A440, 0x9FF38FED72E9052F, 0xE479EE5B9930578C, 0xE7F28ECD2D49EECD, - 0x56C074A581EA17FE, 0x5544F7D774B14AEF, 0x7B3F0195FC6F290F, 0x12153635B2C0CF57, 0x7F5126DBBA5E0CA7, - 0x7A76956C3EAFB413, 0x3D5774A11D31AB39, 0x8A1B083821F40CB4, 0x7B4A38E32537DF62, 0x950113646D1D6E03, - 0x4DA8979A0041E8A9, 0x3BC36E078F7515D7, 0x5D0A12F27AD310D1, 0x7F9D1A2E1EBE1327, 0xDA3A361B1C5157B1, - 0xDCDD7D20903D0C25, 0x36833336D068F707, 0xCE68341F79893389, 0xAB9090168DD05F34, 0x43954B3252DC25E5, - 0xB438C2B67F98E5E9, 0x10DCD78E3851A492, 0xDBC27AB5447822BF, 0x9B3CDB65F82CA382, 0xB67B7896167B4C84, - 0xBFCED1B0048EAC50, 0xA9119B60369FFEBD, 0x1FFF7AC80904BF45, 0xAC12FB171817EEE7, 0xAF08DA9177DDA93D, - 0x1B0CAB936E65C744, 0xB559EB1D04E5E932, 0xC37B45B3F8D6F2BA, 0xC3A9DC228CAAC9E9, 0xF3B8B6675A6507FF, - 0x9FC477DE4ED681DA, 0x67378D8ECCEF96CB, 0x6DD856D94D259236, 0xA319CE15B0B4DB31, 0x073973751F12DD5E, - 0x8A8E849EB32781A5, 0xE1925C71285279F5, 0x74C04BF1790C0EFE, 0x4DDA48153C94938A, 0x9D266D6A1CC0542C, - 0x7440FB816508C4FE, 0x13328503DF48229F, 0xD6BF7BAEE43CAC40, 0x4838D65F6EF6748F, 0x1E152328F3318DEA, - 0x8F8419A348F296BF, 0x72C8834A5957B511, 0xD7A023A73260B45C, 0x94EBC8ABCFB56DAE, 0x9FC10D0F989993E0, - 0xDE68A2355B93CAE6, 0xA44CFE79AE538BBE, 0x9D1D84FCCE371425, 0x51D2B1AB2DDFB636, 0x2FD7E4B9E72CD38C, - 0x65CA5B96B7552210, 0xDD69A0D8AB3B546D, 0x604D51B25FBF70E2, 0x73AA8A564FB7AC9E, 0x1A8C1E992B941148, - 0xAAC40A2703D9BEA0, 0x764DBEAE7FA4F3A6, 0x1E99B96E70A9BE8B, 0x2C5E9DEB57EF4743, 0x3A938FEE32D29981, - 0x26E6DB8FFDF5ADFE, 0x469356C504EC9F9D, 0xC8763C5B08D1908C, 0x3F6C6AF859D80055, 0x7F7CC39420A3A545, - 0x9BFB227EBDF4C5CE, 0x89039D79D6FC5C5C, 0x8FE88B57305E2AB6, 0xA09E8C8C35AB96DE, 0xFA7E393983325753, - 0xD6B6D0ECC617C699, 0xDFEA21EA9E7557E3, 0xB67C1FA481680AF8, 0xCA1E3785A9E724E5, 0x1CFC8BED0D681639, - 0xD18D8549D140CAEA, 0x4ED0FE7E9DC91335, 0xE4DBF0634473F5D2, 0x1761F93A44D5AEFE, 0x53898E4C3910DA55, - 0x734DE8181F6EC39A, 0x2680B122BAA28D97, 0x298AF231C85BAFAB, 0x7983EED3740847D5, 0x66C1A2A1A60CD889, - 0x9E17E49642A3E4C1, 0xEDB454E7BADC0805, 0x50B704CAB602C329, 0x4CC317FB9CDDD023, 0x66B4835D9EAFEA22, - 0x219B97E26FFC81BD, 0x261E4E4C0A333A9D, 0x1FE2CCA76517DB90, 0xD7504DFA8816EDBB, 0xB9571FA04DC089C8, - 0x1DDC0325259B27DE, 0xCF3F4688801EB9AA, 0xF4F5D05C10CAB243, 0x38B6525C21A42B0E, 0x36F60E2BA4FA6800, - 0xEB3593803173E0CE, 0x9C4CD6257C5A3603, 0xAF0C317D32ADAA8A, 0x258E5A80C7204C4B, 0x8B889D624D44885D, - 0xF4D14597E660F855, 0xD4347F66EC8941C3, 0xE699ED85B0DFB40D, 0x2472F6207C2D0484, 0xC2A1E7B5B459AEB5, - 0xAB4F6451CC1D45EC, 0x63767572AE3D6174, 0xA59E0BD101731A28, 0x116D0016CB948F09, 0x2CF9C8CA052F6E9F, - 0x0B090A7560A968E3, 0xABEEDDB2DDE06FF1, 0x58EFC10B06A2068D, 0xC6E57A78FBD986E0, 0x2EAB8CA63CE802D7, - 0x14A195640116F336, 0x7C0828DD624EC390, 0xD74BBE77E6116AC7, 0x804456AF10F5FB53, 0xEBE9EA2ADF4321C7, - 0x03219A39EE587A30, 0x49787FEF17AF9924, 0xA1E9300CD8520548, 0x5B45E522E4B1B4EF, 0xB49C3B3995091A36, - 0xD4490AD526F14431, 0x12A8F216AF9418C2, 0x001F837CC7350524, 0x1877B51E57A764D5, 0xA2853B80F17F58EE, - 0x993E1DE72D36D310, 0xB3598080CE64A656, 0x252F59CF0D9F04BB, 0xD23C8E176D113600, 0x1BDA0492E7E4586E, - 0x21E0BD5026C619BF, 0x3B097ADAF088F94E, 0x8D14DEDB30BE846E, 0xF95CFFA23AF5F6F4, 0x3871700761B3F743, - 0xCA672B91E9E4FA16, 0x64C8E531BFF53B55, 0x241260ED4AD1E87D, 0x106C09B972D2E822, 0x7FBA195410E5CA30, - 0x7884D9BC6CB569D8, 0x0647DFEDCD894A29, 0x63573FF03E224774, 0x4FC8E9560F91B123, 0x1DB956E450275779, - 0xB8D91274B9E9D4FB, 0xA2EBEE47E2FBFCE1, 0xD9F1F30CCD97FB09, 0xEFED53D75FD64E6B, 0x2E6D02C36017F67F, - 0xA9AA4D20DB084E9B, 0xB64BE8D8B25396C1, 0x70CB6AF7C2D5BCF0, 0x98F076A4F7A2322E, 0xBF84470805E69B5F, - 0x94C3251F06F90CF3, 0x3E003E616A6591E9, 0xB925A6CD0421AFF3, 0x61BDD1307C66E300, 0xBF8D5108E27E0D48, - 0x240AB57A8B888B20, 0xFC87614BAF287E07, 0xEF02CDD06FFDB432, 0xA1082C0466DF6C0A, 0x8215E577001332C8, - 0xD39BB9C3A48DB6CF, 0x2738259634305C14, 0x61CF4F94C97DF93D, 0x1B6BACA2AE4E125B, 0x758F450C88572E0B, - 0x959F587D507A8359, 0xB063E962E045F54D, 0x60E8ED72C0DFF5D1, 0x7B64978555326F9F, 0xFD080D236DA814BA, - 0x8C90FD9B083F4558, 0x106F72FE81E2C590, 0x7976033A39F7D952, 0xA4EC0132764CA04B, 0x733EA705FAE4FA77, - 0xB4D8F77BC3E56167, 0x9E21F4F903B33FD9, 0x9D765E419FB69F6D, 0xD30C088BA61EA5EF, 0x5D94337FBFAF7F5B, - 0x1A4E4822EB4D7A59, 0x6FFE73E81B637FB3, 0xDDF957BC36D8B9CA, 0x64D0E29EEA8838B3, 0x08DD9BDFD96B9F63, - 0x087E79E5A57D1D13, 0xE328E230E3E2B3FB, 0x1C2559E30F0946BE, 0x720BF5F26F4D2EAA, 0xB0774D261CC609DB, - 0x443F64EC5A371195, 0x4112CF68649A260E, 0xD813F2FAB7F5C5CA, 0x660D3257380841EE, 0x59AC2C7873F910A3, - 0xE846963877671A17, 0x93B633ABFA3469F8, 0xC0C0F5A60EF4CDCF, 0xCAF21ECD4377B28C, 0x57277707199B8175, - 0x506C11B9D90E8B1D, 0xD83CC2687A19255F, 0x4A29C6465A314CD1, 0xED2DF21216235097, 0xB5635C95FF7296E2, - 0x22AF003AB672E811, 0x52E762596BF68235, 0x9AEBA33AC6ECC6B0, 0x944F6DE09134DFB6, 0x6C47BEC883A7DE39, - 0x6AD047C430A12104, 0xA5B1CFDBA0AB4067, 0x7C45D833AFF07862, 0x5092EF950A16DA0B, 0x9338E69C052B8E7B, - 0x455A4B4CFE30E3F5, 0x6B02E63195AD0CF8, 0x6B17B224BAD6BF27, 0xD1E0CCD25BB9C169, 0xDE0C89A556B9AE70, - 0x50065E535A213CF6, 0x9C1169FA2777B874, 0x78EDEFD694AF1EED, 0x6DC93D9526A50E68, 0xEE97F453F06791ED, - 0x32AB0EDB696703D3, 0x3A6853C7E70757A7, 0x31865CED6120F37D, 0x67FEF95D92607890, 0x1F2B1D1F15F6DC9C, - 0xB69E38A8965C6B65, 0xAA9119FF184CCCF4, 0xF43C732873F24C13, 0xFB4A3D794A9A80D2, 0x3550C2321FD6109C, - 0x371F77E76BB8417E, 0x6BFA9AAE5EC05779, 0xCD04F3FF001A4778, 0xE3273522064480CA, 0x9F91508BFFCFC14A, - 0x049A7F41061A9E60, 0xFCB6BE43A9F2FE9B, 0x08DE8A1C7797DA9B, 0x8F9887E6078735A1, 0xB5B4071DBFC73A66, - 0x230E343DFBA08D33, 0x43ED7F5A0FAE657D, 0x3A88A0FBBCB05C63, 0x21874B8B4D2DBC4F, 0x1BDEA12E35F6A8C9, - 0x53C065C6C8E63528, 0xE34A1D250E7A8D6B, 0xD6B04D3B7651DD7E, 0x5E90277E7CB39E2D, 0x2C046F22062DC67D, - 0xB10BB459132D0A26, 0x3FA9DDFB67E2F199, 0x0E09B88E1914F7AF, 0x10E8B35AF3EEAB37, 0x9EEDECA8E272B933, - 0xD4C718BC4AE8AE5F, 0x81536D601170FC20, 0x91B534F885818A06, 0xEC8177F83F900978, 0x190E714FADA5156E, - 0xB592BF39B0364963, 0x89C350C893AE7DC1, 0xAC042E70F8B383F2, 0xB49B52E587A1EE60, 0xFB152FE3FF26DA89, - 0x3E666E6F69AE2C15, 0x3B544EBE544C19F9, 0xE805A1E290CF2456, 0x24B33C9D7ED25117, 0xE74733427B72F0C1, - 0x0A804D18B7097475, 0x57E3306D881EDB4F, 0x4AE7D6A36EB5DBCB, 0x2D8D5432157064C8, 0xD1E649DE1E7F268B, - 0x8A328A1CEDFE552C, 0x07A3AEC79624C7DA, 0x84547DDC3E203C94, 0x990A98FD5071D263, 0x1A4FF12616EEFC89, - 0xF6F7FD1431714200, 0x30C05B1BA332F41C, 0x8D2636B81555A786, 0x46C9FEB55D120902, 0xCCEC0A73B49C9921, - 0x4E9D2827355FC492, 0x19EBB029435DCB0F, 0x4659D2B743848A2C, 0x963EF2C96B33BE31, 0x74F85198B05A2E7D, - 0x5A0F544DD2B1FB18, 0x03727073C2E134B1, 0xC7F6AA2DE59AEA61, 0x352787BAA0D7C22F, 0x9853EAB63B5E0B35, - 0xABBDCDD7ED5C0860, 0xCF05DAF5AC8D77B0, 0x49CAD48CEBF4A71E, 0x7A4C10EC2158C4A6, 0xD9E92AA246BF719E, - 0x13AE978D09FE5557, 0x730499AF921549FF, 0x4E4B705B92903BA4, 0xFF577222C14F0A3A, 0x55B6344CF97AAFAE, - 0xB862225B055B6960, 0xCAC09AFBDDD2CDB4, 0xDAF8E9829FE96B5F, 0xB5FDFC5D3132C498, 0x310CB380DB6F7503, - 0xE87FBB46217A360E, 0x2102AE466EBB1148, 0xF8549E1A3AA5E00D, 0x07A69AFDCC42261A, 0xC4C118BFE78FEAAE, - 0xF9F4892ED96BD438, 0x1AF3DBE25D8F45DA, 0xF5B4B0B0D2DEEEB4, 0x962ACEEFA82E1C84, 0x046E3ECAAF453CE9, - 0xF05D129681949A4C, 0x964781CE734B3C84, 0x9C2ED44081CE5FBD, 0x522E23F3925E319E, 0x177E00F9FC32F791, - 0x2BC60A63A6F3B3F2, 0x222BBFAE61725606, 0x486289DDCC3D6780, 0x7DC7785B8EFDFC80, 0x8AF38731C02BA980, - 0x1FAB64EA29A2DDF7, 0xE4D9429322CD065A, 0x9DA058C67844F20C, 0x24C0E332B70019B0, 0x233003B5A6CFE6AD, - 0xD586BD01C5C217F6, 0x5E5637885F29BC2B, 0x7EBA726D8C94094B, 0x0A56A5F0BFE39272, 0xD79476A84EE20D06, - 0x9E4C1269BAA4BF37, 0x17EFEE45B0DEE640, 0x1D95B0A5FCF90BC6, 0x93CBE0B699C2585D, 0x65FA4F227A2B6D79, - 0xD5F9E858292504D5, 0xC2B5A03F71471A6F, 0x59300222B4561E00, 0xCE2F8642CA0712DC, 0x7CA9723FBB2E8988, - 0x2785338347F2BA08, 0xC61BB3A141E50E8C, 0x150F361DAB9DEC26, 0x9F6A419D382595F4, 0x64A53DC924FE7AC9, - 0x142DE49FFF7A7C3D, 0x0C335248857FA9E7, 0x0A9C32D5EAE45305, 0xE6C42178C4BBB92E, 0x71F1CE2490D20B07, - 0xF1BCC3D275AFE51A, 0xE728E8C83C334074, 0x96FBF83A12884624, 0x81A1549FD6573DA5, 0x5FA7867CAF35E149, - 0x56986E2EF3ED091B, 0x917F1DD5F8886C61, 0xD20D8C88C8FFE65F, 0x31D71DCE64B2C310, 0xF165B587DF898190, - 0xA57E6339DD2CF3A0, 0x1EF6E6DBB1961EC9, 0x70CC73D90BC26E24, 0xE21A6B35DF0C3AD7, 0x003A93D8B2806962, - 0x1C99DED33CB890A1, 0xCF3145DE0ADD4289, 0xD0E4427A5514FB72, 0x77C621CC9FB3A483, 0x67A34DAC4356550B, - 0xF8D626AAAF278509}; + 0x9D39247E33776D41, 0x2AF7398005AAA5C7, 0x44DB015024623547, 0x9C15F73E62A76AE2, + 0x75834465489C0C89, 0x3290AC3A203001BF, 0x0FBBAD1F61042279, 0xE83A908FF2FB60CA, + 0x0D7E765D58755C10, 0x1A083822CEAFE02D, 0x9605D5F0E25EC3B0, 0xD021FF5CD13A2ED5, + 0x40BDF15D4A672E32, 0x011355146FD56395, 0x5DB4832046F3D9E5, 0x239F8B2D7FF719CC, + 0x05D1A1AE85B49AA1, 0x679F848F6E8FC971, 0x7449BBFF801FED0B, 0x7D11CDB1C3B7ADF0, + 0x82C7709E781EB7CC, 0xF3218F1C9510786C, 0x331478F3AF51BBE6, 0x4BB38DE5E7219443, + 0xAA649C6EBCFD50FC, 0x8DBD98A352AFD40B, 0x87D2074B81D79217, 0x19F3C751D3E92AE1, + 0xB4AB30F062B19ABF, 0x7B0500AC42047AC4, 0xC9452CA81A09D85D, 0x24AA6C514DA27500, + 0x4C9F34427501B447, 0x14A68FD73C910841, 0xA71B9B83461CBD93, 0x03488B95B0F1850F, + 0x637B2B34FF93C040, 0x09D1BC9A3DD90A94, 0x3575668334A1DD3B, 0x735E2B97A4C45A23, + 0x18727070F1BD400B, 0x1FCBACD259BF02E7, 0xD310A7C2CE9B6555, 0xBF983FE0FE5D8244, + 0x9F74D14F7454A824, 0x51EBDC4AB9BA3035, 0x5C82C505DB9AB0FA, 0xFCF7FE8A3430B241, + 0x3253A729B9BA3DDE, 0x8C74C368081B3075, 0xB9BC6C87167C33E7, 0x7EF48F2B83024E20, + 0x11D505D4C351BD7F, 0x6568FCA92C76A243, 0x4DE0B0F40F32A7B8, 0x96D693460CC37E5D, + 0x42E240CB63689F2F, 0x6D2BDCDAE2919661, 0x42880B0236E4D951, 0x5F0F4A5898171BB6, + 0x39F890F579F92F88, 0x93C5B5F47356388B, 0x63DC359D8D231B78, 0xEC16CA8AEA98AD76, + 0x5355F900C2A82DC7, 0x07FB9F855A997142, 0x5093417AA8A7ED5E, 0x7BCBC38DA25A7F3C, + 0x19FC8A768CF4B6D4, 0x637A7780DECFC0D9, 0x8249A47AEE0E41F7, 0x79AD695501E7D1E8, + 0x14ACBAF4777D5776, 0xF145B6BECCDEA195, 0xDABF2AC8201752FC, 0x24C3C94DF9C8D3F6, + 0xBB6E2924F03912EA, 0x0CE26C0B95C980D9, 0xA49CD132BFBF7CC4, 0xE99D662AF4243939, + 0x27E6AD7891165C3F, 0x8535F040B9744FF1, 0x54B3F4FA5F40D873, 0x72B12C32127FED2B, + 0xEE954D3C7B411F47, 0x9A85AC909A24EAA1, 0x70AC4CD9F04F21F5, 0xF9B89D3E99A075C2, + 0x87B3E2B2B5C907B1, 0xA366E5B8C54F48B8, 0xAE4A9346CC3F7CF2, 0x1920C04D47267BBD, + 0x87BF02C6B49E2AE9, 0x092237AC237F3859, 0xFF07F64EF8ED14D0, 0x8DE8DCA9F03CC54E, + 0x9C1633264DB49C89, 0xB3F22C3D0B0B38ED, 0x390E5FB44D01144B, 0x5BFEA5B4712768E9, + 0x1E1032911FA78984, 0x9A74ACB964E78CB3, 0x4F80F7A035DAFB04, 0x6304D09A0B3738C4, + 0x2171E64683023A08, 0x5B9B63EB9CEFF80C, 0x506AACF489889342, 0x1881AFC9A3A701D6, + 0x6503080440750644, 0xDFD395339CDBF4A7, 0xEF927DBCF00C20F2, 0x7B32F7D1E03680EC, + 0xB9FD7620E7316243, 0x05A7E8A57DB91B77, 0xB5889C6E15630A75, 0x4A750A09CE9573F7, + 0xCF464CEC899A2F8A, 0xF538639CE705B824, 0x3C79A0FF5580EF7F, 0xEDE6C87F8477609D, + 0x799E81F05BC93F31, 0x86536B8CF3428A8C, 0x97D7374C60087B73, 0xA246637CFF328532, + 0x043FCAE60CC0EBA0, 0x920E449535DD359E, 0x70EB093B15B290CC, 0x73A1921916591CBD, + 0x56436C9FE1A1AA8D, 0xEFAC4B70633B8F81, 0xBB215798D45DF7AF, 0x45F20042F24F1768, + 0x930F80F4E8EB7462, 0xFF6712FFCFD75EA1, 0xAE623FD67468AA70, 0xDD2C5BC84BC8D8FC, + 0x7EED120D54CF2DD9, 0x22FE545401165F1C, 0xC91800E98FB99929, 0x808BD68E6AC10365, + 0xDEC468145B7605F6, 0x1BEDE3A3AEF53302, 0x43539603D6C55602, 0xAA969B5C691CCB7A, + 0xA87832D392EFEE56, 0x65942C7B3C7E11AE, 0xDED2D633CAD004F6, 0x21F08570F420E565, + 0xB415938D7DA94E3C, 0x91B859E59ECB6350, 0x10CFF333E0ED804A, 0x28AED140BE0BB7DD, + 0xC5CC1D89724FA456, 0x5648F680F11A2741, 0x2D255069F0B7DAB3, 0x9BC5A38EF729ABD4, + 0xEF2F054308F6A2BC, 0xAF2042F5CC5C2858, 0x480412BAB7F5BE2A, 0xAEF3AF4A563DFE43, + 0x19AFE59AE451497F, 0x52593803DFF1E840, 0xF4F076E65F2CE6F0, 0x11379625747D5AF3, + 0xBCE5D2248682C115, 0x9DA4243DE836994F, 0x066F70B33FE09017, 0x4DC4DE189B671A1C, + 0x51039AB7712457C3, 0xC07A3F80C31FB4B4, 0xB46EE9C5E64A6E7C, 0xB3819A42ABE61C87, + 0x21A007933A522A20, 0x2DF16F761598AA4F, 0x763C4A1371B368FD, 0xF793C46702E086A0, + 0xD7288E012AEB8D31, 0xDE336A2A4BC1C44B, 0x0BF692B38D079F23, 0x2C604A7A177326B3, + 0x4850E73E03EB6064, 0xCFC447F1E53C8E1B, 0xB05CA3F564268D99, 0x9AE182C8BC9474E8, + 0xA4FC4BD4FC5558CA, 0xE755178D58FC4E76, 0x69B97DB1A4C03DFE, 0xF9B5B7C4ACC67C96, + 0xFC6A82D64B8655FB, 0x9C684CB6C4D24417, 0x8EC97D2917456ED0, 0x6703DF9D2924E97E, + 0xC547F57E42A7444E, 0x78E37644E7CAD29E, 0xFE9A44E9362F05FA, 0x08BD35CC38336615, + 0x9315E5EB3A129ACE, 0x94061B871E04DF75, 0xDF1D9F9D784BA010, 0x3BBA57B68871B59D, + 0xD2B7ADEEDED1F73F, 0xF7A255D83BC373F8, 0xD7F4F2448C0CEB81, 0xD95BE88CD210FFA7, + 0x336F52F8FF4728E7, 0xA74049DAC312AC71, 0xA2F61BB6E437FDB5, 0x4F2A5CB07F6A35B3, + 0x87D380BDA5BF7859, 0x16B9F7E06C453A21, 0x7BA2484C8A0FD54E, 0xF3A678CAD9A2E38C, + 0x39B0BF7DDE437BA2, 0xFCAF55C1BF8A4424, 0x18FCF680573FA594, 0x4C0563B89F495AC3, + 0x40E087931A00930D, 0x8CFFA9412EB642C1, 0x68CA39053261169F, 0x7A1EE967D27579E2, + 0x9D1D60E5076F5B6F, 0x3810E399B6F65BA2, 0x32095B6D4AB5F9B1, 0x35CAB62109DD038A, + 0xA90B24499FCFAFB1, 0x77A225A07CC2C6BD, 0x513E5E634C70E331, 0x4361C0CA3F692F12, + 0xD941ACA44B20A45B, 0x528F7C8602C5807B, 0x52AB92BEB9613989, 0x9D1DFA2EFC557F73, + 0x722FF175F572C348, 0x1D1260A51107FE97, 0x7A249A57EC0C9BA2, 0x04208FE9E8F7F2D6, + 0x5A110C6058B920A0, 0x0CD9A497658A5698, 0x56FD23C8F9715A4C, 0x284C847B9D887AAE, + 0x04FEABFBBDB619CB, 0x742E1E651C60BA83, 0x9A9632E65904AD3C, 0x881B82A13B51B9E2, + 0x506E6744CD974924, 0xB0183DB56FFC6A79, 0x0ED9B915C66ED37E, 0x5E11E86D5873D484, + 0xF678647E3519AC6E, 0x1B85D488D0F20CC5, 0xDAB9FE6525D89021, 0x0D151D86ADB73615, + 0xA865A54EDCC0F019, 0x93C42566AEF98FFB, 0x99E7AFEABE000731, 0x48CBFF086DDF285A, + 0x7F9B6AF1EBF78BAF, 0x58627E1A149BBA21, 0x2CD16E2ABD791E33, 0xD363EFF5F0977996, + 0x0CE2A38C344A6EED, 0x1A804AADB9CFA741, 0x907F30421D78C5DE, 0x501F65EDB3034D07, + 0x37624AE5A48FA6E9, 0x957BAF61700CFF4E, 0x3A6C27934E31188A, 0xD49503536ABCA345, + 0x088E049589C432E0, 0xF943AEE7FEBF21B8, 0x6C3B8E3E336139D3, 0x364F6FFA464EE52E, + 0xD60F6DCEDC314222, 0x56963B0DCA418FC0, 0x16F50EDF91E513AF, 0xEF1955914B609F93, + 0x565601C0364E3228, 0xECB53939887E8175, 0xBAC7A9A18531294B, 0xB344C470397BBA52, + 0x65D34954DAF3CEBD, 0xB4B81B3FA97511E2, 0xB422061193D6F6A7, 0x071582401C38434D, + 0x7A13F18BBEDC4FF5, 0xBC4097B116C524D2, 0x59B97885E2F2EA28, 0x99170A5DC3115544, + 0x6F423357E7C6A9F9, 0x325928EE6E6F8794, 0xD0E4366228B03343, 0x565C31F7DE89EA27, + 0x30F5611484119414, 0xD873DB391292ED4F, 0x7BD94E1D8E17DEBC, 0xC7D9F16864A76E94, + 0x947AE053EE56E63C, 0xC8C93882F9475F5F, 0x3A9BF55BA91F81CA, 0xD9A11FBB3D9808E4, + 0x0FD22063EDC29FCA, 0xB3F256D8ACA0B0B9, 0xB03031A8B4516E84, 0x35DD37D5871448AF, + 0xE9F6082B05542E4E, 0xEBFAFA33D7254B59, 0x9255ABB50D532280, 0xB9AB4CE57F2D34F3, + 0x693501D628297551, 0xC62C58F97DD949BF, 0xCD454F8F19C5126A, 0xBBE83F4ECC2BDECB, + 0xDC842B7E2819E230, 0xBA89142E007503B8, 0xA3BC941D0A5061CB, 0xE9F6760E32CD8021, + 0x09C7E552BC76492F, 0x852F54934DA55CC9, 0x8107FCCF064FCF56, 0x098954D51FFF6580, + 0x23B70EDB1955C4BF, 0xC330DE426430F69D, 0x4715ED43E8A45C0A, 0xA8D7E4DAB780A08D, + 0x0572B974F03CE0BB, 0xB57D2E985E1419C7, 0xE8D9ECBE2CF3D73F, 0x2FE4B17170E59750, + 0x11317BA87905E790, 0x7FBF21EC8A1F45EC, 0x1725CABFCB045B00, 0x964E915CD5E2B207, + 0x3E2B8BCBF016D66D, 0xBE7444E39328A0AC, 0xF85B2B4FBCDE44B7, 0x49353FEA39BA63B1, + 0x1DD01AAFCD53486A, 0x1FCA8A92FD719F85, 0xFC7C95D827357AFA, 0x18A6A990C8B35EBD, + 0xCCCB7005C6B9C28D, 0x3BDBB92C43B17F26, 0xAA70B5B4F89695A2, 0xE94C39A54A98307F, + 0xB7A0B174CFF6F36E, 0xD4DBA84729AF48AD, 0x2E18BC1AD9704A68, 0x2DE0966DAF2F8B1C, + 0xB9C11D5B1E43A07E, 0x64972D68DEE33360, 0x94628D38D0C20584, 0xDBC0D2B6AB90A559, + 0xD2733C4335C6A72F, 0x7E75D99D94A70F4D, 0x6CED1983376FA72B, 0x97FCAACBF030BC24, + 0x7B77497B32503B12, 0x8547EDDFB81CCB94, 0x79999CDFF70902CB, 0xCFFE1939438E9B24, + 0x829626E3892D95D7, 0x92FAE24291F2B3F1, 0x63E22C147B9C3403, 0xC678B6D860284A1C, + 0x5873888850659AE7, 0x0981DCD296A8736D, 0x9F65789A6509A440, 0x9FF38FED72E9052F, + 0xE479EE5B9930578C, 0xE7F28ECD2D49EECD, 0x56C074A581EA17FE, 0x5544F7D774B14AEF, + 0x7B3F0195FC6F290F, 0x12153635B2C0CF57, 0x7F5126DBBA5E0CA7, 0x7A76956C3EAFB413, + 0x3D5774A11D31AB39, 0x8A1B083821F40CB4, 0x7B4A38E32537DF62, 0x950113646D1D6E03, + 0x4DA8979A0041E8A9, 0x3BC36E078F7515D7, 0x5D0A12F27AD310D1, 0x7F9D1A2E1EBE1327, + 0xDA3A361B1C5157B1, 0xDCDD7D20903D0C25, 0x36833336D068F707, 0xCE68341F79893389, + 0xAB9090168DD05F34, 0x43954B3252DC25E5, 0xB438C2B67F98E5E9, 0x10DCD78E3851A492, + 0xDBC27AB5447822BF, 0x9B3CDB65F82CA382, 0xB67B7896167B4C84, 0xBFCED1B0048EAC50, + 0xA9119B60369FFEBD, 0x1FFF7AC80904BF45, 0xAC12FB171817EEE7, 0xAF08DA9177DDA93D, + 0x1B0CAB936E65C744, 0xB559EB1D04E5E932, 0xC37B45B3F8D6F2BA, 0xC3A9DC228CAAC9E9, + 0xF3B8B6675A6507FF, 0x9FC477DE4ED681DA, 0x67378D8ECCEF96CB, 0x6DD856D94D259236, + 0xA319CE15B0B4DB31, 0x073973751F12DD5E, 0x8A8E849EB32781A5, 0xE1925C71285279F5, + 0x74C04BF1790C0EFE, 0x4DDA48153C94938A, 0x9D266D6A1CC0542C, 0x7440FB816508C4FE, + 0x13328503DF48229F, 0xD6BF7BAEE43CAC40, 0x4838D65F6EF6748F, 0x1E152328F3318DEA, + 0x8F8419A348F296BF, 0x72C8834A5957B511, 0xD7A023A73260B45C, 0x94EBC8ABCFB56DAE, + 0x9FC10D0F989993E0, 0xDE68A2355B93CAE6, 0xA44CFE79AE538BBE, 0x9D1D84FCCE371425, + 0x51D2B1AB2DDFB636, 0x2FD7E4B9E72CD38C, 0x65CA5B96B7552210, 0xDD69A0D8AB3B546D, + 0x604D51B25FBF70E2, 0x73AA8A564FB7AC9E, 0x1A8C1E992B941148, 0xAAC40A2703D9BEA0, + 0x764DBEAE7FA4F3A6, 0x1E99B96E70A9BE8B, 0x2C5E9DEB57EF4743, 0x3A938FEE32D29981, + 0x26E6DB8FFDF5ADFE, 0x469356C504EC9F9D, 0xC8763C5B08D1908C, 0x3F6C6AF859D80055, + 0x7F7CC39420A3A545, 0x9BFB227EBDF4C5CE, 0x89039D79D6FC5C5C, 0x8FE88B57305E2AB6, + 0xA09E8C8C35AB96DE, 0xFA7E393983325753, 0xD6B6D0ECC617C699, 0xDFEA21EA9E7557E3, + 0xB67C1FA481680AF8, 0xCA1E3785A9E724E5, 0x1CFC8BED0D681639, 0xD18D8549D140CAEA, + 0x4ED0FE7E9DC91335, 0xE4DBF0634473F5D2, 0x1761F93A44D5AEFE, 0x53898E4C3910DA55, + 0x734DE8181F6EC39A, 0x2680B122BAA28D97, 0x298AF231C85BAFAB, 0x7983EED3740847D5, + 0x66C1A2A1A60CD889, 0x9E17E49642A3E4C1, 0xEDB454E7BADC0805, 0x50B704CAB602C329, + 0x4CC317FB9CDDD023, 0x66B4835D9EAFEA22, 0x219B97E26FFC81BD, 0x261E4E4C0A333A9D, + 0x1FE2CCA76517DB90, 0xD7504DFA8816EDBB, 0xB9571FA04DC089C8, 0x1DDC0325259B27DE, + 0xCF3F4688801EB9AA, 0xF4F5D05C10CAB243, 0x38B6525C21A42B0E, 0x36F60E2BA4FA6800, + 0xEB3593803173E0CE, 0x9C4CD6257C5A3603, 0xAF0C317D32ADAA8A, 0x258E5A80C7204C4B, + 0x8B889D624D44885D, 0xF4D14597E660F855, 0xD4347F66EC8941C3, 0xE699ED85B0DFB40D, + 0x2472F6207C2D0484, 0xC2A1E7B5B459AEB5, 0xAB4F6451CC1D45EC, 0x63767572AE3D6174, + 0xA59E0BD101731A28, 0x116D0016CB948F09, 0x2CF9C8CA052F6E9F, 0x0B090A7560A968E3, + 0xABEEDDB2DDE06FF1, 0x58EFC10B06A2068D, 0xC6E57A78FBD986E0, 0x2EAB8CA63CE802D7, + 0x14A195640116F336, 0x7C0828DD624EC390, 0xD74BBE77E6116AC7, 0x804456AF10F5FB53, + 0xEBE9EA2ADF4321C7, 0x03219A39EE587A30, 0x49787FEF17AF9924, 0xA1E9300CD8520548, + 0x5B45E522E4B1B4EF, 0xB49C3B3995091A36, 0xD4490AD526F14431, 0x12A8F216AF9418C2, + 0x001F837CC7350524, 0x1877B51E57A764D5, 0xA2853B80F17F58EE, 0x993E1DE72D36D310, + 0xB3598080CE64A656, 0x252F59CF0D9F04BB, 0xD23C8E176D113600, 0x1BDA0492E7E4586E, + 0x21E0BD5026C619BF, 0x3B097ADAF088F94E, 0x8D14DEDB30BE846E, 0xF95CFFA23AF5F6F4, + 0x3871700761B3F743, 0xCA672B91E9E4FA16, 0x64C8E531BFF53B55, 0x241260ED4AD1E87D, + 0x106C09B972D2E822, 0x7FBA195410E5CA30, 0x7884D9BC6CB569D8, 0x0647DFEDCD894A29, + 0x63573FF03E224774, 0x4FC8E9560F91B123, 0x1DB956E450275779, 0xB8D91274B9E9D4FB, + 0xA2EBEE47E2FBFCE1, 0xD9F1F30CCD97FB09, 0xEFED53D75FD64E6B, 0x2E6D02C36017F67F, + 0xA9AA4D20DB084E9B, 0xB64BE8D8B25396C1, 0x70CB6AF7C2D5BCF0, 0x98F076A4F7A2322E, + 0xBF84470805E69B5F, 0x94C3251F06F90CF3, 0x3E003E616A6591E9, 0xB925A6CD0421AFF3, + 0x61BDD1307C66E300, 0xBF8D5108E27E0D48, 0x240AB57A8B888B20, 0xFC87614BAF287E07, + 0xEF02CDD06FFDB432, 0xA1082C0466DF6C0A, 0x8215E577001332C8, 0xD39BB9C3A48DB6CF, + 0x2738259634305C14, 0x61CF4F94C97DF93D, 0x1B6BACA2AE4E125B, 0x758F450C88572E0B, + 0x959F587D507A8359, 0xB063E962E045F54D, 0x60E8ED72C0DFF5D1, 0x7B64978555326F9F, + 0xFD080D236DA814BA, 0x8C90FD9B083F4558, 0x106F72FE81E2C590, 0x7976033A39F7D952, + 0xA4EC0132764CA04B, 0x733EA705FAE4FA77, 0xB4D8F77BC3E56167, 0x9E21F4F903B33FD9, + 0x9D765E419FB69F6D, 0xD30C088BA61EA5EF, 0x5D94337FBFAF7F5B, 0x1A4E4822EB4D7A59, + 0x6FFE73E81B637FB3, 0xDDF957BC36D8B9CA, 0x64D0E29EEA8838B3, 0x08DD9BDFD96B9F63, + 0x087E79E5A57D1D13, 0xE328E230E3E2B3FB, 0x1C2559E30F0946BE, 0x720BF5F26F4D2EAA, + 0xB0774D261CC609DB, 0x443F64EC5A371195, 0x4112CF68649A260E, 0xD813F2FAB7F5C5CA, + 0x660D3257380841EE, 0x59AC2C7873F910A3, 0xE846963877671A17, 0x93B633ABFA3469F8, + 0xC0C0F5A60EF4CDCF, 0xCAF21ECD4377B28C, 0x57277707199B8175, 0x506C11B9D90E8B1D, + 0xD83CC2687A19255F, 0x4A29C6465A314CD1, 0xED2DF21216235097, 0xB5635C95FF7296E2, + 0x22AF003AB672E811, 0x52E762596BF68235, 0x9AEBA33AC6ECC6B0, 0x944F6DE09134DFB6, + 0x6C47BEC883A7DE39, 0x6AD047C430A12104, 0xA5B1CFDBA0AB4067, 0x7C45D833AFF07862, + 0x5092EF950A16DA0B, 0x9338E69C052B8E7B, 0x455A4B4CFE30E3F5, 0x6B02E63195AD0CF8, + 0x6B17B224BAD6BF27, 0xD1E0CCD25BB9C169, 0xDE0C89A556B9AE70, 0x50065E535A213CF6, + 0x9C1169FA2777B874, 0x78EDEFD694AF1EED, 0x6DC93D9526A50E68, 0xEE97F453F06791ED, + 0x32AB0EDB696703D3, 0x3A6853C7E70757A7, 0x31865CED6120F37D, 0x67FEF95D92607890, + 0x1F2B1D1F15F6DC9C, 0xB69E38A8965C6B65, 0xAA9119FF184CCCF4, 0xF43C732873F24C13, + 0xFB4A3D794A9A80D2, 0x3550C2321FD6109C, 0x371F77E76BB8417E, 0x6BFA9AAE5EC05779, + 0xCD04F3FF001A4778, 0xE3273522064480CA, 0x9F91508BFFCFC14A, 0x049A7F41061A9E60, + 0xFCB6BE43A9F2FE9B, 0x08DE8A1C7797DA9B, 0x8F9887E6078735A1, 0xB5B4071DBFC73A66, + 0x230E343DFBA08D33, 0x43ED7F5A0FAE657D, 0x3A88A0FBBCB05C63, 0x21874B8B4D2DBC4F, + 0x1BDEA12E35F6A8C9, 0x53C065C6C8E63528, 0xE34A1D250E7A8D6B, 0xD6B04D3B7651DD7E, + 0x5E90277E7CB39E2D, 0x2C046F22062DC67D, 0xB10BB459132D0A26, 0x3FA9DDFB67E2F199, + 0x0E09B88E1914F7AF, 0x10E8B35AF3EEAB37, 0x9EEDECA8E272B933, 0xD4C718BC4AE8AE5F, + 0x81536D601170FC20, 0x91B534F885818A06, 0xEC8177F83F900978, 0x190E714FADA5156E, + 0xB592BF39B0364963, 0x89C350C893AE7DC1, 0xAC042E70F8B383F2, 0xB49B52E587A1EE60, + 0xFB152FE3FF26DA89, 0x3E666E6F69AE2C15, 0x3B544EBE544C19F9, 0xE805A1E290CF2456, + 0x24B33C9D7ED25117, 0xE74733427B72F0C1, 0x0A804D18B7097475, 0x57E3306D881EDB4F, + 0x4AE7D6A36EB5DBCB, 0x2D8D5432157064C8, 0xD1E649DE1E7F268B, 0x8A328A1CEDFE552C, + 0x07A3AEC79624C7DA, 0x84547DDC3E203C94, 0x990A98FD5071D263, 0x1A4FF12616EEFC89, + 0xF6F7FD1431714200, 0x30C05B1BA332F41C, 0x8D2636B81555A786, 0x46C9FEB55D120902, + 0xCCEC0A73B49C9921, 0x4E9D2827355FC492, 0x19EBB029435DCB0F, 0x4659D2B743848A2C, + 0x963EF2C96B33BE31, 0x74F85198B05A2E7D, 0x5A0F544DD2B1FB18, 0x03727073C2E134B1, + 0xC7F6AA2DE59AEA61, 0x352787BAA0D7C22F, 0x9853EAB63B5E0B35, 0xABBDCDD7ED5C0860, + 0xCF05DAF5AC8D77B0, 0x49CAD48CEBF4A71E, 0x7A4C10EC2158C4A6, 0xD9E92AA246BF719E, + 0x13AE978D09FE5557, 0x730499AF921549FF, 0x4E4B705B92903BA4, 0xFF577222C14F0A3A, + 0x55B6344CF97AAFAE, 0xB862225B055B6960, 0xCAC09AFBDDD2CDB4, 0xDAF8E9829FE96B5F, + 0xB5FDFC5D3132C498, 0x310CB380DB6F7503, 0xE87FBB46217A360E, 0x2102AE466EBB1148, + 0xF8549E1A3AA5E00D, 0x07A69AFDCC42261A, 0xC4C118BFE78FEAAE, 0xF9F4892ED96BD438, + 0x1AF3DBE25D8F45DA, 0xF5B4B0B0D2DEEEB4, 0x962ACEEFA82E1C84, 0x046E3ECAAF453CE9, + 0xF05D129681949A4C, 0x964781CE734B3C84, 0x9C2ED44081CE5FBD, 0x522E23F3925E319E, + 0x177E00F9FC32F791, 0x2BC60A63A6F3B3F2, 0x222BBFAE61725606, 0x486289DDCC3D6780, + 0x7DC7785B8EFDFC80, 0x8AF38731C02BA980, 0x1FAB64EA29A2DDF7, 0xE4D9429322CD065A, + 0x9DA058C67844F20C, 0x24C0E332B70019B0, 0x233003B5A6CFE6AD, 0xD586BD01C5C217F6, + 0x5E5637885F29BC2B, 0x7EBA726D8C94094B, 0x0A56A5F0BFE39272, 0xD79476A84EE20D06, + 0x9E4C1269BAA4BF37, 0x17EFEE45B0DEE640, 0x1D95B0A5FCF90BC6, 0x93CBE0B699C2585D, + 0x65FA4F227A2B6D79, 0xD5F9E858292504D5, 0xC2B5A03F71471A6F, 0x59300222B4561E00, + 0xCE2F8642CA0712DC, 0x7CA9723FBB2E8988, 0x2785338347F2BA08, 0xC61BB3A141E50E8C, + 0x150F361DAB9DEC26, 0x9F6A419D382595F4, 0x64A53DC924FE7AC9, 0x142DE49FFF7A7C3D, + 0x0C335248857FA9E7, 0x0A9C32D5EAE45305, 0xE6C42178C4BBB92E, 0x71F1CE2490D20B07, + 0xF1BCC3D275AFE51A, 0xE728E8C83C334074, 0x96FBF83A12884624, 0x81A1549FD6573DA5, + 0x5FA7867CAF35E149, 0x56986E2EF3ED091B, 0x917F1DD5F8886C61, 0xD20D8C88C8FFE65F, + 0x31D71DCE64B2C310, 0xF165B587DF898190, 0xA57E6339DD2CF3A0, 0x1EF6E6DBB1961EC9, + 0x70CC73D90BC26E24, 0xE21A6B35DF0C3AD7, 0x003A93D8B2806962, 0x1C99DED33CB890A1, + 0xCF3145DE0ADD4289, 0xD0E4427A5514FB72, 0x77C621CC9FB3A483, 0x67A34DAC4356550B, + 0xF8D626AAAF278509}; static constexpr std::array castlingKey = []() constexpr { auto generateCastlingKey = [](int index) constexpr -> U64 { @@ -1686,8 +1835,10 @@ class Zobrist { U64 key = 0; - for (int i = 0; i < RANDOM_COUNT; ++i) { - if (index & (1 << i)) { + for (int i = 0; i < RANDOM_COUNT; ++i) + { + if (index & (1 << i)) + { key ^= RANDOM_ARRAY[RANDOM_OFFSET + i]; } } @@ -1697,7 +1848,8 @@ class Zobrist { std::array arr{}; - for (int i = 0; i < 16; ++i) arr[i] = generateCastlingKey(i); + for (int i = 0; i < 16; ++i) + arr[i] = generateCastlingKey(i); return arr; }(); @@ -1736,25 +1888,29 @@ namespace chess { namespace detail { inline std::optional parseStringViewToInt(std::string_view sv) { - if (sv.empty()) return std::nullopt; + if (sv.empty()) + return std::nullopt; std::string_view parsed_sv = sv; - if (parsed_sv.back() == ';') parsed_sv.remove_suffix(1); + if (parsed_sv.back() == ';') + parsed_sv.remove_suffix(1); - if (parsed_sv.empty()) return std::nullopt; + if (parsed_sv.empty()) + return std::nullopt; #ifdef CHESS_USE_CHARCONV - int result; - const char *begin = parsed_sv.data(); - const char *end = begin + parsed_sv.size(); + int result; + const char* begin = parsed_sv.data(); + const char* end = begin + parsed_sv.size(); auto [ptr, ec] = std::from_chars(begin, end, result); - if (ec == std::errc() && ptr == end) { + if (ec == std::errc() && ptr == end) + { return result; } #else - std::string str(parsed_sv); + std::string str(parsed_sv); std::stringstream ss(str); ss.exceptions(std::ios::goodbit); @@ -1762,7 +1918,8 @@ inline std::optional parseStringViewToInt(std::string_view sv) { int value; ss >> value; - if (!ss.fail() && (ss.eof() || (ss >> std::ws).eof())) { + if (!ss.fail() && (ss.eof() || (ss >> std::ws).eof())) + { return value; } #endif @@ -1771,7 +1928,12 @@ inline std::optional parseStringViewToInt(std::string_view sv) { } } // namespace detail -enum class GameResult { WIN, LOSE, DRAW, NONE }; +enum class GameResult { + WIN, + LOSE, + DRAW, + NONE +}; enum class GameResultReason { CHECKMATE, @@ -1782,7 +1944,11 @@ enum class GameResultReason { NONE }; -enum class CheckType { NO_CHECK, DIRECT_CHECK, DISCOVERY_CHECK }; +enum class CheckType { + NO_CHECK, + DIRECT_CHECK, + DISCOVERY_CHECK +}; // A compact representation of the board in 24 bytes, // does not include the half-move clock or full move number. @@ -1794,13 +1960,18 @@ class Board { public: class CastlingRights { public: - enum class Side : std::uint8_t { KING_SIDE, QUEEN_SIDE }; + enum class Side : std::uint8_t { + KING_SIDE, + QUEEN_SIDE + }; constexpr void setCastlingRight(Color color, Side castle, File rook_file) { rooks[color][static_cast(castle)] = rook_file; } - constexpr void clear() { rooks[0][0] = rooks[0][1] = rooks[1][0] = rooks[1][1] = File::NO_FILE; } + constexpr void clear() { + rooks[0][0] = rooks[0][1] = rooks[1][0] = rooks[1][1] = File::NO_FILE; + } constexpr int clear(Color color, Side castle) { rooks[color][static_cast(castle)] = File::NO_FILE; @@ -1813,18 +1984,22 @@ class Board { return rooks[color][static_cast(castle)] != File::NO_FILE; } - constexpr bool has(Color color) const { return has(color, Side::KING_SIDE) || has(color, Side::QUEEN_SIDE); } + constexpr bool has(Color color) const { + return has(color, Side::KING_SIDE) || has(color, Side::QUEEN_SIDE); + } - constexpr File getRookFile(Color color, Side castle) const { return rooks[color][static_cast(castle)]; } + constexpr File getRookFile(Color color, Side castle) const { + return rooks[color][static_cast(castle)]; + } constexpr int hashIndex() const { - return has(Color::WHITE, Side::KING_SIDE) + 2 * has(Color::WHITE, Side::QUEEN_SIDE) + - 4 * has(Color::BLACK, Side::KING_SIDE) + 8 * has(Color::BLACK, Side::QUEEN_SIDE); + return has(Color::WHITE, Side::KING_SIDE) + 2 * has(Color::WHITE, Side::QUEEN_SIDE) + + 4 * has(Color::BLACK, Side::KING_SIDE) + 8 * has(Color::BLACK, Side::QUEEN_SIDE); } constexpr bool isEmpty() const { return !has(Color::WHITE) && !has(Color::BLACK); } - template + template static constexpr Side closestSide(T sq, T pred) { return sq > pred ? Side::KING_SIDE : Side::QUEEN_SIDE; } @@ -1835,22 +2010,27 @@ class Board { private: struct State { - U64 hash; + U64 hash; CastlingRights castling; - Square enpassant; - std::uint8_t half_moves; - Piece captured_piece; - - State(const U64 &hash, const CastlingRights &castling, const Square &enpassant, const std::uint8_t &half_moves, - const Piece &captured_piece) - : hash(hash), - castling(castling), - enpassant(enpassant), - half_moves(half_moves), - captured_piece(captured_piece) {} + Square enpassant; + std::uint8_t half_moves; + Piece captured_piece; + + State(const U64& hash, + const CastlingRights& castling, + const Square& enpassant, + const std::uint8_t& half_moves, + const Piece& captured_piece) : + hash(hash), + castling(castling), + enpassant(enpassant), + half_moves(half_moves), + captured_piece(captured_piece) {} }; - enum class PrivateCtor { CREATE }; + enum class PrivateCtor { + CREATE + }; // private constructor to avoid initialization Board(PrivateCtor) {} @@ -1885,36 +2065,47 @@ class Board { bool setEpd(const std::string_view epd) { auto parts = utils::splitString(epd, ' '); - if (parts.size() < 4) return false; + if (parts.size() < 4) + return false; int hm = 0; int fm = 1; - if (auto it = std::find(parts.begin(), parts.end(), "hmvc"); it != parts.end()) { - if (std::distance(it, parts.end()) > 1) { + if (auto it = std::find(parts.begin(), parts.end(), "hmvc"); it != parts.end()) + { + if (std::distance(it, parts.end()) > 1) + { auto num = *(it + 1); auto parsed = detail::parseStringViewToInt(num); - if (parsed) hm = *parsed; - } else { + if (parsed) + hm = *parsed; + } + else + { return false; } } - if (auto it = std::find(parts.begin(), parts.end(), "fmvn"); it != parts.end()) { - if (std::distance(it, parts.end()) > 1) { + if (auto it = std::find(parts.begin(), parts.end(), "fmvn"); it != parts.end()) + { + if (std::distance(it, parts.end()) > 1) + { auto num = *(it + 1); auto parsed = detail::parseStringViewToInt(num); if (parsed && *parsed > 0) fm = *parsed; else return false; - } else { + } + else + { return false; } } - auto fen = std::string(parts[0]) + " " + std::string(parts[1]) + " " + std::string(parts[2]) + " " + - std::string(parts[3]) + " " + std::to_string(hm) + " " + std::to_string(fm); + auto fen = std::string(parts[0]) + " " + std::string(parts[1]) + " " + std::string(parts[2]) + + " " + std::string(parts[3]) + " " + std::to_string(hm) + " " + + std::to_string(fm); return setFen(fen); } @@ -1929,26 +2120,32 @@ class Board { ss.reserve(100); // Loop through the ranks of the board in reverse order - for (int rank = 7; rank >= 0; rank--) { + for (int rank = 7; rank >= 0; rank--) + { std::uint32_t free_space = 0; // Loop through the files of the board - for (int file = 0; file < 8; file++) { + for (int file = 0; file < 8; file++) + { // Calculate the square index const int sq = rank * 8 + file; // If there is a piece at the current square - if (Piece piece = at(Square(sq)); piece != Piece::NONE) { + if (Piece piece = at(Square(sq)); piece != Piece::NONE) + { // If there were any empty squares before this piece, // append the number of empty squares to the FEN string - if (free_space) { + if (free_space) + { ss += std::to_string(free_space); free_space = 0; } // Append the character representing the piece to the FEN string ss += static_cast(piece); - } else { + } + else + { // If there is no piece at the current square, increment the // counter for the number of empty squares free_space++; @@ -1957,7 +2154,8 @@ class Board { // If there are any empty squares at the end of the rank, // append the number of empty squares to the FEN string - if (free_space != 0) { + if (free_space != 0) + { ss += std::to_string(free_space); } @@ -1973,7 +2171,8 @@ class Board { // whether castling is allowed for each player if (cr_.isEmpty()) ss += " -"; - else { + else + { ss += ' '; ss += getCastleString(); } @@ -1982,12 +2181,14 @@ class Board { // and the half-move clock and full move number to the FEN string if (ep_sq_ == Square::NO_SQ) ss += " -"; - else { + else + { ss += ' '; ss += static_cast(ep_sq_); } - if (move_counters) { + if (move_counters) + { ss += ' '; ss += std::to_string(halfMoveClock()); ss += ' '; @@ -2019,7 +2220,7 @@ class Board { * @tparam EXACT * @param move */ - template + template void makeMove(const Move move) { const auto capture = at(move.to()) != Piece::NONE && move.typeOf() != Move::CASTLING; const auto captured = at(move.to()); @@ -2033,53 +2234,66 @@ class Board { hfm_++; plies_++; - if (ep_sq_ != Square::NO_SQ) key_ ^= Zobrist::enpassant(ep_sq_.file()); + if (ep_sq_ != Square::NO_SQ) + key_ ^= Zobrist::enpassant(ep_sq_.file()); ep_sq_ = Square::NO_SQ; - if (capture) { + if (capture) + { removePiece(captured, move.to()); hfm_ = 0; key_ ^= Zobrist::piece(captured, move.to()); // remove castling rights if rook is captured - if (captured.type() == PieceType::ROOK && Rank::back_rank(move.to().rank(), ~stm_)) { + if (captured.type() == PieceType::ROOK && Rank::back_rank(move.to().rank(), ~stm_)) + { const auto king_sq = kingSq(~stm_); const auto file = CastlingRights::closestSide(move.to(), king_sq); - if (cr_.getRookFile(~stm_, file) == move.to().file()) { + if (cr_.getRookFile(~stm_, file) == move.to().file()) + { key_ ^= Zobrist::castlingIndex(cr_.clear(~stm_, file)); } } } // remove castling rights if king moves - if (pt == PieceType::KING && cr_.has(stm_)) { + if (pt == PieceType::KING && cr_.has(stm_)) + { key_ ^= Zobrist::castling(cr_.hashIndex()); cr_.clear(stm_); key_ ^= Zobrist::castling(cr_.hashIndex()); - } else if (pt == PieceType::ROOK && Square::back_rank(move.from(), stm_)) { + } + else if (pt == PieceType::ROOK && Square::back_rank(move.from(), stm_)) + { const auto king_sq = kingSq(stm_); const auto file = CastlingRights::closestSide(move.from(), king_sq); // remove castling rights if rook moves from back rank - if (cr_.getRookFile(stm_, file) == move.from().file()) { + if (cr_.getRookFile(stm_, file) == move.from().file()) + { key_ ^= Zobrist::castlingIndex(cr_.clear(stm_, file)); } - } else if (pt == PieceType::PAWN) { + } + else if (pt == PieceType::PAWN) + { hfm_ = 0; // double push - if (Square::value_distance(move.to(), move.from()) == 16) { + if (Square::value_distance(move.to(), move.from()) == 16) + { // imaginary attacks from the ep square from the pawn which moved Bitboard ep_mask = attacks::pawn(stm_, move.to().ep_square()); // add enpassant hash if enemy pawns are attacking the square - if (static_cast(ep_mask & pieces(PieceType::PAWN, ~stm_))) { + if (static_cast(ep_mask & pieces(PieceType::PAWN, ~stm_))) + { int found = -1; // check if the enemy can legally capture the pawn on the next move - if constexpr (EXACT) { + if constexpr (EXACT) + { const auto piece = at(move.from()); found = 0; @@ -2091,13 +2305,19 @@ class Board { bool valid; - if (stm_ == Color::WHITE) { - valid = movegen::isEpSquareValid(*this, move.to().ep_square()); - } else { - valid = movegen::isEpSquareValid(*this, move.to().ep_square()); + if (stm_ == Color::WHITE) + { + valid = + movegen::isEpSquareValid(*this, move.to().ep_square()); + } + else + { + valid = + movegen::isEpSquareValid(*this, move.to().ep_square()); } - if (valid) found = 1; + if (valid) + found = 1; // undo stm_ = ~stm_; @@ -2106,7 +2326,8 @@ class Board { placePieceInternal(piece, move.from()); } - if (found != 0) { + if (found != 0) + { assert(at(move.to().ep_square()) == Piece::NONE); ep_sq_ = move.to().ep_square(); key_ ^= Zobrist::enpassant(move.to().ep_square().file()); @@ -2115,7 +2336,8 @@ class Board { } } - if (move.typeOf() == Move::CASTLING) { + if (move.typeOf() == Move::CASTLING) + { assert(at(move.from()) == PieceType::KING); assert(at(move.to()) == PieceType::ROOK); @@ -2137,7 +2359,9 @@ class Board { key_ ^= Zobrist::piece(king, move.from()) ^ Zobrist::piece(king, kingTo); key_ ^= Zobrist::piece(rook, move.to()) ^ Zobrist::piece(rook, rookTo); - } else if (move.typeOf() == Move::PROMOTION) { + } + else if (move.typeOf() == Move::PROMOTION) + { const auto piece_pawn = Piece(PieceType::PAWN, stm_); const auto piece_prom = Piece(move.promotionType(), stm_); @@ -2145,7 +2369,9 @@ class Board { placePiece(piece_prom, move.to()); key_ ^= Zobrist::piece(piece_pawn, move.from()) ^ Zobrist::piece(piece_prom, move.to()); - } else { + } + else + { assert(at(move.from()) != Piece::NONE); assert(at(move.to()) == Piece::NONE); @@ -2157,7 +2383,8 @@ class Board { key_ ^= Zobrist::piece(piece, move.from()) ^ Zobrist::piece(piece, move.to()); } - if (move.typeOf() == Move::ENPASSANT) { + if (move.typeOf() == Move::ENPASSANT) + { assert(at(move.to().ep_square()) == PieceType::PAWN); const auto piece = Piece(PieceType::PAWN, ~stm_); @@ -2172,7 +2399,7 @@ class Board { } void unmakeMove(const Move move) { - const auto &prev = prev_states_.back(); + const auto& prev = prev_states_.back(); ep_sq_ = prev.enpassant; cr_ = prev.castling; @@ -2180,10 +2407,13 @@ class Board { stm_ = ~stm_; plies_--; - if (move.typeOf() == Move::CASTLING) { - const bool king_side = move.to() > move.from(); - const auto rook_from_sq = Square(king_side ? File::FILE_F : File::FILE_D, move.from().rank()); - const auto king_to_sq = Square(king_side ? File::FILE_G : File::FILE_C, move.from().rank()); + if (move.typeOf() == Move::CASTLING) + { + const bool king_side = move.to() > move.from(); + const auto rook_from_sq = + Square(king_side ? File::FILE_F : File::FILE_D, move.from().rank()); + const auto king_to_sq = + Square(king_side ? File::FILE_G : File::FILE_C, move.from().rank()); assert(at(rook_from_sq) == PieceType::ROOK); assert(at(king_to_sq) == PieceType::KING); @@ -2199,8 +2429,9 @@ class Board { placePiece(king, move.from()); placePiece(rook, move.to()); - - } else if (move.typeOf() == Move::PROMOTION) { + } + else if (move.typeOf() == Move::PROMOTION) + { const auto pawn = Piece(PieceType::PAWN, stm_); const auto piece = at(move.to()); @@ -2212,12 +2443,14 @@ class Board { removePiece(piece, move.to()); placePiece(pawn, move.from()); - if (prev.captured_piece != Piece::NONE) { + if (prev.captured_piece != Piece::NONE) + { assert(at(move.to()) == Piece::NONE); placePiece(prev.captured_piece, move.to()); } - - } else { + } + else + { assert(at(move.to()) != Piece::NONE); assert(at(move.from()) == Piece::NONE); @@ -2226,14 +2459,17 @@ class Board { removePiece(piece, move.to()); placePiece(piece, move.from()); - if (move.typeOf() == Move::ENPASSANT) { + if (move.typeOf() == Move::ENPASSANT) + { const auto pawn = Piece(PieceType::PAWN, ~stm_); const auto pawnTo = static_cast(ep_sq_ ^ 8); assert(at(pawnTo) == Piece::NONE); placePiece(pawn, pawnTo); - } else if (prev.captured_piece != Piece::NONE) { + } + else if (prev.captured_piece != Piece::NONE) + { assert(at(move.to()) == Piece::NONE); placePiece(prev.captured_piece, move.to()); @@ -2251,7 +2487,8 @@ class Board { prev_states_.emplace_back(key_, cr_, ep_sq_, hfm_, Piece::NONE); key_ ^= Zobrist::sideToMove(); - if (ep_sq_ != Square::NO_SQ) key_ ^= Zobrist::enpassant(ep_sq_.file()); + if (ep_sq_ != Square::NO_SQ) + key_ ^= Zobrist::enpassant(ep_sq_.file()); ep_sq_ = Square::NO_SQ; stm_ = ~stm_; @@ -2263,7 +2500,7 @@ class Board { * @brief Unmake a null move. (Switches the side to move) */ void unmakeNullMove() { - const auto &prev = prev_states_.back(); + const auto& prev = prev_states_.back(); ep_sq_ = prev.enpassant; cr_ = prev.castling; @@ -2331,7 +2568,8 @@ class Board { */ [[nodiscard]] Bitboard pieces(PieceType type) const noexcept { return pieces_bb_[type]; } - template && ...)>> + template && ...)>> [[nodiscard]] Bitboard pieces(Pieces... pieces) const noexcept { return (pieces_bb_[static_cast(pieces)] | ...); } @@ -2342,13 +2580,16 @@ class Board { * @param sq * @return */ - template + template [[nodiscard]] T at(Square sq) const noexcept { assert(sq.is_valid()); - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) + { return board_[sq.index()].type(); - } else { + } + else + { return board_[sq.index()]; } } @@ -2359,23 +2600,25 @@ class Board { * @return */ bool isCapture(const Move move) const noexcept { - return (at(move.to()) != Piece::NONE && move.typeOf() != Move::CASTLING) || move.typeOf() == Move::ENPASSANT; + return (at(move.to()) != Piece::NONE && move.typeOf() != Move::CASTLING) + || move.typeOf() == Move::ENPASSANT; } /** * @brief Get the current zobrist hash key of the board * @return */ - [[nodiscard]] U64 hash() const noexcept { return key_; } - [[nodiscard]] Color sideToMove() const noexcept { return stm_; } - [[nodiscard]] Square enpassantSq() const noexcept { return ep_sq_; } + [[nodiscard]] U64 hash() const noexcept { return key_; } + [[nodiscard]] Color sideToMove() const noexcept { return stm_; } + [[nodiscard]] Square enpassantSq() const noexcept { return ep_sq_; } [[nodiscard]] CastlingRights castlingRights() const noexcept { return cr_; } - [[nodiscard]] std::uint32_t halfMoveClock() const noexcept { return hfm_; } - [[nodiscard]] std::uint32_t fullMoveNumber() const noexcept { return 1 + plies_ / 2; } + [[nodiscard]] std::uint32_t halfMoveClock() const noexcept { return hfm_; } + [[nodiscard]] std::uint32_t fullMoveNumber() const noexcept { return 1 + plies_ / 2; } void set960(bool is960) { chess960_ = is960; - if (!original_fen_.empty()) setFen(original_fen_); + if (!original_fen_.empty()) + setFen(original_fen_); } /** @@ -2389,27 +2632,35 @@ class Board { * @return */ [[nodiscard]] std::string getCastleString() const { - static const auto get_file = [](const CastlingRights &cr, Color c, CastlingRights::Side side) { + static const auto get_file = [](const CastlingRights& cr, Color c, + CastlingRights::Side side) { auto file = static_cast(cr.getRookFile(c, side)); return c == Color::WHITE ? std::toupper(file[0]) : file[0]; }; - if (chess960_) { + if (chess960_) + { std::string ss; for (auto color : {Color::WHITE, Color::BLACK}) - for (auto side : {CastlingRights::Side::KING_SIDE, CastlingRights::Side::QUEEN_SIDE}) - if (cr_.has(color, side)) ss += get_file(cr_, color, side); + for (auto side : + {CastlingRights::Side::KING_SIDE, CastlingRights::Side::QUEEN_SIDE}) + if (cr_.has(color, side)) + ss += get_file(cr_, color, side); return ss; } std::string ss; - if (cr_.has(Color::WHITE, CastlingRights::Side::KING_SIDE)) ss += 'K'; - if (cr_.has(Color::WHITE, CastlingRights::Side::QUEEN_SIDE)) ss += 'Q'; - if (cr_.has(Color::BLACK, CastlingRights::Side::KING_SIDE)) ss += 'k'; - if (cr_.has(Color::BLACK, CastlingRights::Side::QUEEN_SIDE)) ss += 'q'; + if (cr_.has(Color::WHITE, CastlingRights::Side::KING_SIDE)) + ss += 'K'; + if (cr_.has(Color::WHITE, CastlingRights::Side::QUEEN_SIDE)) + ss += 'Q'; + if (cr_.has(Color::BLACK, CastlingRights::Side::KING_SIDE)) + ss += 'k'; + if (cr_.has(Color::BLACK, CastlingRights::Side::QUEEN_SIDE)) + ss += 'q'; return ss; } @@ -2428,9 +2679,12 @@ class Board { // be across half-moves. const auto size = static_cast(prev_states_.size()); - for (int i = size - 2; i >= 0 && i >= size - hfm_ - 1; i -= 2) { - if (prev_states_[i].hash == key_) c++; - if (c == count) return true; + for (int i = size - 2; i >= 0 && i >= size - hfm_ - 1; i -= 2) + { + if (prev_states_[i].hash == key_) + c++; + if (c == count) + return true; } return false; @@ -2454,7 +2708,8 @@ class Board { Movelist movelist; movegen::legalmoves(movelist, *this); - if (movelist.empty() && inCheck()) { + if (movelist.empty() && inCheck()) + { return {GameResultReason::CHECKMATE, GameResult::LOSE}; } @@ -2469,29 +2724,39 @@ class Board { const auto count = occ().count(); // only kings, draw - if (count == 2) return true; + if (count == 2) + return true; // only bishop + knight, cant mate - if (count == 3) { - if (pieces(PieceType::BISHOP, Color::WHITE) || pieces(PieceType::BISHOP, Color::BLACK)) return true; - if (pieces(PieceType::KNIGHT, Color::WHITE) || pieces(PieceType::KNIGHT, Color::BLACK)) return true; + if (count == 3) + { + if (pieces(PieceType::BISHOP, Color::WHITE) || pieces(PieceType::BISHOP, Color::BLACK)) + return true; + if (pieces(PieceType::KNIGHT, Color::WHITE) || pieces(PieceType::KNIGHT, Color::BLACK)) + return true; } // same colored bishops, cant mate - if (count == 4) { - if (pieces(PieceType::BISHOP, Color::WHITE) && pieces(PieceType::BISHOP, Color::BLACK) && - Square::same_color(pieces(PieceType::BISHOP, Color::WHITE).lsb(), - pieces(PieceType::BISHOP, Color::BLACK).lsb())) + if (count == 4) + { + if (pieces(PieceType::BISHOP, Color::WHITE) && pieces(PieceType::BISHOP, Color::BLACK) + && Square::same_color(pieces(PieceType::BISHOP, Color::WHITE).lsb(), + pieces(PieceType::BISHOP, Color::BLACK).lsb())) return true; // one side with two bishops which have the same color auto white_bishops = pieces(PieceType::BISHOP, Color::WHITE); auto black_bishops = pieces(PieceType::BISHOP, Color::BLACK); - if (white_bishops.count() == 2) { - if (Square::same_color(white_bishops.lsb(), white_bishops.msb())) return true; - } else if (black_bishops.count() == 2) { - if (Square::same_color(black_bishops.lsb(), black_bishops.msb())) return true; + if (white_bishops.count() == 2) + { + if (Square::same_color(white_bishops.lsb(), white_bishops.msb())) + return true; + } + else if (black_bishops.count() == 2) + { + if (Square::same_color(black_bishops.lsb(), black_bishops.msb())) + return true; } } @@ -2505,15 +2770,20 @@ class Board { * @return */ [[nodiscard]] std::pair isGameOver() const noexcept { - if (isHalfMoveDraw()) return getHalfMoveDrawType(); - if (isInsufficientMaterial()) return {GameResultReason::INSUFFICIENT_MATERIAL, GameResult::DRAW}; - if (isRepetition()) return {GameResultReason::THREEFOLD_REPETITION, GameResult::DRAW}; + if (isHalfMoveDraw()) + return getHalfMoveDrawType(); + if (isInsufficientMaterial()) + return {GameResultReason::INSUFFICIENT_MATERIAL, GameResult::DRAW}; + if (isRepetition()) + return {GameResultReason::THREEFOLD_REPETITION, GameResult::DRAW}; Movelist movelist; movegen::legalmoves(movelist, *this); - if (movelist.empty()) { - if (inCheck()) return {GameResultReason::CHECKMATE, GameResult::LOSE}; + if (movelist.empty()) + { + if (inCheck()) + return {GameResultReason::CHECKMATE, GameResult::LOSE}; return {GameResultReason::STALEMATE, GameResult::DRAW}; } @@ -2528,11 +2798,17 @@ class Board { */ [[nodiscard]] bool isAttacked(Square square, Color color) const noexcept { // cheap checks first - if (attacks::pawn(~color, square) & pieces(PieceType::PAWN, color)) return true; - if (attacks::knight(square) & pieces(PieceType::KNIGHT, color)) return true; - if (attacks::king(square) & pieces(PieceType::KING, color)) return true; - if (attacks::bishop(square, occ()) & pieces(PieceType::BISHOP, PieceType::QUEEN) & us(color)) return true; - if (attacks::rook(square, occ()) & pieces(PieceType::ROOK, PieceType::QUEEN) & us(color)) return true; + if (attacks::pawn(~color, square) & pieces(PieceType::PAWN, color)) + return true; + if (attacks::knight(square) & pieces(PieceType::KNIGHT, color)) + return true; + if (attacks::king(square) & pieces(PieceType::KING, color)) + return true; + if (attacks::bishop(square, occ()) & pieces(PieceType::BISHOP, PieceType::QUEEN) + & us(color)) + return true; + if (attacks::rook(square, occ()) & pieces(PieceType::ROOK, PieceType::QUEEN) & us(color)) + return true; return false; } @@ -2543,7 +2819,7 @@ class Board { */ [[nodiscard]] bool inCheck() const noexcept { return isAttacked(kingSq(stm_), ~stm_); } - [[nodiscard]] CheckType givesCheck(const Move &m) const noexcept; + [[nodiscard]] CheckType givesCheck(const Move& m) const noexcept; /** * @brief Checks if the given color has at least 1 piece thats not pawn and not king @@ -2563,16 +2839,19 @@ class Board { auto pieces = occ(); - while (pieces) { + while (pieces) + { const Square sq = pieces.pop(); hash_key ^= Zobrist::piece(at(sq), sq); } U64 ep_hash = 0ULL; - if (ep_sq_ != Square::NO_SQ) ep_hash ^= Zobrist::enpassant(ep_sq_.file()); + if (ep_sq_ != Square::NO_SQ) + ep_hash ^= Zobrist::enpassant(ep_sq_.file()); U64 stm_hash = 0ULL; - if (stm_ == Color::WHITE) stm_hash ^= Zobrist::sideToMove(); + if (stm_ == Color::WHITE) + stm_hash ^= Zobrist::sideToMove(); U64 castling_hash = 0ULL; castling_hash ^= Zobrist::castling(cr_.hashIndex()); @@ -2584,7 +2863,7 @@ class Board { return castling_path[c][isKingSide]; } - friend std::ostream &operator<<(std::ostream &os, const Board &board); + friend std::ostream& operator<<(std::ostream& os, const Board& board); /** * @brief Compresses the board into a PackedBoard. @@ -2599,9 +2878,11 @@ class Board { * @param board * @return */ - static PackedBoard encode(const Board &board) { return encodeState(board); } + static PackedBoard encode(const Board& board) { return encodeState(board); } - static PackedBoard encode(std::string_view fen, bool chess960 = false) { return encodeState(fen, chess960); } + static PackedBoard encode(std::string_view fen, bool chess960 = false) { + return encodeState(fen, chess960); + } /** * @brief Creates a Board object from a PackedBoard @@ -2609,7 +2890,7 @@ class Board { * @param chess960 If the board is a chess960 position, set this to true * @return */ - static Board decode(const PackedBoard &compressed, bool chess960 = false) { + static Board decode(const PackedBoard& compressed, bool chess960 = false) { Board board = Board(PrivateCtor::CREATE); board.chess960_ = chess960; decode(board, compressed); @@ -2637,7 +2918,7 @@ class Board { * * We will later deduce the square of the pieces from the occupancy bitboard. */ - static PackedBoard encodeState(const Board &board) { + static PackedBoard encodeState(const Board& board) { PackedBoard packed{}; packed[0] = board.occ().getBits() >> 56; @@ -2652,13 +2933,15 @@ class Board { auto offset = 8 * 2; auto occ = board.occ(); - while (occ) { + while (occ) + { // we now fill the packed array, since our convertedpiece only actually needs 4 bits, // we can store 2 pieces in one byte. - const auto sq = Square(occ.pop()); - const auto shift = (offset % 2 == 0 ? 4 : 0); - const auto meaning = convertMeaning(board.cr_, board.sideToMove(), board.ep_sq_, sq, board.at(sq)); - const auto nibble = meaning << shift; + const auto sq = Square(occ.pop()); + const auto shift = (offset % 2 == 0 ? 4 : 0); + const auto meaning = + convertMeaning(board.cr_, board.sideToMove(), board.ep_sq_, sq, board.at(sq)); + const auto nibble = meaning << shift; packed[offset / 2] |= nibble; offset++; @@ -2669,15 +2952,17 @@ class Board { static PackedBoard encodeState(std::string_view fen, bool chess960 = false) { // fallback to slower method - if (chess960) { + if (chess960) + { return encodeState(Board(fen, true)); } PackedBoard packed{}; - while (fen[0] == ' ') fen.remove_prefix(1); + while (fen[0] == ' ') + fen.remove_prefix(1); - const auto params = split_string_view<6>(fen); + const auto params = split_string_view<4>(fen); const auto position = params[0].has_value() ? *params[0] : ""; const auto move_right = params[1].has_value() ? *params[1] : "w"; const auto castling = params[2].has_value() ? *params[2] : "-"; @@ -2688,16 +2973,22 @@ class Board { CastlingRights cr; - for (char i : castling) { - if (i == '-') break; + for (char i : castling) + { + if (i == '-') + break; const auto king_side = CastlingRights::Side::KING_SIDE; const auto queen_side = CastlingRights::Side::QUEEN_SIDE; - if (i == 'K') cr.setCastlingRight(Color::WHITE, king_side, File::FILE_H); - if (i == 'Q') cr.setCastlingRight(Color::WHITE, queen_side, File::FILE_A); - if (i == 'k') cr.setCastlingRight(Color::BLACK, king_side, File::FILE_H); - if (i == 'q') cr.setCastlingRight(Color::BLACK, queen_side, File::FILE_A); + if (i == 'K') + cr.setCastlingRight(Color::WHITE, king_side, File::FILE_H); + if (i == 'Q') + cr.setCastlingRight(Color::WHITE, queen_side, File::FILE_A); + if (i == 'k') + cr.setCastlingRight(Color::BLACK, king_side, File::FILE_H); + if (i == 'q') + cr.setCastlingRight(Color::BLACK, queen_side, File::FILE_A); assert(i == 'K' || i == 'Q' || i == 'k' || i == 'q'); @@ -2706,23 +2997,31 @@ class Board { const auto parts = split_string_view<8>(position, '/'); - int offset = 8 * 2; - int square = 0; - Bitboard occ = 0ull; + int offset = 8 * 2; + int square = 0; + Bitboard occ = 0ull; - for (auto i = parts.rbegin(); i != parts.rend(); i++) { + for (auto i = parts.rbegin(); i != parts.rend(); i++) + { auto part = *i; - for (char curr : *part) { - if (isdigit(curr)) { + for (char curr : *part) + { + if (isdigit(curr)) + { square += (curr - '0'); - } else if (curr == '/') { + } + else if (curr == '/') + { square++; - } else { + } + else + { const auto p = Piece(std::string_view(&curr, 1)); const auto shift = (offset % 2 == 0 ? 4 : 0); - packed[offset / 2] |= convertMeaning(cr, stm, ep, Square(square), p) << shift; + packed[offset / 2] |= convertMeaning(cr, stm, ep, Square(square), p) + << shift; offset++; occ.set(square); @@ -2743,15 +3042,16 @@ class Board { return packed; } - static void decode(Board &board, const PackedBoard &compressed) { + static void decode(Board& board, const PackedBoard& compressed) { Bitboard occupied = 0ull; - for (int i = 0; i < 8; i++) { + for (int i = 0; i < 8; i++) + { occupied |= Bitboard(compressed[i]) << (56 - i * 8); } - int offset = 16; - int white_castle_idx = 0, black_castle_idx = 0; + int offset = 16; + int white_castle_idx = 0, black_castle_idx = 0; File white_castle[2] = {File::NO_FILE, File::NO_FILE}; File black_castle[2] = {File::NO_FILE, File::NO_FILE}; @@ -2771,12 +3071,14 @@ class Board { board.board_.fill(Piece::NONE); // place pieces back on the board - while (occupied) { + while (occupied) + { const auto sq = Square(occupied.pop()); const auto nibble = compressed[offset / 2] >> (offset % 2 == 0 ? 4 : 0) & 0b1111; const auto piece = convertPiece(nibble); - if (piece != Piece::NONE) { + if (piece != Piece::NONE) + { board.placePiece(piece, sq); offset++; @@ -2785,26 +3087,30 @@ class Board { // Piece has a special meaning, interpret it from the raw integer // pawn with ep square behind it - if (nibble == 12) { + if (nibble == 12) + { board.ep_sq_ = sq.ep_square(); // depending on the rank this is a white or black pawn auto color = sq.rank() == Rank::RANK_4 ? Color::WHITE : Color::BLACK; board.placePiece(Piece(PieceType::PAWN, color), sq); } // castling rights for white - else if (nibble == 13) { + else if (nibble == 13) + { assert(white_castle_idx < 2); white_castle[white_castle_idx++] = sq.file(); board.placePiece(Piece(PieceType::ROOK, Color::WHITE), sq); } // castling rights for black - else if (nibble == 14) { + else if (nibble == 14) + { assert(black_castle_idx < 2); black_castle[black_castle_idx++] = sq.file(); board.placePiece(Piece(PieceType::ROOK, Color::BLACK), sq); } // black to move - else if (nibble == 15) { + else if (nibble == 15) + { board.stm_ = Color::BLACK; board.placePiece(Piece(PieceType::KING, Color::BLACK), sq); } @@ -2813,8 +3119,10 @@ class Board { } // reapply castling - for (int i = 0; i < 2; i++) { - if (white_castle[i] != File::NO_FILE) { + for (int i = 0; i < 2; i++) + { + if (white_castle[i] != File::NO_FILE) + { const auto king_sq = board.kingSq(Color::WHITE); const auto file = white_castle[i]; const auto side = CastlingRights::closestSide(file, king_sq.file()); @@ -2822,7 +3130,8 @@ class Board { board.cr_.setCastlingRight(Color::WHITE, side, file); } - if (black_castle[i] != File::NO_FILE) { + if (black_castle[i] != File::NO_FILE) + { const auto king_sq = board.kingSq(Color::BLACK); const auto file = black_castle[i]; const auto side = CastlingRights::closestSide(file, king_sq.file()); @@ -2831,7 +3140,8 @@ class Board { } } - if (board.stm_ == Color::BLACK) { + if (board.stm_ == Color::BLACK) + { board.plies_++; } @@ -2843,7 +3153,8 @@ class Board { // for pieces with a special meaning return Piece::NONE since this is otherwise not used static Piece convertPiece(std::uint8_t piece) { - if (piece >= 12) return Piece::NONE; + if (piece >= 12) + return Piece::NONE; return Piece(Piece::underlying(piece)); } @@ -2851,23 +3162,31 @@ class Board { // 13 => any white rook with castling rights, side will be deduced from the file // 14 => any black rook with castling rights, side will be deduced from the file // 15 => black king and black is side to move - static std::uint8_t convertMeaning(const CastlingRights &cr, Color stm, Square ep, Square sq, Piece piece) { - if (piece.type() == PieceType::PAWN && ep != Square::NO_SQ) { - if (Square(static_cast(sq.index()) ^ 8) == ep) return 12; + static std::uint8_t + convertMeaning(const CastlingRights& cr, Color stm, Square ep, Square sq, Piece piece) { + if (piece.type() == PieceType::PAWN && ep != Square::NO_SQ) + { + if (Square(static_cast(sq.index()) ^ 8) == ep) + return 12; } - if (piece.type() == PieceType::ROOK) { - if (piece.color() == Color::WHITE && Square::back_rank(sq, Color::WHITE) && - (cr.getRookFile(Color::WHITE, CastlingRights::Side::KING_SIDE) == sq.file() || - cr.getRookFile(Color::WHITE, CastlingRights::Side::QUEEN_SIDE) == sq.file())) + if (piece.type() == PieceType::ROOK) + { + if (piece.color() == Color::WHITE && Square::back_rank(sq, Color::WHITE) + && (cr.getRookFile(Color::WHITE, CastlingRights::Side::KING_SIDE) == sq.file() + || cr.getRookFile(Color::WHITE, CastlingRights::Side::QUEEN_SIDE) + == sq.file())) return 13; - if (piece.color() == Color::BLACK && Square::back_rank(sq, Color::BLACK) && - (cr.getRookFile(Color::BLACK, CastlingRights::Side::KING_SIDE) == sq.file() || - cr.getRookFile(Color::BLACK, CastlingRights::Side::QUEEN_SIDE) == sq.file())) + if (piece.color() == Color::BLACK && Square::back_rank(sq, Color::BLACK) + && (cr.getRookFile(Color::BLACK, CastlingRights::Side::KING_SIDE) == sq.file() + || cr.getRookFile(Color::BLACK, CastlingRights::Side::QUEEN_SIDE) + == sq.file())) return 14; } - if (piece.type() == PieceType::KING && piece.color() == Color::BLACK && stm == Color::BLACK) { + if (piece.type() == PieceType::KING && piece.color() == Color::BLACK + && stm == Color::BLACK) + { return 15; } @@ -2884,14 +3203,14 @@ class Board { std::array pieces_bb_ = {}; std::array occ_bb_ = {}; - std::array board_ = {}; + std::array board_ = {}; - U64 key_ = 0ULL; - CastlingRights cr_ = {}; - std::uint16_t plies_ = 0; - Color stm_ = Color::WHITE; - Square ep_sq_ = Square::NO_SQ; - std::uint8_t hfm_ = 0; + U64 key_ = 0ULL; + CastlingRights cr_ = {}; + std::uint16_t plies_ = 0; + Color stm_ = Color::WHITE; + Square ep_sq_ = Square::NO_SQ; + std::uint8_t hfm_ = 0; bool chess960_ = false; @@ -2930,15 +3249,17 @@ class Board { board_[index] = piece; } - template + template bool setFenInternal(std::string_view fen) { original_fen_ = fen; reset(); - while (!fen.empty() && fen[0] == ' ') fen.remove_prefix(1); + while (!fen.empty() && fen[0] == ' ') + fen.remove_prefix(1); - if (fen.empty()) return false; + if (fen.empty()) + return false; const auto params = split_string_view<6>(fen); const auto position = params[0].has_value() ? *params[0] : ""; @@ -2948,9 +3269,11 @@ class Board { const auto half_move = params[4].has_value() ? *params[4] : "0"; const auto full_move = params[5].has_value() ? *params[5] : "1"; - if (position.empty()) return false; + if (position.empty()) + return false; - if (move_right != "w" && move_right != "b") return false; + if (move_right != "w" && move_right != "b") + return false; const auto half_move_opt = detail::parseStringViewToInt(half_move).value_or(0); hfm_ = half_move_opt; @@ -2960,36 +3283,52 @@ class Board { plies_ = plies_ * 2 - 2; - if (en_passant != "-") { - if (!Square::is_valid_string_sq(en_passant)) { + if (en_passant != "-") + { + if (!Square::is_valid_string_sq(en_passant)) + { return false; } ep_sq_ = Square(en_passant); - if (ep_sq_ == Square::NO_SQ) return false; + if (ep_sq_ == Square::NO_SQ) + return false; } stm_ = (move_right == "w") ? Color::WHITE : Color::BLACK; - if (stm_ == Color::BLACK) { + if (stm_ == Color::BLACK) + { plies_++; - } else { + } + else + { key_ ^= Zobrist::sideToMove(); } auto square = 56; - for (char curr : position) { - if (isdigit(curr)) { + for (char curr : position) + { + if (isdigit(curr)) + { square += (curr - '0'); - } else if (curr == '/') { + } + else if (curr == '/') + { square -= 16; - } else { + } + else + { auto p = Piece(std::string_view(&curr, 1)); - if (p == Piece::NONE || !Square::is_valid_sq(square) || at(square) != Piece::NONE) return false; + if (p == Piece::NONE || !Square::is_valid_sq(square) || at(square) != Piece::NONE) + return false; - if constexpr (ctor) { + if constexpr (ctor) + { placePieceInternal(p, Square(square)); - } else { + } + else + { placePiece(p, square); } @@ -2998,16 +3337,20 @@ class Board { } } - static const auto find_rook = [](const Board &board, CastlingRights::Side side, Color color) -> File { + static const auto find_rook = [](const Board& board, CastlingRights::Side side, + Color color) -> File { const auto king_side = CastlingRights::Side::KING_SIDE; const auto king_sq = board.kingSq(color); - const auto sq_corner = Square(side == king_side ? Square::SQ_H1 : Square::SQ_A1).relative_square(color); + const auto sq_corner = + Square(side == king_side ? Square::SQ_H1 : Square::SQ_A1).relative_square(color); const auto start = side == king_side ? king_sq + 1 : king_sq - 1; for (Square sq = start; (side == king_side ? sq <= sq_corner : sq >= sq_corner); - (side == king_side ? sq++ : sq--)) { - if (board.at(sq) == PieceType::ROOK && board.at(sq).color() == color) { + (side == king_side ? sq++ : sq--)) + { + if (board.at(sq) == PieceType::ROOK && board.at(sq).color() == color) + { return sq.file(); } } @@ -3016,13 +3359,16 @@ class Board { }; // Parse castling rights - for (char i : castling) { - if (i == '-') break; + for (char i : castling) + { + if (i == '-') + break; const auto king_side = CastlingRights::Side::KING_SIDE; const auto queen_side = CastlingRights::Side::QUEEN_SIDE; - if (!chess960_) { + if (!chess960_) + { if (i == 'K') cr_.setCastlingRight(Color::WHITE, king_side, File::FILE_H); else if (i == 'Q') @@ -3041,33 +3387,47 @@ class Board { const auto color = isupper(i) ? Color::WHITE : Color::BLACK; const auto king_sq = kingSq(color); - if (i == 'K' || i == 'k') { + if (i == 'K' || i == 'k') + { auto file = find_rook(*this, king_side, color); - if (file == File::NO_FILE) return false; + if (file == File::NO_FILE) + return false; cr_.setCastlingRight(color, king_side, file); - } else if (i == 'Q' || i == 'q') { + } + else if (i == 'Q' || i == 'q') + { auto file = find_rook(*this, queen_side, color); - if (file == File::NO_FILE) return false; + if (file == File::NO_FILE) + return false; cr_.setCastlingRight(color, queen_side, file); - } else { + } + else + { const auto file = File(std::string_view(&i, 1)); - if (file == File::NO_FILE) return false; + if (file == File::NO_FILE) + return false; const auto side = CastlingRights::closestSide(file, king_sq.file()); cr_.setCastlingRight(color, side, file); } } - if (ep_sq_ != Square::NO_SQ && !((ep_sq_.rank() == Rank::RANK_3 && stm_ == Color::BLACK) || - (ep_sq_.rank() == Rank::RANK_6 && stm_ == Color::WHITE))) { + if (ep_sq_ != Square::NO_SQ + && !((ep_sq_.rank() == Rank::RANK_3 && stm_ == Color::BLACK) + || (ep_sq_.rank() == Rank::RANK_6 && stm_ == Color::WHITE))) + { ep_sq_ = Square::NO_SQ; } - if (ep_sq_ != Square::NO_SQ) { + if (ep_sq_ != Square::NO_SQ) + { bool valid; - if (stm_ == Color::WHITE) { + if (stm_ == Color::WHITE) + { valid = movegen::isEpSquareValid(*this, ep_sq_); - } else { + } + else + { valid = movegen::isEpSquareValid(*this, ep_sq_); } @@ -3082,26 +3442,32 @@ class Board { assert(key_ == zobrist()); // init castling_path - for (Color c : {Color::WHITE, Color::BLACK}) { + for (Color c : {Color::WHITE, Color::BLACK}) + { const auto king_from = kingSq(c); - for (const auto side : {CastlingRights::Side::KING_SIDE, CastlingRights::Side::QUEEN_SIDE}) { - if (!cr_.has(c, side)) continue; + for (const auto side : + {CastlingRights::Side::KING_SIDE, CastlingRights::Side::QUEEN_SIDE}) + { + if (!cr_.has(c, side)) + continue; const auto rook_from = Square(cr_.getRookFile(c, side), king_from.rank()); - const auto king_to = Square::castling_king_square(side == CastlingRights::Side::KING_SIDE, c); - const auto rook_to = Square::castling_rook_square(side == CastlingRights::Side::KING_SIDE, c); + const auto king_to = + Square::castling_king_square(side == CastlingRights::Side::KING_SIDE, c); + const auto rook_to = + Square::castling_rook_square(side == CastlingRights::Side::KING_SIDE, c); castling_path[c][side == CastlingRights::Side::KING_SIDE] = - (movegen::between(rook_from, rook_to) | movegen::between(king_from, king_to)) & - ~(Bitboard::fromSquare(king_from) | Bitboard::fromSquare(rook_from)); + (movegen::between(rook_from, rook_to) | movegen::between(king_from, king_to)) + & ~(Bitboard::fromSquare(king_from) | Bitboard::fromSquare(rook_from)); } } return true; } - template + template std::array, N> static split_string_view(std::string_view fen, char delimiter = ' ') { std::array, N> arr = {}; @@ -3109,9 +3475,11 @@ class Board { std::size_t start = 0; std::size_t end = 0; - for (std::size_t i = 0; i < N; i++) { + for (std::size_t i = 0; i < N; i++) + { end = fen.find(delimiter, start); - if (end == std::string::npos) { + if (end == std::string::npos) + { arr[i] = fen.substr(start); break; } @@ -3141,9 +3509,11 @@ class Board { std::string original_fen_; }; -inline std::ostream &operator<<(std::ostream &os, const Board &b) { - for (int i = 63; i >= 0; i -= 8) { - for (int j = 7; j >= 0; j--) { +inline std::ostream& operator<<(std::ostream& os, const Board& b) { + for (int i = 63; i >= 0; i -= 8) + { + for (int j = 7; j >= 0; j--) + { os << " " << static_cast(b.board_[i - j]); } @@ -3163,37 +3533,49 @@ inline std::ostream &operator<<(std::ostream &os, const Board &b) { return os; } -inline CheckType Board::givesCheck(const Move &m) const noexcept { - const static auto getSniper = [](const Board *board, Square ksq, Bitboard oc) { +inline CheckType Board::givesCheck(const Move& m) const noexcept { + const static auto getSniper = [](const Board* board, Square ksq, Bitboard oc) { const auto us_occ = board->us(board->sideToMove()); - const auto bishop = attacks::bishop(ksq, oc) & board->pieces(PieceType::BISHOP, PieceType::QUEEN) & us_occ; - const auto rook = attacks::rook(ksq, oc) & board->pieces(PieceType::ROOK, PieceType::QUEEN) & us_occ; + const auto bishop = + attacks::bishop(ksq, oc) & board->pieces(PieceType::BISHOP, PieceType::QUEEN) & us_occ; + const auto rook = + attacks::rook(ksq, oc) & board->pieces(PieceType::ROOK, PieceType::QUEEN) & us_occ; return (bishop | rook); }; assert(at(m.from()).color() == stm_); - const Square from = m.from(); - const Square to = m.to(); - const Square ksq = kingSq(~stm_); - const Bitboard toBB = Bitboard::fromSquare(to); - const PieceType pt = at(from).type(); + const Square from = m.from(); + const Square to = m.to(); + const Square ksq = kingSq(~stm_); + const Bitboard toBB = Bitboard::fromSquare(to); + const PieceType pt = at(from).type(); Bitboard fromKing = 0ull; - if (pt == PieceType::PAWN) { + if (pt == PieceType::PAWN) + { fromKing = attacks::pawn(~stm_, ksq); - } else if (pt == PieceType::KNIGHT) { + } + else if (pt == PieceType::KNIGHT) + { fromKing = attacks::knight(ksq); - } else if (pt == PieceType::BISHOP) { + } + else if (pt == PieceType::BISHOP) + { fromKing = attacks::bishop(ksq, occ()); - } else if (pt == PieceType::ROOK) { + } + else if (pt == PieceType::ROOK) + { fromKing = attacks::rook(ksq, occ()); - } else if (pt == PieceType::QUEEN) { + } + else if (pt == PieceType::QUEEN) + { fromKing = attacks::queen(ksq, occ()); } - if (fromKing & toBB) return CheckType::DIRECT_CHECK; + if (fromKing & toBB) + return CheckType::DIRECT_CHECK; // Discovery check const Bitboard fromBB = Bitboard::fromSquare(from); @@ -3201,47 +3583,54 @@ inline CheckType Board::givesCheck(const Move &m) const noexcept { Bitboard sniper = getSniper(this, ksq, oc); - while (sniper) { + while (sniper) + { Square sq = sniper.pop(); - return (!(movegen::between(ksq, sq) & toBB) || m.typeOf() == Move::CASTLING) ? CheckType::DISCOVERY_CHECK - : CheckType::NO_CHECK; + return (!(movegen::between(ksq, sq) & toBB) || m.typeOf() == Move::CASTLING) + ? CheckType::DISCOVERY_CHECK + : CheckType::NO_CHECK; } - switch (m.typeOf()) { - case Move::NORMAL: - return CheckType::NO_CHECK; + switch (m.typeOf()) + { + case Move::NORMAL : + return CheckType::NO_CHECK; - case Move::PROMOTION: { - Bitboard attacks = 0ull; + case Move::PROMOTION : { + Bitboard attacks = 0ull; - switch (m.promotionType()) { - case static_cast(PieceType::KNIGHT): - attacks = attacks::knight(to); - break; - case static_cast(PieceType::BISHOP): - attacks = attacks::bishop(to, oc); - break; - case static_cast(PieceType::ROOK): - attacks = attacks::rook(to, oc); - break; - case static_cast(PieceType::QUEEN): - attacks = attacks::queen(to, oc); - } - - return (attacks & pieces(PieceType::KING, ~stm_)) ? CheckType::DIRECT_CHECK : CheckType::NO_CHECK; + switch (m.promotionType()) + { + case static_cast(PieceType::KNIGHT) : + attacks = attacks::knight(to); + break; + case static_cast(PieceType::BISHOP) : + attacks = attacks::bishop(to, oc); + break; + case static_cast(PieceType::ROOK) : + attacks = attacks::rook(to, oc); + break; + case static_cast(PieceType::QUEEN) : + attacks = attacks::queen(to, oc); } - case Move::ENPASSANT: { - Square capSq(to.file(), from.rank()); - return (getSniper(this, ksq, (oc ^ Bitboard::fromSquare(capSq)) | toBB)) ? CheckType::DISCOVERY_CHECK - : CheckType::NO_CHECK; - } + return (attacks & pieces(PieceType::KING, ~stm_)) ? CheckType::DIRECT_CHECK + : CheckType::NO_CHECK; + } - case Move::CASTLING: { - Square rookTo = Square::castling_rook_square(to > from, stm_); - return (attacks::rook(ksq, occ()) & Bitboard::fromSquare(rookTo)) ? CheckType::DISCOVERY_CHECK - : CheckType::NO_CHECK; - } + case Move::ENPASSANT : { + Square capSq(to.file(), from.rank()); + return (getSniper(this, ksq, (oc ^ Bitboard::fromSquare(capSq)) | toBB)) + ? CheckType::DISCOVERY_CHECK + : CheckType::NO_CHECK; + } + + case Move::CASTLING : { + Square rookTo = Square::castling_rook_square(to > from, stm_); + return (attacks::rook(ksq, occ()) & Bitboard::fromSquare(rookTo)) + ? CheckType::DISCOVERY_CHECK + : CheckType::NO_CHECK; + } } assert(false); @@ -3252,25 +3641,26 @@ inline CheckType Board::givesCheck(const Move &m) const noexcept { namespace chess { -template +template [[nodiscard]] inline constexpr Bitboard attacks::shift(const Bitboard b) { - switch (direction) { - case Direction::NORTH: - return b << 8; - case Direction::SOUTH: - return b >> 8; - case Direction::NORTH_WEST: - return (b & ~MASK_FILE[0]) << 7; - case Direction::WEST: - return (b & ~MASK_FILE[0]) >> 1; - case Direction::SOUTH_WEST: - return (b & ~MASK_FILE[0]) >> 9; - case Direction::NORTH_EAST: - return (b & ~MASK_FILE[7]) << 9; - case Direction::EAST: - return (b & ~MASK_FILE[7]) << 1; - case Direction::SOUTH_EAST: - return (b & ~MASK_FILE[7]) >> 7; + switch (direction) + { + case Direction::NORTH : + return b << 8; + case Direction::SOUTH : + return b >> 8; + case Direction::NORTH_WEST : + return (b & ~MASK_FILE[0]) << 7; + case Direction::WEST : + return (b & ~MASK_FILE[0]) >> 1; + case Direction::SOUTH_WEST : + return (b & ~MASK_FILE[0]) >> 9; + case Direction::NORTH_EAST : + return (b & ~MASK_FILE[7]) << 9; + case Direction::EAST : + return (b & ~MASK_FILE[7]) << 1; + case Direction::SOUTH_EAST : + return (b & ~MASK_FILE[7]) >> 7; } assert(false); @@ -3278,21 +3668,25 @@ template return {}; } -template +template [[nodiscard]] inline Bitboard attacks::pawnLeftAttacks(const Bitboard pawns) { return c == Color::WHITE ? (pawns << 7) & ~MASK_FILE[static_cast(File::FILE_H)] : (pawns >> 7) & ~MASK_FILE[static_cast(File::FILE_A)]; } -template +template [[nodiscard]] inline Bitboard attacks::pawnRightAttacks(const Bitboard pawns) { return c == Color::WHITE ? (pawns << 9) & ~MASK_FILE[static_cast(File::FILE_A)] : (pawns >> 9) & ~MASK_FILE[static_cast(File::FILE_H)]; } -[[nodiscard]] inline Bitboard attacks::pawn(Color c, Square sq) noexcept { return PawnAttacks[c][sq.index()]; } +[[nodiscard]] inline Bitboard attacks::pawn(Color c, Square sq) noexcept { + return PawnAttacks[c][sq.index()]; +} -[[nodiscard]] inline Bitboard attacks::knight(Square sq) noexcept { return KnightAttacks[sq.index()]; } +[[nodiscard]] inline Bitboard attacks::knight(Square sq) noexcept { + return KnightAttacks[sq.index()]; +} [[nodiscard]] inline Bitboard attacks::bishop(Square sq, Bitboard occupied) noexcept { return BishopTable[sq.index()].attacks[BishopTable[sq.index()](occupied)]; @@ -3308,7 +3702,8 @@ template [[nodiscard]] inline Bitboard attacks::king(Square sq) noexcept { return KingAttacks[sq.index()]; } -[[nodiscard]] inline Bitboard attacks::attackers(const Board &board, Color color, Square square) noexcept { +[[nodiscard]] inline Bitboard +attacks::attackers(const Board& board, Color color, Square square) noexcept { const auto queens = board.pieces(PieceType::QUEEN, color); const auto occupied = board.occ(); @@ -3322,51 +3717,61 @@ template return atks & occupied; } -template +template [[nodiscard]] inline Bitboard attacks::slider(Square sq, Bitboard occupied) noexcept { static_assert(pt == PieceType::BISHOP || pt == PieceType::ROOK || pt == PieceType::QUEEN, "PieceType must be a slider!"); - if constexpr (pt == PieceType::BISHOP) return bishop(sq, occupied); - if constexpr (pt == PieceType::ROOK) return rook(sq, occupied); - if constexpr (pt == PieceType::QUEEN) return queen(sq, occupied); + if constexpr (pt == PieceType::BISHOP) + return bishop(sq, occupied); + if constexpr (pt == PieceType::ROOK) + return rook(sq, occupied); + if constexpr (pt == PieceType::QUEEN) + return queen(sq, occupied); } -template +template [[nodiscard]] inline Bitboard attacks::sliderAttacks(Square sq, Bitboard occupied) noexcept { - static constexpr int dirs[2][4][2] = {{1, 1, 1, -1, -1, -1, -1, 1}, {1, 0, 0, -1, -1, 0, 0, 1}}; + static constexpr int dirs[2][4][2] = {{{1, 1}, {1, -1}, {-1, -1}, {-1, 1}}, + {{1, 0}, {0, -1}, {-1, 0}, {0, 1}}}; Bitboard attacks = 0ull; File pf = sq.file(); Rank pr = sq.rank(); - for (int i = 0; i < 4; ++i) { + for (int i = 0; i < 4; ++i) + { int off_f = dirs[ISROOK][i][0]; int off_r = dirs[ISROOK][i][1]; File f; Rank r; - for (f = pf + off_f, r = pr + off_r; Square::is_valid(r, f); f += off_f, r += off_r) { + for (f = pf + off_f, r = pr + off_r; Square::is_valid(r, f); f += off_f, r += off_r) + { const auto index = Square(f, r).index(); attacks.set(index); - if (occupied.check(index)) break; + if (occupied.check(index)) + break; } } return attacks; } -inline void attacks::initSliders(Square sq, Magic table[], U64 magic, - const std::function &attacks) { +inline void attacks::initSliders(Square sq, + Magic table[], + U64 magic, + const std::function& attacks) { // The edges of the board are not considered for the attacks // i.e. for the sq h7 edges will be a1-h1, a1-a8, a8-h8, ignoring the edge of the current square - const Bitboard edges = ((Bitboard(Rank::RANK_1) | Bitboard(Rank::RANK_8)) & ~Bitboard(sq.rank())) | - ((Bitboard(File::FILE_A) | Bitboard(File::FILE_H)) & ~Bitboard(sq.file())); + const Bitboard edges = + ((Bitboard(Rank::RANK_1) | Bitboard(Rank::RANK_8)) & ~Bitboard(sq.rank())) + | ((Bitboard(File::FILE_A) | Bitboard(File::FILE_H)) & ~Bitboard(sq.file())); U64 occ = 0ULL; - auto &table_sq = table[sq.index()]; + auto& table_sq = table[sq.index()]; #ifndef CHESS_USE_PEXT table_sq.magic = magic; @@ -3376,11 +3781,14 @@ inline void attacks::initSliders(Square sq, Magic table[], U64 magic, table_sq.shift = 64 - Bitboard(table_sq.mask).count(); #endif - if (sq < 64 - 1) { - table[sq.index() + 1].attacks = table_sq.attacks + (1ull << Bitboard(table_sq.mask).count()); + if (sq < 64 - 1) + { + table[sq.index() + 1].attacks = + table_sq.attacks + (1ull << Bitboard(table_sq.mask).count()); } - do { + do + { table_sq.attacks[table_sq(occ)] = attacks(sq, occ); occ = (occ - table_sq.mask) & table_sq.mask; } while (occ); @@ -3390,7 +3798,8 @@ inline void attacks::initAttacks() { BishopTable[0].attacks = BishopAttacks; RookTable[0].attacks = RookAttacks; - for (int i = 0; i < 64; i++) { + for (int i = 0; i < 64; i++) + { initSliders(static_cast(i), BishopTable, BishopMagics[i], sliderAttacks); initSliders(static_cast(i), RookTable, RookMagics[i], sliderAttacks); } @@ -3398,7 +3807,6 @@ inline void attacks::initAttacks() { } // namespace chess - namespace chess { inline auto movegen::init_squares_between() { @@ -3408,12 +3816,16 @@ inline auto movegen::init_squares_between() { return (pt == PieceType::BISHOP) ? attacks::bishop(sq, occ) : attacks::rook(sq, occ); }; - for (int sq1 = 0; sq1 < 64; ++sq1) { - for (PieceType pt : {PieceType::BISHOP, PieceType::ROOK}) { - for (int sq2 = 0; sq2 < 64; ++sq2) { - if (att(pt, sq1, 0).check(sq2)) { - squares_between_bb[sq1][sq2] = - att(pt, sq1, Bitboard::fromSquare(sq2)) & att(pt, sq2, Bitboard::fromSquare(sq1)); + for (int sq1 = 0; sq1 < 64; ++sq1) + { + for (PieceType pt : {PieceType::BISHOP, PieceType::ROOK}) + { + for (int sq2 = 0; sq2 < 64; ++sq2) + { + if (att(pt, sq1, 0).check(sq2)) + { + squares_between_bb[sq1][sq2] = att(pt, sq1, Bitboard::fromSquare(sq2)) + & att(pt, sq2, Bitboard::fromSquare(sq1)); } squares_between_bb[sq1][sq2].set(sq2); } @@ -3423,8 +3835,8 @@ inline auto movegen::init_squares_between() { return squares_between_bb; } -template -[[nodiscard]] inline std::pair movegen::checkMask(const Board &board, Square sq) { +template +[[nodiscard]] inline std::pair movegen::checkMask(const Board& board, Square sq) { const auto opp_knight = board.pieces(PieceType::KNIGHT, ~c); const auto opp_bishop = board.pieces(PieceType::BISHOP, ~c); const auto opp_rook = board.pieces(PieceType::ROOK, ~c); @@ -3448,15 +3860,18 @@ template // check for bishop checks Bitboard bishop_attacks = attacks::bishop(sq, board.occ()) & (opp_bishop | opp_queen); - if (bishop_attacks) { + if (bishop_attacks) + { mask |= between(sq, bishop_attacks.lsb()); checks++; } Bitboard rook_attacks = attacks::rook(sq, board.occ()) & (opp_rook | opp_queen); - if (rook_attacks) { - if (rook_attacks.count() > 1) { + if (rook_attacks) + { + if (rook_attacks.count() > 1) + { checks = 2; return {mask, checks}; } @@ -3465,16 +3880,17 @@ template checks++; } - if (!mask) { + if (!mask) + { return {constants::DEFAULT_CHECKMASK, checks}; } return {mask, checks}; } -template -[[nodiscard]] inline Bitboard movegen::pinMask(const Board &board, Square sq, Bitboard occ_opp, - Bitboard occ_us) noexcept { +template +[[nodiscard]] inline Bitboard +movegen::pinMask(const Board& board, Square sq, Bitboard occ_opp, Bitboard occ_us) noexcept { static_assert(pt == PieceType::BISHOP || pt == PieceType::ROOK, "Only bishop or rook allowed!"); const auto opp_pt_queen = board.pieces(pt, PieceType::QUEEN) & board.us(~c); @@ -3483,20 +3899,23 @@ template Bitboard pin = 0ull; - while (pt_attacks) { + while (pt_attacks) + { const auto possible_pin = between(sq, pt_attacks.pop()); - if ((possible_pin & occ_us).count() == 1) pin |= possible_pin; + if ((possible_pin & occ_us).count() == 1) + pin |= possible_pin; } return pin; } -template -[[nodiscard]] inline Bitboard movegen::seenSquares(const Board &board, Bitboard enemy_empty) { - auto king_sq = board.kingSq(~c); +template +[[nodiscard]] inline Bitboard movegen::seenSquares(const Board& board, Bitboard enemy_empty) { + auto king_sq = board.kingSq(~c); Bitboard map_king_atk = attacks::king(king_sq) & enemy_empty; - if (map_king_atk == Bitboard(0ull) && !board.chess960()) return 0ull; + if (map_king_atk == Bitboard(0ull) && !board.chess960()) + return 0ull; auto occ = board.occ() ^ Bitboard::fromSquare(king_sq); auto queens = board.pieces(PieceType::QUEEN, c); @@ -3507,15 +3926,18 @@ template Bitboard seen = attacks::pawnLeftAttacks(pawns) | attacks::pawnRightAttacks(pawns); - while (knights) { + while (knights) + { seen |= attacks::knight(knights.pop()); } - while (bishops) { + while (bishops) + { seen |= attacks::bishop(bishops.pop(), occ); } - while (rooks) { + while (rooks) + { seen |= attacks::rook(rooks.pop(), occ); } @@ -3524,9 +3946,13 @@ template return seen; } -template -inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitboard pin_d, Bitboard pin_hv, - Bitboard checkmask, Bitboard occ_opp) { +template +inline void movegen::generatePawnMoves(const Board& board, + Movelist& moves, + Bitboard pin_d, + Bitboard pin_hv, + Bitboard checkmask, + Bitboard occ_opp) { // flipped for black constexpr auto UP = make_direction(Direction::NORTH, c); @@ -3547,8 +3973,10 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb const Bitboard unpinned_pawns_lr = pawns_lr & ~pin_d; const Bitboard pinned_pawns_lr = pawns_lr & pin_d; - auto l_pawns = attacks::shift(unpinned_pawns_lr) | (attacks::shift(pinned_pawns_lr) & pin_d); - auto r_pawns = attacks::shift(unpinned_pawns_lr) | (attacks::shift(pinned_pawns_lr) & pin_d); + auto l_pawns = attacks::shift(unpinned_pawns_lr) + | (attacks::shift(pinned_pawns_lr) & pin_d); + auto r_pawns = attacks::shift(unpinned_pawns_lr) + | (attacks::shift(pinned_pawns_lr) & pin_d); // Prune moves that don't capture a piece and are not on the checkmask. l_pawns &= occ_opp & checkmask; @@ -3567,18 +3995,21 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb // Prune moves that are not on the checkmask. Bitboard single_push = (single_push_unpinned | single_push_pinned) & checkmask; - Bitboard double_push = ((attacks::shift(single_push_unpinned & DOUBLE_PUSH_RANK) & ~board.occ()) | - (attacks::shift(single_push_pinned & DOUBLE_PUSH_RANK) & ~board.occ())) & - checkmask; + Bitboard double_push = + ((attacks::shift(single_push_unpinned & DOUBLE_PUSH_RANK) & ~board.occ()) + | (attacks::shift(single_push_pinned & DOUBLE_PUSH_RANK) & ~board.occ())) + & checkmask; - if (pawns & RANK_B_PROMO) { + if (pawns & RANK_B_PROMO) + { Bitboard promo_left = l_pawns & RANK_PROMO; Bitboard promo_right = r_pawns & RANK_PROMO; Bitboard promo_push = single_push & RANK_PROMO; // Skip capturing promotions if we are only generating quiet moves. // Generates at ALL and CAPTURE - while (mt != MoveGenType::QUIET && promo_left) { + while (mt != MoveGenType::QUIET && promo_left) + { const auto index = promo_left.pop(); moves.add(Move::make(index + DOWN_RIGHT, index, PieceType::QUEEN)); moves.add(Move::make(index + DOWN_RIGHT, index, PieceType::ROOK)); @@ -3588,7 +4019,8 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb // Skip capturing promotions if we are only generating quiet moves. // Generates at ALL and CAPTURE - while (mt != MoveGenType::QUIET && promo_right) { + while (mt != MoveGenType::QUIET && promo_right) + { const auto index = promo_right.pop(); moves.add(Move::make(index + DOWN_LEFT, index, PieceType::QUEEN)); moves.add(Move::make(index + DOWN_LEFT, index, PieceType::ROOK)); @@ -3598,7 +4030,8 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb // Skip quiet promotions if we are only generating captures. // Generates at ALL and QUIET - while (mt != MoveGenType::CAPTURE && promo_push) { + while (mt != MoveGenType::CAPTURE && promo_push) + { const auto index = promo_push.pop(); moves.add(Move::make(index + DOWN, index, PieceType::QUEEN)); moves.add(Move::make(index + DOWN, index, PieceType::ROOK)); @@ -3611,46 +4044,54 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb l_pawns &= ~RANK_PROMO; r_pawns &= ~RANK_PROMO; - while (mt != MoveGenType::QUIET && l_pawns) { + while (mt != MoveGenType::QUIET && l_pawns) + { const auto index = l_pawns.pop(); moves.add(Move::make(index + DOWN_RIGHT, index)); } - while (mt != MoveGenType::QUIET && r_pawns) { + while (mt != MoveGenType::QUIET && r_pawns) + { const auto index = r_pawns.pop(); moves.add(Move::make(index + DOWN_LEFT, index)); } - while (mt != MoveGenType::CAPTURE && single_push) { + while (mt != MoveGenType::CAPTURE && single_push) + { const auto index = single_push.pop(); moves.add(Move::make(index + DOWN, index)); } - while (mt != MoveGenType::CAPTURE && double_push) { + while (mt != MoveGenType::CAPTURE && double_push) + { const auto index = double_push.pop(); moves.add(Move::make(index + DOWN + DOWN, index)); } - if constexpr (mt == MoveGenType::QUIET) return; + if constexpr (mt == MoveGenType::QUIET) + return; const Square ep = board.enpassantSq(); - if (ep != Square::NO_SQ) { + if (ep != Square::NO_SQ) + { auto m = generateEPMove(board, checkmask, pin_d, pawns_lr, ep, c); - for (const auto &move : m) { - if (move != Move::NO_MOVE) moves.add(move); + for (const auto& move : m) + { + if (move != Move::NO_MOVE) + moves.add(move); } } } -[[nodiscard]] inline std::array movegen::generateEPMove(const Board &board, Bitboard checkmask, Bitboard pin_d, - Bitboard pawns_lr, Square ep, Color c) { - assert((ep.rank() == Rank::RANK_3 && board.sideToMove() == Color::BLACK) || - (ep.rank() == Rank::RANK_6 && board.sideToMove() == Color::WHITE)); +[[nodiscard]] inline std::array movegen::generateEPMove( + const Board& board, Bitboard checkmask, Bitboard pin_d, Bitboard pawns_lr, Square ep, Color c) { + assert((ep.rank() == Rank::RANK_3 && board.sideToMove() == Color::BLACK) + || (ep.rank() == Rank::RANK_6 && board.sideToMove() == Color::WHITE)); std::array moves = {Move::NO_MOVE, Move::NO_MOVE}; - int i = 0; + int i = 0; const auto DOWN = make_direction(Direction::SOUTH, c); const auto epPawnSq = ep + DOWN; @@ -3660,16 +4101,18 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb that just moved are not on the checkmask en passant is not available. */ - if ((checkmask & (Bitboard::fromSquare(epPawnSq) | Bitboard::fromSquare(ep))) == 0ull) return moves; + if ((checkmask & (Bitboard::fromSquare(epPawnSq) | Bitboard::fromSquare(ep))) == 0ull) + return moves; - const Square kSQ = board.kingSq(c); + const Square kSQ = board.kingSq(c); const Bitboard kingMask = Bitboard::fromSquare(kSQ) & epPawnSq.rank().bb(); const Bitboard enemyQueenRook = board.pieces(PieceType::ROOK, PieceType::QUEEN) & board.us(~c); auto epBB = attacks::pawn(~c, ep) & pawns_lr; // For one en passant square two pawns could potentially take there. - while (epBB) { + while (epBB) + { const auto from = epBB.pop(); const auto to = ep; @@ -3677,7 +4120,8 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb If the pawn is pinned but the en passant square is not on the pin mask then the move is illegal. */ - if ((Bitboard::fromSquare(from) & pin_d) && !(pin_d & Bitboard::fromSquare(ep))) continue; + if ((Bitboard::fromSquare(from) & pin_d) && !(pin_d & Bitboard::fromSquare(ep))) + continue; const auto connectingPawns = Bitboard::fromSquare(epPawnSq) | Bitboard::fromSquare(from); @@ -3691,7 +4135,9 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb */ const auto isPossiblePin = kingMask && enemyQueenRook; - if (isPossiblePin && (attacks::rook(kSQ, board.occ() ^ connectingPawns) & enemyQueenRook) != 0ull) break; + if (isPossiblePin + && (attacks::rook(kSQ, board.occ() ^ connectingPawns) & enemyQueenRook) != 0ull) + break; moves[i++] = Move::make(from, to); } @@ -3699,29 +4145,36 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb return moves; } -[[nodiscard]] inline Bitboard movegen::generateKnightMoves(Square sq) { return attacks::knight(sq); } +[[nodiscard]] inline Bitboard movegen::generateKnightMoves(Square sq) { + return attacks::knight(sq); +} -[[nodiscard]] inline Bitboard movegen::generateBishopMoves(Square sq, Bitboard pin_d, Bitboard occ_all) { +[[nodiscard]] inline Bitboard +movegen::generateBishopMoves(Square sq, Bitboard pin_d, Bitboard occ_all) { // The Bishop is pinned diagonally thus can only move diagonally. - if (pin_d & Bitboard::fromSquare(sq)) return attacks::bishop(sq, occ_all) & pin_d; + if (pin_d & Bitboard::fromSquare(sq)) + return attacks::bishop(sq, occ_all) & pin_d; return attacks::bishop(sq, occ_all); } -[[nodiscard]] inline Bitboard movegen::generateRookMoves(Square sq, Bitboard pin_hv, Bitboard occ_all) { +[[nodiscard]] inline Bitboard +movegen::generateRookMoves(Square sq, Bitboard pin_hv, Bitboard occ_all) { // The Rook is pinned horizontally thus can only move horizontally. - if (pin_hv & Bitboard::fromSquare(sq)) return attacks::rook(sq, occ_all) & pin_hv; + if (pin_hv & Bitboard::fromSquare(sq)) + return attacks::rook(sq, occ_all) & pin_hv; return attacks::rook(sq, occ_all); } -[[nodiscard]] inline Bitboard movegen::generateQueenMoves(Square sq, Bitboard pin_d, Bitboard pin_hv, - Bitboard occ_all) { +[[nodiscard]] inline Bitboard +movegen::generateQueenMoves(Square sq, Bitboard pin_d, Bitboard pin_hv, Bitboard occ_all) { Bitboard moves = 0ULL; if (pin_d & Bitboard::fromSquare(sq)) moves |= attacks::bishop(sq, occ_all) & pin_d; else if (pin_hv & Bitboard::fromSquare(sq)) moves |= attacks::rook(sq, occ_all) & pin_hv; - else { + else + { moves |= attacks::rook(sq, occ_all); moves |= attacks::bishop(sq, occ_all); } @@ -3729,34 +4182,45 @@ inline void movegen::generatePawnMoves(const Board &board, Movelist &moves, Bitb return moves; } -[[nodiscard]] inline Bitboard movegen::generateKingMoves(Square sq, Bitboard seen, Bitboard movable_square) { +[[nodiscard]] inline Bitboard +movegen::generateKingMoves(Square sq, Bitboard seen, Bitboard movable_square) { return attacks::king(sq) & movable_square & ~seen; } -template -[[nodiscard]] inline Bitboard movegen::generateCastleMoves(const Board &board, Square sq, Bitboard seen, - Bitboard pin_hv) noexcept { - if (!Square::back_rank(sq, c) || !board.castlingRights().has(c)) return 0ull; +template +[[nodiscard]] inline Bitboard movegen::generateCastleMoves(const Board& board, + Square sq, + Bitboard seen, + Bitboard pin_hv) noexcept { + if (!Square::back_rank(sq, c) || !board.castlingRights().has(c)) + return 0ull; const auto rights = board.castlingRights(); Bitboard moves = 0ull; - for (const auto side : {Board::CastlingRights::Side::KING_SIDE, Board::CastlingRights::Side::QUEEN_SIDE}) { - if (!rights.has(c, side)) continue; + for (const auto side : + {Board::CastlingRights::Side::KING_SIDE, Board::CastlingRights::Side::QUEEN_SIDE}) + { + if (!rights.has(c, side)) + continue; const auto is_king_side = side == Board::CastlingRights::Side::KING_SIDE; // No pieces on the castling path - if (board.occ() & board.getCastlingPath(c, is_king_side)) continue; + if (board.occ() & board.getCastlingPath(c, is_king_side)) + continue; // No attacks on the king path const auto king_to = Square::castling_king_square(is_king_side, c); - if (between(sq, king_to) & seen) continue; + if (between(sq, king_to) & seen) + continue; // Chess960: Rook is pinned on the backrank. - const auto from_rook_bb = Bitboard::fromSquare(Square(rights.getRookFile(c, side), sq.rank())); - if (board.chess960() && (pin_hv & board.us(board.sideToMove()) & from_rook_bb)) continue; + const auto from_rook_bb = + Bitboard::fromSquare(Square(rights.getRookFile(c, side), sq.rank())); + if (board.chess960() && (pin_hv & board.us(board.sideToMove()) & from_rook_bb)) + continue; moves |= from_rook_bb; } @@ -3764,20 +4228,22 @@ template return moves; } -template -inline void movegen::whileBitboardAdd(Movelist &movelist, Bitboard mask, T func) { - while (mask) { - const Square from = mask.pop(); - auto moves = func(from); - while (moves) { +template +inline void movegen::whileBitboardAdd(Movelist& movelist, Bitboard mask, T func) { + while (mask) + { + const Square from = mask.pop(); + auto moves = func(from); + while (moves) + { const Square to = moves.pop(); movelist.add(Move::make(from, to)); } } } -template -inline void movegen::legalmoves(Movelist &movelist, const Board &board, int pieces) { +template +inline void movegen::legalmoves(Movelist& movelist, const Board& board, int pieces) { /* The size of the movelist might not be 0! This is done on purpose since it enables @@ -3807,16 +4273,19 @@ inline void movegen::legalmoves(Movelist &movelist, const Board &board, int piec else // QUIET moves movable_square = ~occ_all; - if (pieces & PieceGenType::KING) { + if (pieces & PieceGenType::KING) + { Bitboard seen = seenSquares<~c>(board, opp_empty); whileBitboardAdd(movelist, Bitboard::fromSquare(king_sq), [&](Square sq) { return generateKingMoves(sq, seen, movable_square); }); - if (mt != MoveGenType::CAPTURE && checks == 0) { + if (mt != MoveGenType::CAPTURE && checks == 0) + { Bitboard moves_bb = generateCastleMoves(board, king_sq, seen, pin_hv); - while (moves_bb) { + while (moves_bb) + { Square to = moves_bb.pop(); movelist.add(Move::make(king_sq, to)); } @@ -3824,50 +4293,60 @@ inline void movegen::legalmoves(Movelist &movelist, const Board &board, int piec } // Early return for double check as described earlier - if (checks == 2) return; + if (checks == 2) + return; // Moves have to be on the checkmask movable_square &= checkmask; // Add the moves to the movelist. - if (pieces & PieceGenType::PAWN) { + if (pieces & PieceGenType::PAWN) + { generatePawnMoves(board, movelist, pin_d, pin_hv, checkmask, occ_opp); } - if (pieces & PieceGenType::KNIGHT) { + if (pieces & PieceGenType::KNIGHT) + { // Prune knights that are pinned since these cannot move. Bitboard knights_mask = board.pieces(PieceType::KNIGHT, c) & ~(pin_d | pin_hv); - whileBitboardAdd(movelist, knights_mask, [&](Square sq) { return generateKnightMoves(sq) & movable_square; }); + whileBitboardAdd(movelist, knights_mask, + [&](Square sq) { return generateKnightMoves(sq) & movable_square; }); } - if (pieces & PieceGenType::BISHOP) { + if (pieces & PieceGenType::BISHOP) + { // Prune horizontally pinned bishops Bitboard bishops_mask = board.pieces(PieceType::BISHOP, c) & ~pin_hv; - whileBitboardAdd(movelist, bishops_mask, - [&](Square sq) { return generateBishopMoves(sq, pin_d, occ_all) & movable_square; }); + whileBitboardAdd(movelist, bishops_mask, [&](Square sq) { + return generateBishopMoves(sq, pin_d, occ_all) & movable_square; + }); } - if (pieces & PieceGenType::ROOK) { + if (pieces & PieceGenType::ROOK) + { // Prune diagonally pinned rooks Bitboard rooks_mask = board.pieces(PieceType::ROOK, c) & ~pin_d; - whileBitboardAdd(movelist, rooks_mask, - [&](Square sq) { return generateRookMoves(sq, pin_hv, occ_all) & movable_square; }); + whileBitboardAdd(movelist, rooks_mask, [&](Square sq) { + return generateRookMoves(sq, pin_hv, occ_all) & movable_square; + }); } - if (pieces & PieceGenType::QUEEN) { + if (pieces & PieceGenType::QUEEN) + { // Prune double pinned queens Bitboard queens_mask = board.pieces(PieceType::QUEEN, c) & ~(pin_d & pin_hv); - whileBitboardAdd(movelist, queens_mask, - [&](Square sq) { return generateQueenMoves(sq, pin_d, pin_hv, occ_all) & movable_square; }); + whileBitboardAdd(movelist, queens_mask, [&](Square sq) { + return generateQueenMoves(sq, pin_d, pin_hv, occ_all) & movable_square; + }); } } -template -inline void movegen::legalmoves(Movelist &movelist, const Board &board, int pieces) { +template +inline void movegen::legalmoves(Movelist& movelist, const Board& board, int pieces) { movelist.clear(); if (board.sideToMove() == Color::WHITE) @@ -3876,25 +4355,27 @@ inline void movegen::legalmoves(Movelist &movelist, const Board &board, int piec legalmoves(movelist, board, pieces); } -template -inline bool movegen::isEpSquareValid(const Board &board, Square ep) { +template +inline bool movegen::isEpSquareValid(const Board& board, Square ep) { const auto stm = board.sideToMove(); Bitboard occ_us = board.us(stm); Bitboard occ_opp = board.us(~stm); - auto king_sq = board.kingSq(stm); + auto king_sq = board.kingSq(stm); const auto [checkmask, checks] = movegen::checkMask(board, king_sq); - const auto pin_hv = movegen::pinMask(board, king_sq, occ_opp, occ_us); - const auto pin_d = movegen::pinMask(board, king_sq, occ_opp, occ_us); + const auto pin_hv = movegen::pinMask(board, king_sq, occ_opp, occ_us); + const auto pin_d = movegen::pinMask(board, king_sq, occ_opp, occ_us); const auto pawns = board.pieces(PieceType::PAWN, stm); const auto pawns_lr = pawns & ~pin_hv; const auto m = movegen::generateEPMove(board, checkmask, pin_d, pawns_lr, ep, stm); - bool found = false; + bool found = false; - for (const auto &move : m) { - if (move != Move::NO_MOVE) { + for (const auto& move : m) + { + if (move != Move::NO_MOVE) + { found = true; break; } @@ -3932,7 +4413,8 @@ class StringBuffer { std::string_view get() const noexcept { return std::string_view(buffer_.data(), index_); } bool add(char c) { - if (index_ >= N) { + if (index_ >= N) + { return false; } @@ -3956,22 +4438,26 @@ class StringBuffer { * @brief Private class * @tparam BUFFER_SIZE */ -template +template class StreamBuffer { private: static constexpr std::size_t N = BUFFER_SIZE; using BufferType = std::array; public: - StreamBuffer(std::istream &stream) : stream_(stream) {} + StreamBuffer(std::istream& stream) : + stream_(stream) {} // Get the current character, skip carriage returns std::optional some() { - while (true) { - if (buffer_index_ < bytes_read_) { + while (true) + { + if (buffer_index_ < bytes_read_) + { const auto c = buffer_[buffer_index_]; - if (c == '\r') { + if (c == '\r') + { ++buffer_index_; continue; } @@ -3979,7 +4465,8 @@ class StreamBuffer { return c; } - if (!fill()) { + if (!fill()) + { return std::nullopt; } } @@ -3989,23 +4476,32 @@ class StreamBuffer { bool skipUntil(char open_delim, char close_delim) { int stack = 0; - while (true) { + while (true) + { const auto ret = some(); advance(); - if (!ret.has_value()) { + if (!ret.has_value()) + { return false; } - if (*ret == open_delim) { + if (*ret == open_delim) + { ++stack; - } else if (*ret == close_delim) { - if (stack == 0) { + } + else if (*ret == close_delim) + { + if (stack == 0) + { // Mismatched closing delimiter return false; - } else { + } + else + { --stack; - if (stack == 0) { + if (stack == 0) + { // Matching closing delimiter found return true; } @@ -4027,7 +4523,8 @@ class StreamBuffer { } void advance() { - if (buffer_index_ >= bytes_read_) { + if (buffer_index_ >= bytes_read_) + { fill(); } @@ -4035,7 +4532,8 @@ class StreamBuffer { } char peek() { - if (buffer_index_ + 1 >= bytes_read_) { + if (buffer_index_ + 1 >= bytes_read_) + { return stream_.peek(); } @@ -4043,7 +4541,8 @@ class StreamBuffer { } std::optional current() { - if (buffer_index_ >= bytes_read_) { + if (buffer_index_ >= bytes_read_) + { return fill() ? std::optional(buffer_[buffer_index_]) : std::nullopt; } @@ -4051,8 +4550,8 @@ class StreamBuffer { } private: - std::istream &stream_; - BufferType buffer_; + std::istream& stream_; + BufferType buffer_; std::streamsize bytes_read_ = 0; std::streamsize buffer_index_ = 0; }; @@ -4118,34 +4617,37 @@ class StreamParserError { NotEnoughData }; - StreamParserError() : code_(None) {} + StreamParserError() : + code_(None) {} - StreamParserError(Code code) : code_(code) {} + StreamParserError(Code code) : + code_(code) {} Code code() const { return code_; } bool hasError() const { return code_ != None; } std::string message() const { - switch (code_) { - case None: - return "No error"; - case InvalidHeaderMissingClosingBracket: - return "Invalid header: missing closing bracket"; - case InvalidHeaderMissingClosingQuote: - return "Invalid header: missing closing quote"; - case NotEnoughData: - return "Not enough data"; - default: - assert(false); - return "Unknown error"; + switch (code_) + { + case None : + return "No error"; + case InvalidHeaderMissingClosingBracket : + return "Invalid header: missing closing bracket"; + case InvalidHeaderMissingClosingQuote : + return "Invalid header: missing closing quote"; + case NotEnoughData : + return "Not enough data"; + default : + assert(false); + return "Unknown error"; } } bool operator==(Code code) const { return code_ == code; } bool operator!=(Code code) const { return code_ != code; } - bool operator==(const StreamParserError &other) const { return code_ == other.code_; } - bool operator!=(const StreamParserError &other) const { return code_ != other.code_; } + bool operator==(const StreamParserError& other) const { return code_ == other.code_; } + bool operator!=(const StreamParserError& other) const { return code_ != other.code_; } operator bool() const { return code_ != None; } @@ -4153,54 +4655,64 @@ class StreamParserError { Code code_; }; -template + > class StreamParser { public: - StreamParser(std::istream &stream) : stream_buffer(stream) {} + StreamParser(std::istream& stream) : + stream_buffer(stream) {} - StreamParserError readGames(Visitor &vis) { + StreamParserError readGames(Visitor& vis) { visitor = &vis; - if (!stream_buffer.fill()) { + if (!stream_buffer.fill()) + { return StreamParserError::NotEnoughData; } - while (auto c = stream_buffer.some()) { - if (in_header) { + while (auto c = stream_buffer.some()) + { + if (in_header) + { visitor->skipPgn(false); - if (*c == '[') { + if (*c == '[') + { visitor->startPgn(); pgn_end = false; processHeader(); - if (error != StreamParserError::None) { + if (error != StreamParserError::None) + { return error; } } - - } else if (in_body) { + } + else if (in_body) + { processBody(); - if (error != StreamParserError::None) { + if (error != StreamParserError::None) + { return error; } } - if (!dont_advance_after_body) stream_buffer.advance(); + if (!dont_advance_after_body) + stream_buffer.advance(); dont_advance_after_body = false; } - if (!pgn_end) { + if (!pgn_end) + { onEnd(); } @@ -4220,8 +4732,10 @@ class StreamParser { } void callVisitorMoveFunction() { - if (!move.empty()) { - if (!visitor->skip()) visitor->move(move.get(), comment); + if (!move.empty()) + { + if (!visitor->skip()) + visitor->move(move.get(), comment); move.clear(); comment.clear(); @@ -4231,93 +4745,114 @@ class StreamParser { void processHeader() { bool backslash = false; - while (auto c = stream_buffer.some()) { - switch (*c) { - // tag start - case '[': - stream_buffer.advance(); - - while (auto k = stream_buffer.some()) { - if (is_space(*k)) { - break; - } else { - if (!header.first.add(*k)) { - error = StreamParserError::ExceededMaxStringLength; - return; - } + while (auto c = stream_buffer.some()) + { + switch (*c) + { + // tag start + case '[' : + stream_buffer.advance(); - stream_buffer.advance(); + while (auto k = stream_buffer.some()) + { + if (is_space(*k)) + { + break; + } + else + { + if (!header.first.add(*k)) + { + error = StreamParserError::ExceededMaxStringLength; + return; } + + stream_buffer.advance(); } + } - stream_buffer.advance(); - break; - case '"': - stream_buffer.advance(); + stream_buffer.advance(); + break; + case '"' : + stream_buffer.advance(); + + while (auto k = stream_buffer.some()) + { + if (*k == '\\') + { + backslash = true; + // don't add backslash to header, is this really correct? + stream_buffer.advance(); + } + else if (*k == '"' && !backslash) + { + stream_buffer.advance(); - while (auto k = stream_buffer.some()) { - if (*k == '\\') { - backslash = true; - // don't add backslash to header, is this really correct? - stream_buffer.advance(); - } else if (*k == '"' && !backslash) { - stream_buffer.advance(); - - // we should be now at ] - if (stream_buffer.current().value_or('\0') != ']') { - error = StreamParserError::InvalidHeaderMissingClosingBracket; - return; - } - - stream_buffer.advance(); - - break; - } else if (*k == '\n') { - // we missed the closing quote and read until the newline character - // this is an invalid pgn, let's throw an error - error = StreamParserError::InvalidHeaderMissingClosingQuote; + // we should be now at ] + if (stream_buffer.current().value_or('\0') != ']') + { + error = StreamParserError::InvalidHeaderMissingClosingBracket; return; - } else { - backslash = false; + } - if (!header.second.add(*k)) { - error = StreamParserError::ExceededMaxStringLength; - return; - } + stream_buffer.advance(); - stream_buffer.advance(); - } + break; } + else if (*k == '\n') + { + // we missed the closing quote and read until the newline character + // this is an invalid pgn, let's throw an error + error = StreamParserError::InvalidHeaderMissingClosingQuote; + return; + } + else + { + backslash = false; + + if (!header.second.add(*k)) + { + error = StreamParserError::ExceededMaxStringLength; + return; + } - // manually skip carriage return, otherwise we would be in the body - // ideally we should completely skip all carriage returns and newlines to avoid this - if (stream_buffer.current() == '\r') { stream_buffer.advance(); } + } - if (!visitor->skip()) visitor->header(header.first.get(), header.second.get()); + // manually skip carriage return, otherwise we would be in the body + // ideally we should completely skip all carriage returns and newlines to avoid this + if (stream_buffer.current() == '\r') + { + stream_buffer.advance(); + } - header.first.clear(); - header.second.clear(); + if (!visitor->skip()) + visitor->header(header.first.get(), header.second.get()); - stream_buffer.advance(); - break; - case '\n': - in_header = false; - in_body = true; + header.first.clear(); + header.second.clear(); - if (!visitor->skip()) visitor->startMoves(); + stream_buffer.advance(); + break; + case '\n' : + in_header = false; + in_body = true; - return; - default: - // this should normally not happen - // lets just go into the body, will this always be save? - in_header = false; - in_body = true; + if (!visitor->skip()) + visitor->startMoves(); - if (!visitor->skip()) visitor->startMoves(); + return; + default : + // this should normally not happen + // lets just go into the body, will this always be save? + in_header = false; + in_body = true; - return; + if (!visitor->skip()) + visitor->startMoves(); + + return; } } } @@ -4326,7 +4861,7 @@ class StreamParser { auto is_termination_symbol = false; auto has_comment = false; - start: +start: /* Skip first move number or game termination Also skip - * / to fix games @@ -4334,22 +4869,30 @@ class StreamParser { this https://github.com/Disservin/chess-library/issues/68 */ - while (auto c = stream_buffer.some()) { - if (*c == ' ' || is_digit(*c)) { + while (auto c = stream_buffer.some()) + { + if (*c == ' ' || is_digit(*c)) + { stream_buffer.advance(); - } else if (*c == '-' || *c == '*' || c == '/') { + } + else if (*c == '-' || *c == '*' || *c == '/') + { is_termination_symbol = true; stream_buffer.advance(); - } else if (*c == '{') { + } + else if (*c == '{') + { has_comment = true; // reading comment stream_buffer.advance(); - while (auto k = stream_buffer.some()) { + while (auto k = stream_buffer.some()) + { stream_buffer.advance(); - if (*k == '}') { + if (*k == '}') + { break; } @@ -4357,29 +4900,37 @@ class StreamParser { } // the game has no moves, but a comment followed by a game termination - if (!visitor->skip()) { + if (!visitor->skip()) + { visitor->move("", comment); + has_comment = false; comment.clear(); } - } else { + } + else + { break; } } // we need to reparse the termination symbol - if (has_comment && !is_termination_symbol) { + if (!visitor->skip() && has_comment && !is_termination_symbol) + { goto start; } // game had no moves, so we can skip it and call endPgn - if (is_termination_symbol) { + if (is_termination_symbol) + { onEnd(); return; } - while (auto c = stream_buffer.some()) { - if (is_space(*c)) { + while (auto c = stream_buffer.some()) + { + if (is_space(*c)) + { stream_buffer.advance(); continue; } @@ -4387,14 +4938,16 @@ class StreamParser { break; } - while (auto cd = stream_buffer.some()) { + while (auto cd = stream_buffer.some()) + { // Pgn are build up in the following way. // {move_number} {move} {comment} {move} {comment} {move_number} ... // So we need to skip the move_number then start reading the move, then save the comment // then read the second move in the group. After that a move_number will follow again. // [ is unexpected here, it probably is a new pgn and the current one is finished - if (*cd == '[') { + if (*cd == '[') + { onEnd(); dont_advance_after_body = true; // break; @@ -4402,42 +4955,59 @@ class StreamParser { } // skip move number digits - while (auto c = stream_buffer.some()) { - if (is_space(*c) || is_digit(*c)) { + while (auto c = stream_buffer.some()) + { + if (is_space(*c) || is_digit(*c)) + { stream_buffer.advance(); - } else { + } + else + { break; } } // skip dots - while (auto c = stream_buffer.some()) { - if (*c == '.') { + while (auto c = stream_buffer.some()) + { + if (*c == '.') + { stream_buffer.advance(); - } else { + } + else + { break; } } // skip spaces - while (auto c = stream_buffer.some()) { - if (is_space(*c)) { + while (auto c = stream_buffer.some()) + { + if (is_space(*c)) + { stream_buffer.advance(); - } else { + } + else + { break; } } // parse move - if (parseMove()) { + if (parseMove()) + { break; } // skip spaces - while (auto c = stream_buffer.some()) { - if (is_space(*c)) { + while (auto c = stream_buffer.some()) + { + if (is_space(*c)) + { stream_buffer.advance(); - } else { + } + else + { break; } } @@ -4445,13 +5015,15 @@ class StreamParser { // game termination auto curr = stream_buffer.current(); - if (!curr.has_value()) { + if (!curr.has_value()) + { onEnd(); break; } // game termination - if (*curr == '*') { + if (*curr == '*') + { onEnd(); stream_buffer.advance(); @@ -4460,15 +5032,20 @@ class StreamParser { const auto peek = stream_buffer.peek(); - if (*curr == '1') { - if (peek == '-') { + if (*curr == '1') + { + if (peek == '-') + { stream_buffer.advance(); stream_buffer.advance(); onEnd(); break; - } else if (peek == '/') { - for (size_t i = 0; i <= 6; ++i) { + } + else if (peek == '/') + { + for (size_t i = 0; i <= 6; ++i) + { stream_buffer.advance(); } @@ -4478,32 +5055,38 @@ class StreamParser { } // might be 0-1 (game termination) or 0-0-0/0-0 (castling) - if (*curr == '0' && stream_buffer.peek() == '-') { + if (*curr == '0' && stream_buffer.peek() == '-') + { stream_buffer.advance(); stream_buffer.advance(); const auto c = stream_buffer.current(); - if (!c.has_value()) { + if (!c.has_value()) + { onEnd(); break; } // game termination - if (*c == '1') { + if (*c == '1') + { onEnd(); stream_buffer.advance(); break; } // castling - else { - if (!move.add('0') || !move.add('-')) { + else + { + if (!move.add('0') || !move.add('-')) + { error = StreamParserError::ExceededMaxStringLength; return; } - if (parseMove()) { + if (parseMove()) + { stream_buffer.advance(); break; } @@ -4514,12 +5097,15 @@ class StreamParser { bool parseMove() { // reading move - while (auto c = stream_buffer.some()) { - if (is_space(*c)) { + while (auto c = stream_buffer.some()) + { + if (is_space(*c)) + { break; } - if (!move.add(*c)) { + if (!move.add(*c)) + { error = StreamParserError::ExceededMaxStringLength; return true; } @@ -4531,60 +5117,69 @@ class StreamParser { } bool parseMoveAppendix() { - while (true) { + while (true) + { auto curr = stream_buffer.current(); - if (!curr.has_value()) { + if (!curr.has_value()) + { onEnd(); return true; } - switch (*curr) { - case '{': { - // reading comment - stream_buffer.advance(); - - while (auto c = stream_buffer.some()) { - stream_buffer.advance(); + switch (*curr) + { + case '{' : { + // reading comment + stream_buffer.advance(); - if (*c == '}') { - break; - } + while (auto c = stream_buffer.some()) + { + stream_buffer.advance(); - comment += *c; + if (*c == '}') + { + break; } - break; - } - case '(': { - stream_buffer.skipUntil('(', ')'); - break; + comment += *c; } - case '$': { - while (auto c = stream_buffer.some()) { - if (is_space(*c)) { - break; - } - stream_buffer.advance(); + break; + } + case '(' : { + stream_buffer.skipUntil('(', ')'); + break; + } + case '$' : { + while (auto c = stream_buffer.some()) + { + if (is_space(*c)) + { + break; } - break; + stream_buffer.advance(); } - case ' ': { - while (auto c = stream_buffer.some()) { - if (!is_space(*c)) { - break; - } - stream_buffer.advance(); + break; + } + case ' ' : { + while (auto c = stream_buffer.some()) + { + if (!is_space(*c)) + { + break; } - break; + stream_buffer.advance(); } - default: - callVisitorMoveFunction(); - return false; + + break; + } + default : + callVisitorMoveFunction(); + return false; } } } @@ -4600,44 +5195,47 @@ class StreamParser { } bool is_space(const char c) noexcept { - switch (c) { - case ' ': - case '\t': - case '\n': - case '\r': - return true; - default: - return false; + switch (c) + { + case ' ' : + case '\t' : + case '\n' : + case '\r' : + return true; + default : + return false; } } bool is_digit(const char c) noexcept { - switch (c) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - return true; - default: - return false; + switch (c) + { + case '0' : + case '1' : + case '2' : + case '3' : + case '4' : + case '5' : + case '6' : + case '7' : + case '8' : + case '9' : + return true; + default : + return false; } } detail::StreamBuffer stream_buffer; - Visitor *visitor = nullptr; + Visitor* visitor = nullptr; // one time allocations - std::pair header = {detail::StringBuffer{}, detail::StringBuffer{}}; + std::pair header = {detail::StringBuffer{}, + detail::StringBuffer{}}; - detail::StringBuffer move = {}; - std::string comment = {}; + detail::StringBuffer move = {}; + std::string comment = {}; // State @@ -4664,14 +5262,16 @@ class uci { * @param chess960 * @return */ - [[nodiscard]] static std::string moveToUci(const Move &move, bool chess960 = false) noexcept(false) { + [[nodiscard]] static std::string moveToUci(const Move& move, + bool chess960 = false) noexcept(false) { // Get the from and to squares Square from_sq = move.from(); Square to_sq = move.to(); // If the move is not a chess960 castling move and is a king moving more than one square, // update the to square to be the correct square for a regular castling move - if (!chess960 && move.typeOf() == Move::CASTLING) { + if (!chess960 && move.typeOf() == Move::CASTLING) + { to_sq = Square(to_sq > from_sq ? File::FILE_G : File::FILE_C, from_sq.rank()); } @@ -4681,7 +5281,8 @@ class uci { ss << from_sq << to_sq; // If the move is a promotion, add the promoted piece to the string stream - if (move.typeOf() == Move::PROMOTION) { + if (move.typeOf() == Move::PROMOTION) + { ss << static_cast(move.promotionType()); } @@ -4694,43 +5295,53 @@ class uci { * @param uci * @return */ - [[nodiscard]] static Move uciToMove(const Board &board, const std::string &uci) noexcept(false) { - if (uci.length() < 4) { + [[nodiscard]] static Move uciToMove(const Board& board, + const std::string& uci) noexcept(false) { + if (uci.length() < 4) + { return Move::NO_MOVE; } Square source = Square(uci.substr(0, 2)); Square target = Square(uci.substr(2, 2)); - if (!source.is_valid() || !target.is_valid()) { + if (!source.is_valid() || !target.is_valid()) + { return Move::NO_MOVE; } auto pt = board.at(source).type(); // castling in chess960 - if (board.chess960() && pt == PieceType::KING && board.at(target).type() == PieceType::ROOK && - board.at(target).color() == board.sideToMove()) { + if (board.chess960() && pt == PieceType::KING && board.at(target).type() == PieceType::ROOK + && board.at(target).color() == board.sideToMove()) + { return Move::make(source, target); } // convert to king captures rook // in chess960 the move should be sent as king captures rook already! - if (!board.chess960() && pt == PieceType::KING && Square::distance(target, source) == 2) { + if (!board.chess960() && pt == PieceType::KING && Square::distance(target, source) == 2) + { target = Square(target > source ? File::FILE_H : File::FILE_A, source.rank()); return Move::make(source, target); } // en passant - if (pt == PieceType::PAWN && target == board.enpassantSq()) { + if (pt == PieceType::PAWN && target == board.enpassantSq()) + { return Move::make(source, target); } // promotion - if (pt == PieceType::PAWN && uci.length() == 5 && Square::back_rank(target, ~board.sideToMove())) { + if (pt == PieceType::PAWN && uci.length() == 5 + && Square::back_rank(target, ~board.sideToMove())) + { auto promotion = PieceType(uci.substr(4, 1)); - if (promotion == PieceType::NONE || promotion == PieceType::KING || promotion == PieceType::PAWN) { + if (promotion == PieceType::NONE || promotion == PieceType::KING + || promotion == PieceType::PAWN) + { return Move::NO_MOVE; } @@ -4746,7 +5357,8 @@ class uci { * @param move * @return */ - [[nodiscard]] static std::string moveToSan(const Board &board, const Move &move) noexcept(false) { + [[nodiscard]] static std::string moveToSan(const Board& board, + const Move& move) noexcept(false) { std::string san; moveToRep(board, move, san); return san; @@ -4758,35 +5370,40 @@ class uci { * @param move * @return */ - [[nodiscard]] static std::string moveToLan(const Board &board, const Move &move) noexcept(false) { + [[nodiscard]] static std::string moveToLan(const Board& board, + const Move& move) noexcept(false) { std::string lan; moveToRep(board, move, lan); return lan; } - class SanParseError : public std::exception { + class SanParseError: public std::exception { public: - explicit SanParseError(const char *message) : msg_(message) {} + explicit SanParseError(const char* message) : + msg_(message) {} - explicit SanParseError(const std::string &message) : msg_(message) {} + explicit SanParseError(const std::string& message) : + msg_(message) {} virtual ~SanParseError() noexcept {} - virtual const char *what() const noexcept { return msg_.c_str(); } + virtual const char* what() const noexcept { return msg_.c_str(); } protected: std::string msg_; }; - class AmbiguousMoveError : public std::exception { + class AmbiguousMoveError: public std::exception { public: - explicit AmbiguousMoveError(const char *message) : msg_(message) {} + explicit AmbiguousMoveError(const char* message) : + msg_(message) {} - explicit AmbiguousMoveError(const std::string &message) : msg_(message) {} + explicit AmbiguousMoveError(const std::string& message) : + msg_(message) {} virtual ~AmbiguousMoveError() noexcept {} - virtual const char *what() const noexcept { return msg_.c_str(); } + virtual const char* what() const noexcept { return msg_.c_str(); } protected: std::string msg_; @@ -4799,7 +5416,7 @@ class uci { * @param san * @return */ - [[nodiscard]] static Move parseSan(const Board &board, std::string_view san) noexcept(false) { + [[nodiscard]] static Move parseSan(const Board& board, std::string_view san) noexcept(false) { Movelist moves; return parseSan(board, san, moves); @@ -4813,76 +5430,98 @@ class uci { * @param moves * @return */ - [[nodiscard]] static Move parseSan(const Board &board, std::string_view san, Movelist &moves) noexcept(false) { - if (san.empty()) { + [[nodiscard]] static Move + parseSan(const Board& board, std::string_view san, Movelist& moves) noexcept(false) { + if (san.empty()) + { return Move::NO_MOVE; } - static constexpr auto pt_to_pgt = [](PieceType pt) { return 1 << (pt); }; - const SanMoveInformation info = parseSanInfo(san); + static constexpr auto pt_to_pgt = [](PieceType pt) { return 1 << (pt); }; + const SanMoveInformation info = parseSanInfo(san); - if (info.capture) { + if (info.capture) + { movegen::legalmoves(moves, board, pt_to_pgt(info.piece)); - } else { + } + else + { movegen::legalmoves(moves, board, pt_to_pgt(info.piece)); } - if (info.castling_short || info.castling_long) { - for (const auto &move : moves) { - if (move.typeOf() == Move::CASTLING) { - if ((info.castling_short && move.to() > move.from()) || - (info.castling_long && move.to() < move.from())) { + if (info.castling_short || info.castling_long) + { + for (const auto& move : moves) + { + if (move.typeOf() == Move::CASTLING) + { + if ((info.castling_short && move.to() > move.from()) + || (info.castling_long && move.to() < move.from())) + { return move; } } } -#ifndef CHESS_NO_EXCEPTIONS - throw SanParseError("Failed to parse san. At step 2: " + std::string(san) + " " + board.getFen()); +#ifndef NDEBUG + throw SanParseError("Failed to parse san. At step 2: " + std::string(san) + " " + + board.getFen()); #endif } Move matchingMove = Move::NO_MOVE; bool foundMatch = false; - for (const auto &move : moves) { + for (const auto& move : moves) + { // Skip all moves that are not to the correct square // or are castling moves - if (move.to() != info.to || move.typeOf() == Move::CASTLING) { + if (move.to() != info.to || move.typeOf() == Move::CASTLING) + { continue; } // Handle promotion moves - if (info.promotion != PieceType::NONE) { - if (move.typeOf() != Move::PROMOTION || info.promotion != move.promotionType() || - move.from().file() != info.from_file) { + if (info.promotion != PieceType::NONE) + { + if (move.typeOf() != Move::PROMOTION || info.promotion != move.promotionType() + || move.from().file() != info.from_file) + { continue; } } // Handle en passant moves - else if (move.typeOf() == Move::ENPASSANT) { - if (move.from().file() != info.from_file) { + else if (move.typeOf() == Move::ENPASSANT) + { + if (move.from().file() != info.from_file) + { continue; } } // Handle moves with specific from square - else if (info.from != Square::NO_SQ) { - if (move.from() != info.from) { + else if (info.from != Square::NO_SQ) + { + if (move.from() != info.from) + { continue; } } // Handle moves with partial from information (rank or file) - else if (info.from_rank != Rank::NO_RANK || info.from_file != File::NO_FILE) { - if ((info.from_file != File::NO_FILE && move.from().file() != info.from_file) || - (info.from_rank != Rank::NO_RANK && move.from().rank() != info.from_rank)) { + else if (info.from_rank != Rank::NO_RANK || info.from_file != File::NO_FILE) + { + if ((info.from_file != File::NO_FILE && move.from().file() != info.from_file) + || (info.from_rank != Rank::NO_RANK && move.from().rank() != info.from_rank)) + { continue; } } // If we get here, the move matches our criteria - if (foundMatch) { -#ifndef CHESS_NO_EXCEPTIONS - throw AmbiguousMoveError("Ambiguous san: " + std::string(san) + " in " + board.getFen()); + if (foundMatch) + { +#ifndef NDEBUG + throw AmbiguousMoveError("Ambiguous san: " + std::string(san) + " in " + + board.getFen()); #endif } @@ -4890,9 +5529,11 @@ class uci { foundMatch = true; } - if (!foundMatch) { -#ifndef CHESS_NO_EXCEPTIONS - throw SanParseError("Failed to parse san. At step 3: " + std::string(san) + " " + board.getFen()); + if (!foundMatch) + { +#ifndef NDEBUG + throw SanParseError("Failed to parse san, illegal move: " + std::string(san) + " " + + board.getFen()); #endif } @@ -4904,23 +5545,28 @@ class uci { * @param move * @return */ - static bool isUciMove(const std::string &move) noexcept { + static bool isUciMove(const std::string& move) noexcept { bool is_uci = false; static constexpr auto is_digit = [](char c) { return c >= '1' && c <= '8'; }; static constexpr auto is_file = [](char c) { return c >= 'a' && c <= 'h'; }; - static constexpr auto is_promotion = [](char c) { return c == 'n' || c == 'b' || c == 'r' || c == 'q'; }; + static constexpr auto is_promotion = [](char c) { + return c == 'n' || c == 'b' || c == 'r' || c == 'q'; + }; // assert that the move is in uci format, [abcdefgh][1-8][abcdefgh][1-8][nbrq] - if (move.size() >= 4) { + if (move.size() >= 4) + { is_uci = is_file(move[0]) && is_digit(move[1]) && is_file(move[2]) && is_digit(move[3]); } - if (move.size() == 5) { + if (move.size() == 5) + { is_uci = is_uci && is_promotion(move[4]); } - if (move.size() > 5) { + if (move.size() > 5) + { return false; } @@ -4948,12 +5594,14 @@ class uci { }; [[nodiscard]] static SanMoveInformation parseSanInfo(std::string_view san) noexcept(false) { -#ifndef CHESS_NO_EXCEPTIONS - if (san.length() < 2) { +#ifndef NDEBUG + if (san.length() < 2) + { throw SanParseError("Failed to parse san. At step 0: " + std::string(san)); } #endif - constexpr auto parse_castle = [](std::string_view &san, SanMoveInformation &info, char castling_char) { + constexpr auto parse_castle = [](std::string_view& san, SanMoveInformation& info, + char castling_char) { info.piece = PieceType::KING; san.remove_prefix(3); @@ -4961,79 +5609,95 @@ class uci { info.castling_short = san.length() == 0 || (san.length() >= 1 && san[0] != '-'); info.castling_long = san.length() >= 2 && san[0] == '-' && san[1] == castling_char; - assert((info.castling_short && !info.castling_long) || (!info.castling_short && info.castling_long) || - (!info.castling_short && !info.castling_long)); + assert((info.castling_short && !info.castling_long) + || (!info.castling_short && info.castling_long) + || (!info.castling_short && !info.castling_long)); }; static constexpr auto isRank = [](char c) { return c >= '1' && c <= '8'; }; static constexpr auto isFile = [](char c) { return c >= 'a' && c <= 'h'; }; - static constexpr auto sw = [](const char &c) { return std::string_view(&c, 1); }; + static constexpr auto sw = [](const char& c) { return std::string_view(&c, 1); }; SanMoveInformation info; + bool throw_error = false; // set to 1 to skip piece type offset std::size_t index = 1; - if (san[0] == 'O' || san[0] == '0') { + if (san[0] == 'O' || san[0] == '0') + { parse_castle(san, info, san[0]); return info; - } else if (isFile(san[0])) { + } + else if (isFile(san[0])) + { index--; info.piece = PieceType::PAWN; - } else { + } + else + { info.piece = PieceType(san); + if (info.piece == PieceType::NONE) + { + throw_error = true; + } } File file_to = File::NO_FILE; Rank rank_to = Rank::NO_RANK; // check if san starts with a file, if so it will be start file - if (index < san.size() && isFile(san[index])) { + if (index < san.size() && isFile(san[index])) + { info.from_file = File(sw(san[index])); index++; } // check if san starts with a rank, if so it will be start rank - if (index < san.size() && isRank(san[index])) { + if (index < san.size() && isRank(san[index])) + { info.from_rank = Rank(sw(san[index])); index++; } // skip capture sign - if (index < san.size() && san[index] == 'x') { + if (index < san.size() && san[index] == 'x') + { info.capture = true; index++; } // to file - if (index < san.size() && isFile(san[index])) { + if (index < san.size() && isFile(san[index])) + { file_to = File(sw(san[index])); index++; } // to rank - if (index < san.size() && isRank(san[index])) { + if (index < san.size() && isRank(san[index])) + { rank_to = Rank(sw(san[index])); index++; } // promotion - if (index < san.size() && san[index] == '=') { + if (index < san.size() && san[index] == '=') + { index++; info.promotion = PieceType(sw(san[index])); - -#ifndef CHESS_NO_EXCEPTIONS - if (info.promotion == PieceType::KING || info.promotion == PieceType::PAWN || - info.promotion == PieceType::NONE) - throw SanParseError("Failed to parse promotion, during san conversion." + std::string(san)); -#endif - + if (info.promotion == PieceType::KING || info.promotion == PieceType::PAWN + || info.promotion == PieceType::NONE) + { + throw_error = true; + } index++; } // for simple moves like Nf3, e4, etc. all the information is contained // in the from file and rank. Thus we need to move it to the to file and rank. - if (file_to == File::NO_FILE && rank_to == Rank::NO_RANK) { + if (file_to == File::NO_FILE && rank_to == Rank::NO_RANK) + { file_to = info.from_file; rank_to = info.from_rank; @@ -5042,86 +5706,117 @@ class uci { } // pawns which are not capturing stay on the same file - if (info.piece == PieceType::PAWN && info.from_file == File::NO_FILE && !info.capture) { + if (info.piece == PieceType::PAWN && info.from_file == File::NO_FILE && !info.capture) + { info.from_file = file_to; } - info.to = Square(file_to, rank_to); + if (file_to != File::NO_FILE && rank_to != Rank::NO_RANK) + { + info.to = Square(file_to, rank_to); + } + else + { + throw_error = true; + } + +#ifndef NDEBUG + if (throw_error) + { + throw SanParseError("Failed to parse san. At step 1: " + std::string(san)); + } +#endif - if (info.from_file != File::NO_FILE && info.from_rank != Rank::NO_RANK) { + if (info.from_file != File::NO_FILE && info.from_rank != Rank::NO_RANK) + { info.from = Square(info.from_file, info.from_rank); } return info; } - template - static void moveToRep(Board board, const Move &move, std::string &str) { - if (handleCastling(move, str)) { + template + static void moveToRep(Board board, const Move& move, std::string& str) { + if (handleCastling(move, str)) + { board.makeMove(move); - if (board.inCheck()) appendCheckSymbol(board, str); + if (board.inCheck()) + appendCheckSymbol(board, str); return; } - const PieceType pt = board.at(move.from()).type(); - const bool isCapture = board.at(move.to()) != Piece::NONE || move.typeOf() == Move::ENPASSANT; + const PieceType pt = board.at(move.from()).type(); + const bool isCapture = + board.at(move.to()) != Piece::NONE || move.typeOf() == Move::ENPASSANT; assert(pt != PieceType::NONE); - if (pt != PieceType::PAWN) { + if (pt != PieceType::PAWN) + { appendPieceSymbol(pt, str); } - if constexpr (LAN) { + if constexpr (LAN) + { appendSquare(move.from(), str); - } else { - if (pt == PieceType::PAWN) { + } + else + { + if (pt == PieceType::PAWN) + { str += isCapture ? static_cast(move.from().file()) : ""; - } else { + } + else + { resolveAmbiguity(board, move, pt, str); } } - if (isCapture) { + if (isCapture) + { str += 'x'; } appendSquare(move.to(), str); - if (move.typeOf() == Move::PROMOTION) appendPromotion(move, str); + if (move.typeOf() == Move::PROMOTION) + appendPromotion(move, str); board.makeMove(move); - if (board.inCheck()) appendCheckSymbol(board, str); + if (board.inCheck()) + appendCheckSymbol(board, str); } - static bool handleCastling(const Move &move, std::string &str) { - if (move.typeOf() != Move::CASTLING) return false; + static bool handleCastling(const Move& move, std::string& str) { + if (move.typeOf() != Move::CASTLING) + return false; str = (move.to().file() > move.from().file()) ? "O-O" : "O-O-O"; return true; } - static void appendPieceSymbol(PieceType pieceType, std::string &str) { + static void appendPieceSymbol(PieceType pieceType, std::string& str) { str += std::toupper(static_cast(pieceType)[0]); } - static void appendSquare(Square square, std::string &str) { + static void appendSquare(Square square, std::string& str) { str += static_cast(square.file()); str += static_cast(square.rank()); } - static void appendPromotion(const Move &move, std::string &str) { + static void appendPromotion(const Move& move, std::string& str) { str += '='; str += std::toupper(static_cast(move.promotionType())[0]); } - static void appendCheckSymbol(Board &board, std::string &str) { + static void appendCheckSymbol(Board& board, std::string& str) { const auto gameState = board.isGameOver().second; str += (gameState == GameResult::LOSE) ? '#' : '+'; } - static void resolveAmbiguity(const Board &board, const Move &move, PieceType pieceType, std::string &str) { + static void + resolveAmbiguity(const Board& board, const Move& move, PieceType pieceType, std::string& str) { Movelist moves; movegen::legalmoves(moves, board, 1 << pieceType); @@ -5129,8 +5824,10 @@ class uci { bool needRank = false; bool hasAmbiguousMove = false; - for (const auto &m : moves) { - if (m != move && m.to() == move.to()) { + for (const auto& m : moves) + { + if (m != move && m.to() == move.to()) + { hasAmbiguousMove = true; /* @@ -5146,44 +5843,55 @@ class uci { letter. */ - if (isIdentifiableByType(moves, move, move.from().file())) { + if (isIdentifiableByType(moves, move, move.from().file())) + { needFile = true; break; } - if (isIdentifiableByType(moves, move, move.from().rank())) { + if (isIdentifiableByType(moves, move, move.from().rank())) + { needRank = true; break; } } } - if (needFile) str += static_cast(move.from().file()); - if (needRank) str += static_cast(move.from().rank()); + if (needFile) + str += static_cast(move.from().file()); + if (needRank) + str += static_cast(move.from().rank()); // we weren't able to disambiguate the move by either file or rank, so we need to use both - if (hasAmbiguousMove && !needFile && !needRank) { + if (hasAmbiguousMove && !needFile && !needRank) + { appendSquare(move.from(), str); } } - template - static bool isIdentifiableByType(const Movelist &moves, const Move move, CoordinateType type) { + template + static bool isIdentifiableByType(const Movelist& moves, const Move move, CoordinateType type) { static_assert(std::is_same_v || std::is_same_v, "CoordinateType must be either File or Rank"); - for (const auto &m : moves) { - if (m == move || m.to() != move.to()) { + for (const auto& m : moves) + { + if (m == move || m.to() != move.to()) + { continue; } // file - if constexpr (std::is_same_v) { - if (type == m.from().file()) return false; + if constexpr (std::is_same_v) + { + if (type == m.from().file()) + return false; } // rank - else { - if (type == m.from().rank()) return false; + else + { + if (type == m.from().rank()) + return false; } } diff --git a/src/evaluate.cpp b/src/evaluate.cpp new file mode 100644 index 0000000..42c76d2 --- /dev/null +++ b/src/evaluate.cpp @@ -0,0 +1,124 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "evaluate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nnue/network.h" +#include "nnue/nnue_misc.h" +#include "position.h" +#include "types.h" +#include "uci.hpp" +#include "nnue/nnue_accumulator.h" + +namespace Stockfish { + +// Returns a static, purely materialistic evaluation of the position from +// the point of view of the side to move. It can be divided by PawnValue to get +// an approximation of the material advantage on the board in terms of pawns. +int Eval::simple_eval(const Position& pos) { + Color c = pos.side_to_move(); + return PawnValue * (pos.count(c) - pos.count(~c)) + + (pos.non_pawn_material(c) - pos.non_pawn_material(~c)); +} + +bool Eval::use_smallnet(const Position& pos) { return std::abs(simple_eval(pos)) > 962; } + +// Evaluate is the evaluator for the outer world. It returns a static evaluation +// of the position from the point of view of the side to move. +Value Eval::evaluate(const Eval::NNUE::Networks& networks, + const Position& pos, + Eval::NNUE::AccumulatorStack& accumulators, + Eval::NNUE::AccumulatorCaches& caches, + int optimism) { + + assert(!pos.b.inCheck()); + + bool smallNet = use_smallnet(pos); + auto [psqt, positional] = smallNet ? networks.small.evaluate(pos, accumulators, &caches.small) + : networks.big.evaluate(pos, accumulators, &caches.big); + + Value nnue = (125 * psqt + 131 * positional) / 128; + + // Re-evaluate the position when higher eval accuracy is worth the time spent + if (smallNet && (std::abs(nnue) < 236)) + { + std::tie(psqt, positional) = networks.big.evaluate(pos, accumulators, &caches.big); + nnue = (125 * psqt + 131 * positional) / 128; + smallNet = false; + } + + // Blend optimism and eval with nnue complexity + int nnueComplexity = std::abs(psqt - positional); + optimism += optimism * nnueComplexity / 468; + nnue -= nnue * nnueComplexity / 18000; + + int material = 535 * pos.count() + pos.non_pawn_material(); + int v = (nnue * (77777 + material) + optimism * (7777 + material)) / 77777; + + // Damp down the evaluation linearly when shuffling + v -= v * pos.rule50_count() / 212; + + // Guarantee evaluation does not hit the tablebase range + v = std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); + + return v; +} + +// Like evaluate(), but instead of returning a value, it returns +// a string (suitable for outputting to stdout) that contains the detailed +// descriptions and values of each evaluation term. Useful for debugging. +// Trace scores are from white's point of view +std::string Eval::trace(Position& pos, const Eval::NNUE::Networks& networks) { + + if (pos.b.inCheck()) + return "Final evaluation: none (in check)"; + + Eval::NNUE::AccumulatorStack accumulators; + auto caches = std::make_unique(networks); + + std::stringstream ss; + ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2); + ss << '\n' << NNUE::trace(pos, networks, *caches) << '\n'; + + ss << std::showpoint << std::showpos << std::fixed << std::setprecision(2) << std::setw(15); + + auto [psqt, positional] = networks.big.evaluate(pos, accumulators, &caches->big); + Value v = psqt + positional; + v = pos.side_to_move() == WHITE ? v : -v; + ss << "NNUE evaluation " << 0.01 * to_cp(v, pos) << " (white side)\n"; + + v = evaluate(networks, pos, accumulators, *caches, VALUE_ZERO); + v = pos.side_to_move() == WHITE ? v : -v; + ss << "Final evaluation " << 0.01 * to_cp(v, pos) << " (white side)"; + ss << " [with scaled NNUE, ...]"; + ss << "\n"; + + return ss.str(); +} + +} // namespace Stockfish diff --git a/src/evaluate.h b/src/evaluate.h new file mode 100644 index 0000000..2a6c9af --- /dev/null +++ b/src/evaluate.h @@ -0,0 +1,58 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef EVALUATE_H_INCLUDED +#define EVALUATE_H_INCLUDED + +#include + +#include "types.h" + +namespace Stockfish { + +class Position; + +namespace Eval { + +// The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue +// for the build process (profile-build and fishtest) to work. Do not change the +// name of the macro or the location where this macro is defined, as it is used +// in the Makefile/Fishtest. +#define EvalFileDefaultNameBig "nn-1c0000000000.nnue" +#define EvalFileDefaultNameSmall "nn-37f18f62d772.nnue" + +namespace NNUE { +struct Networks; +struct AccumulatorCaches; +class AccumulatorStack; +} + +std::string trace(Position& pos, const Eval::NNUE::Networks& networks); + +int simple_eval(const Position& pos); +bool use_smallnet(const Position& pos); +Value evaluate(const NNUE::Networks& networks, + const Position& pos, + Eval::NNUE::AccumulatorStack& accumulators, + Eval::NNUE::AccumulatorCaches& caches, + int optimism); +} // namespace Eval + +} // namespace Stockfish + +#endif // #ifndef EVALUATE_H_INCLUDED diff --git a/src/incbin/UNLICENCE b/src/incbin/UNLICENCE new file mode 100644 index 0000000..32484ab --- /dev/null +++ b/src/incbin/UNLICENCE @@ -0,0 +1,26 @@ +The file "incbin.h" is free and unencumbered software released into +the public domain by Dale Weiler, see: + + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/src/incbin/incbin.h b/src/incbin/incbin.h new file mode 100644 index 0000000..3f662e1 --- /dev/null +++ b/src/incbin/incbin.h @@ -0,0 +1,476 @@ +/** + * @file incbin.h + * @author Dale Weiler + * @brief Utility for including binary files + * + * Facilities for including binary files into the current translation unit and + * making use from them externally in other translation units. + */ +#ifndef INCBIN_HDR +#define INCBIN_HDR +#include +#if defined(__AVX512BW__) || \ + defined(__AVX512CD__) || \ + defined(__AVX512DQ__) || \ + defined(__AVX512ER__) || \ + defined(__AVX512PF__) || \ + defined(__AVX512VL__) || \ + defined(__AVX512F__) +# define INCBIN_ALIGNMENT_INDEX 6 +#elif defined(__AVX__) || \ + defined(__AVX2__) +# define INCBIN_ALIGNMENT_INDEX 5 +#elif defined(__SSE__) || \ + defined(__SSE2__) || \ + defined(__SSE3__) || \ + defined(__SSSE3__) || \ + defined(__SSE4_1__) || \ + defined(__SSE4_2__) || \ + defined(__neon__) || \ + defined(__ARM_NEON) || \ + defined(__ALTIVEC__) +# define INCBIN_ALIGNMENT_INDEX 4 +#elif ULONG_MAX != 0xffffffffu +# define INCBIN_ALIGNMENT_INDEX 3 +# else +# define INCBIN_ALIGNMENT_INDEX 2 +#endif + +/* Lookup table of (1 << n) where `n' is `INCBIN_ALIGNMENT_INDEX' */ +#define INCBIN_ALIGN_SHIFT_0 1 +#define INCBIN_ALIGN_SHIFT_1 2 +#define INCBIN_ALIGN_SHIFT_2 4 +#define INCBIN_ALIGN_SHIFT_3 8 +#define INCBIN_ALIGN_SHIFT_4 16 +#define INCBIN_ALIGN_SHIFT_5 32 +#define INCBIN_ALIGN_SHIFT_6 64 + +/* Actual alignment value */ +#define INCBIN_ALIGNMENT \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_ALIGN_SHIFT, _), \ + INCBIN_ALIGNMENT_INDEX) + +/* Stringize */ +#define INCBIN_STR(X) \ + #X +#define INCBIN_STRINGIZE(X) \ + INCBIN_STR(X) +/* Concatenate */ +#define INCBIN_CAT(X, Y) \ + X ## Y +#define INCBIN_CONCATENATE(X, Y) \ + INCBIN_CAT(X, Y) +/* Deferred macro expansion */ +#define INCBIN_EVAL(X) \ + X +#define INCBIN_INVOKE(N, ...) \ + INCBIN_EVAL(N(__VA_ARGS__)) +/* Variable argument count for overloading by arity */ +#define INCBIN_VA_ARG_COUNTER(_1, _2, _3, N, ...) N +#define INCBIN_VA_ARGC(...) INCBIN_VA_ARG_COUNTER(__VA_ARGS__, 3, 2, 1, 0) + +/* Green Hills uses a different directive for including binary data */ +#if defined(__ghs__) +# if (__ghs_asm == 2) +# define INCBIN_MACRO ".file" +/* Or consider the ".myrawdata" entry in the ld file */ +# else +# define INCBIN_MACRO "\tINCBIN" +# endif +#else +# define INCBIN_MACRO ".incbin" +#endif + +#ifndef _MSC_VER +# define INCBIN_ALIGN \ + __attribute__((aligned(INCBIN_ALIGNMENT))) +#else +# define INCBIN_ALIGN __declspec(align(INCBIN_ALIGNMENT)) +#endif + +#if defined(__arm__) || /* GNU C and RealView */ \ + defined(__arm) || /* Diab */ \ + defined(_ARM) /* ImageCraft */ +# define INCBIN_ARM +#endif + +#ifdef __GNUC__ +/* Utilize .balign where supported */ +# define INCBIN_ALIGN_HOST ".balign " INCBIN_STRINGIZE(INCBIN_ALIGNMENT) "\n" +# define INCBIN_ALIGN_BYTE ".balign 1\n" +#elif defined(INCBIN_ARM) +/* + * On arm assemblers, the alignment value is calculated as (1 << n) where `n' is + * the shift count. This is the value passed to `.align' + */ +# define INCBIN_ALIGN_HOST ".align " INCBIN_STRINGIZE(INCBIN_ALIGNMENT_INDEX) "\n" +# define INCBIN_ALIGN_BYTE ".align 0\n" +#else +/* We assume other inline assembler's treat `.align' as `.balign' */ +# define INCBIN_ALIGN_HOST ".align " INCBIN_STRINGIZE(INCBIN_ALIGNMENT) "\n" +# define INCBIN_ALIGN_BYTE ".align 1\n" +#endif + +/* INCBIN_CONST is used by incbin.c generated files */ +#if defined(__cplusplus) +# define INCBIN_EXTERNAL extern "C" +# define INCBIN_CONST extern const +#else +# define INCBIN_EXTERNAL extern +# define INCBIN_CONST const +#endif + +/** + * @brief Optionally override the linker section into which size and data is + * emitted. + * + * @warning If you use this facility, you might have to deal with + * platform-specific linker output section naming on your own. + */ +#if !defined(INCBIN_OUTPUT_SECTION) +# if defined(__APPLE__) +# define INCBIN_OUTPUT_SECTION ".const_data" +# else +# define INCBIN_OUTPUT_SECTION ".rodata" +# endif +#endif + +/** + * @brief Optionally override the linker section into which data is emitted. + * + * @warning If you use this facility, you might have to deal with + * platform-specific linker output section naming on your own. + */ +#if !defined(INCBIN_OUTPUT_DATA_SECTION) +# define INCBIN_OUTPUT_DATA_SECTION INCBIN_OUTPUT_SECTION +#endif + +/** + * @brief Optionally override the linker section into which size is emitted. + * + * @warning If you use this facility, you might have to deal with + * platform-specific linker output section naming on your own. + * + * @note This is useful for Harvard architectures where program memory cannot + * be directly read from the program without special instructions. With this you + * can chose to put the size variable in RAM rather than ROM. + */ +#if !defined(INCBIN_OUTPUT_SIZE_SECTION) +# define INCBIN_OUTPUT_SIZE_SECTION INCBIN_OUTPUT_SECTION +#endif + +#if defined(__APPLE__) +# include "TargetConditionals.h" +# if defined(TARGET_OS_IPHONE) && !defined(INCBIN_SILENCE_BITCODE_WARNING) +# warning "incbin is incompatible with bitcode. Using the library will break upload to App Store if you have bitcode enabled. Add `#define INCBIN_SILENCE_BITCODE_WARNING` before including this header to silence this warning." +# endif +/* The directives are different for Apple branded compilers */ +# define INCBIN_SECTION INCBIN_OUTPUT_SECTION "\n" +# define INCBIN_GLOBAL(NAME) ".globl " INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME "\n" +# define INCBIN_INT ".long " +# define INCBIN_MANGLE "_" +# define INCBIN_BYTE ".byte " +# define INCBIN_TYPE(...) +#else +# define INCBIN_SECTION ".section " INCBIN_OUTPUT_SECTION "\n" +# define INCBIN_GLOBAL(NAME) ".global " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME "\n" +# if defined(__ghs__) +# define INCBIN_INT ".word " +# else +# define INCBIN_INT ".int " +# endif +# if defined(__USER_LABEL_PREFIX__) +# define INCBIN_MANGLE INCBIN_STRINGIZE(__USER_LABEL_PREFIX__) +# else +# define INCBIN_MANGLE "" +# endif +# if defined(INCBIN_ARM) +/* On arm assemblers, `@' is used as a line comment token */ +# define INCBIN_TYPE(NAME) ".type " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME ", %object\n" +# elif defined(__MINGW32__) || defined(__MINGW64__) +/* Mingw doesn't support this directive either */ +# define INCBIN_TYPE(NAME) +# else +/* It's safe to use `@' on other architectures */ +# define INCBIN_TYPE(NAME) ".type " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME ", @object\n" +# endif +# define INCBIN_BYTE ".byte " +#endif + +/* List of style types used for symbol names */ +#define INCBIN_STYLE_CAMEL 0 +#define INCBIN_STYLE_SNAKE 1 + +/** + * @brief Specify the prefix to use for symbol names. + * + * @note By default this is "g". + * + * @code + * #define INCBIN_PREFIX incbin + * #include "incbin.h" + * INCBIN(Foo, "foo.txt"); + * + * // Now you have the following symbols instead: + * // const unsigned char incbinFoo[]; + * // const unsigned char *const incbinFoo; + * // const unsigned int incbinFoo; + * @endcode + */ +#if !defined(INCBIN_PREFIX) +# define INCBIN_PREFIX g +#endif + +/** + * @brief Specify the style used for symbol names. + * + * Possible options are + * - INCBIN_STYLE_CAMEL "CamelCase" + * - INCBIN_STYLE_SNAKE "snake_case" + * + * @note By default this is INCBIN_STYLE_CAMEL + * + * @code + * #define INCBIN_STYLE INCBIN_STYLE_SNAKE + * #include "incbin.h" + * INCBIN(foo, "foo.txt"); + * + * // Now you have the following symbols: + * // const unsigned char foo_data[]; + * // const unsigned char *const foo_end; + * // const unsigned int foo_size; + * @endcode + */ +#if !defined(INCBIN_STYLE) +# define INCBIN_STYLE INCBIN_STYLE_CAMEL +#endif + +/* Style lookup tables */ +#define INCBIN_STYLE_0_DATA Data +#define INCBIN_STYLE_0_END End +#define INCBIN_STYLE_0_SIZE Size +#define INCBIN_STYLE_1_DATA _data +#define INCBIN_STYLE_1_END _end +#define INCBIN_STYLE_1_SIZE _size + +/* Style lookup: returning identifier */ +#define INCBIN_STYLE_IDENT(TYPE) \ + INCBIN_CONCATENATE( \ + INCBIN_STYLE_, \ + INCBIN_CONCATENATE( \ + INCBIN_EVAL(INCBIN_STYLE), \ + INCBIN_CONCATENATE(_, TYPE))) + +/* Style lookup: returning string literal */ +#define INCBIN_STYLE_STRING(TYPE) \ + INCBIN_STRINGIZE( \ + INCBIN_STYLE_IDENT(TYPE)) \ + +/* Generate the global labels by indirectly invoking the macro with our style + * type and concatenating the name against them. */ +#define INCBIN_GLOBAL_LABELS(NAME, TYPE) \ + INCBIN_INVOKE( \ + INCBIN_GLOBAL, \ + INCBIN_CONCATENATE( \ + NAME, \ + INCBIN_INVOKE( \ + INCBIN_STYLE_IDENT, \ + TYPE))) \ + INCBIN_INVOKE( \ + INCBIN_TYPE, \ + INCBIN_CONCATENATE( \ + NAME, \ + INCBIN_INVOKE( \ + INCBIN_STYLE_IDENT, \ + TYPE))) + +/** + * @brief Externally reference binary data included in another translation unit. + * + * Produces three external symbols that reference the binary data included in + * another translation unit. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param TYPE Optional array type. Omitting this picks a default of `unsigned char`. + * @param NAME The name given for the binary data + * + * @code + * INCBIN_EXTERN(Foo); + * + * // Now you have the following symbols: + * // extern const unsigned char Foo[]; + * // extern const unsigned char *const Foo; + * // extern const unsigned int Foo; + * @endcode + * + * You may specify a custom optional data type as well as the first argument. + * @code + * INCBIN_EXTERN(custom_type, Foo); + * + * // Now you have the following symbols: + * // extern const custom_type Foo[]; + * // extern const custom_type *const Foo; + * // extern const unsigned int Foo; + * @endcode + */ +#define INCBIN_EXTERN(...) \ + INCBIN_CONCATENATE(INCBIN_EXTERN_, INCBIN_VA_ARGC(__VA_ARGS__))(__VA_ARGS__) +#define INCBIN_EXTERN_1(NAME, ...) \ + INCBIN_EXTERN_2(unsigned char, NAME) +#define INCBIN_EXTERN_2(TYPE, NAME) \ + INCBIN_EXTERNAL const INCBIN_ALIGN TYPE \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ + INCBIN_STYLE_IDENT(DATA))[]; \ + INCBIN_EXTERNAL const INCBIN_ALIGN TYPE *const \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ + INCBIN_STYLE_IDENT(END)); \ + INCBIN_EXTERNAL const unsigned int \ + INCBIN_CONCATENATE( \ + INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \ + INCBIN_STYLE_IDENT(SIZE)) + +/** + * @brief Externally reference textual data included in another translation unit. + * + * Produces three external symbols that reference the textual data included in + * another translation unit. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param NAME The name given for the textual data + * + * @code + * INCBIN_EXTERN(Foo); + * + * // Now you have the following symbols: + * // extern const char Foo[]; + * // extern const char *const Foo; + * // extern const unsigned int Foo; + * @endcode + */ +#define INCTXT_EXTERN(NAME) \ + INCBIN_EXTERN_2(char, NAME) + +/** + * @brief Include a binary file into the current translation unit. + * + * Includes a binary file into the current translation unit, producing three symbols + * for objects that encode the data and size respectively. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param TYPE Optional array type. Omitting this picks a default of `unsigned char`. + * @param NAME The name to associate with this binary data (as an identifier.) + * @param FILENAME The file to include (as a string literal.) + * + * @code + * INCBIN(Icon, "icon.png"); + * + * // Now you have the following symbols: + * // const unsigned char Icon[]; + * // const unsigned char *const Icon; + * // const unsigned int Icon; + * @endcode + * + * You may specify a custom optional data type as well as the first argument. + * These macros are specialized by arity. + * @code + * INCBIN(custom_type, Icon, "icon.png"); + * + * // Now you have the following symbols: + * // const custom_type Icon[]; + * // const custom_type *const Icon; + * // const unsigned int Icon; + * @endcode + * + * @warning This must be used in global scope + * @warning The identifiers may be different if INCBIN_STYLE is not default + * + * To externally reference the data included by this in another translation unit + * please @see INCBIN_EXTERN. + */ +#ifdef _MSC_VER +# define INCBIN(NAME, FILENAME) \ + INCBIN_EXTERN(NAME) +#else +# define INCBIN(...) \ + INCBIN_CONCATENATE(INCBIN_, INCBIN_VA_ARGC(__VA_ARGS__))(__VA_ARGS__) +# if defined(__GNUC__) +# define INCBIN_1(...) _Pragma("GCC error \"Single argument INCBIN not allowed\"") +# elif defined(__clang__) +# define INCBIN_1(...) _Pragma("clang error \"Single argument INCBIN not allowed\"") +# else +# define INCBIN_1(...) /* Cannot do anything here */ +# endif +# define INCBIN_2(NAME, FILENAME) \ + INCBIN_3(unsigned char, NAME, FILENAME) +# define INCBIN_3(TYPE, NAME, FILENAME) INCBIN_COMMON(TYPE, NAME, FILENAME, /* No terminator for binary data */) +# define INCBIN_COMMON(TYPE, NAME, FILENAME, TERMINATOR) \ + __asm__(INCBIN_SECTION \ + INCBIN_GLOBAL_LABELS(NAME, DATA) \ + INCBIN_ALIGN_HOST \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(DATA) ":\n" \ + INCBIN_MACRO " \"" FILENAME "\"\n" \ + TERMINATOR \ + INCBIN_GLOBAL_LABELS(NAME, END) \ + INCBIN_ALIGN_BYTE \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(END) ":\n" \ + INCBIN_BYTE "1\n" \ + INCBIN_GLOBAL_LABELS(NAME, SIZE) \ + INCBIN_ALIGN_HOST \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(SIZE) ":\n" \ + INCBIN_INT INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(END) " - " \ + INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(DATA) "\n" \ + INCBIN_ALIGN_HOST \ + ".text\n" \ + ); \ + INCBIN_EXTERN(TYPE, NAME) +#endif + +/** + * @brief Include a textual file into the current translation unit. + * + * This behaves the same as INCBIN except it produces char compatible arrays + * and implicitly adds a null-terminator byte, thus the size of data included + * by this is one byte larger than that of INCBIN. + * + * Includes a textual file into the current translation unit, producing three + * symbols for objects that encode the data and size respectively. + * + * The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with + * "Data", as well as "End" and "Size" after. An example is provided below. + * + * @param NAME The name to associate with this binary data (as an identifier.) + * @param FILENAME The file to include (as a string literal.) + * + * @code + * INCTXT(Readme, "readme.txt"); + * + * // Now you have the following symbols: + * // const char Readme[]; + * // const char *const Readme; + * // const unsigned int Readme; + * @endcode + * + * @warning This must be used in global scope + * @warning The identifiers may be different if INCBIN_STYLE is not default + * + * To externally reference the data included by this in another translation unit + * please @see INCBIN_EXTERN. + */ +#if defined(_MSC_VER) +# define INCTXT(NAME, FILENAME) \ + INCBIN_EXTERN(NAME) +#else +# define INCTXT(NAME, FILENAME) \ + INCBIN_COMMON(char, NAME, FILENAME, INCBIN_BYTE "0\n") +#endif + +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..304f831 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,28 @@ +#include +#include "search.h" +#include "uci.hpp" +#include "ucioptions.hpp" + +int main(int argc, char** argv) { + UCIOptions::addSpin("Hash", 16, 1, 1048576, [](const UCIOptions::Option& opt) { + search::tt.resize(std::get(opt.value)); + }); + UCIOptions::setOption("Hash", "16"); + search::init(); + UCIOptions::addString("NNUEEvalFileBig", EvalFileDefaultNameBig, + [](const UCIOptions::Option& opt) { + search::nn->big.load(".", std::get(opt.value)); + }); + UCIOptions::addString("NNUEEvalFileSmall", EvalFileDefaultNameSmall, + [](const UCIOptions::Option& opt) { + search::nn->small.load(".", std::get(opt.value)); + }); + search::init(); + if (argc == 2 && !strcmp(argv[1], "bench")) // SF bench (Makefile) + { + handle_bench(); + return 0; + } + uci_loop(); + return 0; +} diff --git a/src/memory.cpp b/src/memory.cpp new file mode 100644 index 0000000..d4ccd8d --- /dev/null +++ b/src/memory.cpp @@ -0,0 +1,266 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "memory.h" + +#include + +#if __has_include("features.h") + #include +#endif + +#if defined(__linux__) && !defined(__ANDROID__) + #include +#endif + +#if defined(__APPLE__) || defined(__ANDROID__) || defined(__OpenBSD__) \ + || (defined(__GLIBCXX__) && !defined(_GLIBCXX_HAVE_ALIGNED_ALLOC) && !defined(_WIN32)) \ + || defined(__e2k__) + #define POSIXALIGNEDALLOC + #include +#endif + +#ifdef _WIN32 + #if _WIN32_WINNT < 0x0601 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0601 // Force to include needed API prototypes + #endif + + #ifndef NOMINMAX + #define NOMINMAX + #endif + + #include // std::hex, std::dec + #include // std::cerr + #include // std::endl + #include + +// The needed Windows API for processor groups could be missed from old Windows +// versions, so instead of calling them directly (forcing the linker to resolve +// the calls at compile time), try to load them at runtime. To do this we need +// first to define the corresponding function pointers. + +extern "C" { +using OpenProcessToken_t = bool (*)(HANDLE, DWORD, PHANDLE); +using LookupPrivilegeValueA_t = bool (*)(LPCSTR, LPCSTR, PLUID); +using AdjustTokenPrivileges_t = + bool (*)(HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, PDWORD); +} +#endif + +namespace NNUEParser { + +// Wrappers for systems where the c++17 implementation does not guarantee the +// availability of aligned_alloc(). Memory allocated with std_aligned_alloc() +// must be freed with std_aligned_free(). + +void* std_aligned_alloc(size_t alignment, size_t size) { +#if defined(_ISOC11_SOURCE) + return aligned_alloc(alignment, size); // C11 aligned_alloc, must free with free() +#elif defined(POSIXALIGNEDALLOC) + void* mem = nullptr; + return posix_memalign(&mem, alignment, size) == 0 ? mem + : nullptr; // POSIX, must free with free() +#elif defined(_WIN32) && !defined(_M_ARM) && !defined(_M_ARM64) + return _mm_malloc(size, alignment); // Intel intrinsics on Windows, must free with _mm_free() +#elif defined(_WIN32) + return _aligned_malloc(size, alignment); // MSVC aligned alloc, must free with _aligned_free() +#else + return std::aligned_alloc(alignment, size); // C++17 stdlib +#endif +} + +void std_aligned_free(void* ptr) { + +#if defined(POSIXALIGNEDALLOC) + free(ptr); +#elif defined(_WIN32) && !defined(_M_ARM) && !defined(_M_ARM64) + _mm_free(ptr); +#elif defined(_WIN32) + _aligned_free(ptr); +#else + free(ptr); +#endif +} + +// aligned_large_pages_alloc() will return suitably aligned memory, +// if possible using large pages. + +#if defined(_WIN32) + +static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize) { + + #if !defined(_WIN64) + return nullptr; + #else + + HANDLE hProcessToken{}; + LUID luid{}; + void* mem = nullptr; + + const size_t largePageSize = GetLargePageMinimum(); + if (!largePageSize) + return nullptr; + + // Dynamically link OpenProcessToken, LookupPrivilegeValue and + // AdjustTokenPrivileges + + HMODULE hAdvapi32 = GetModuleHandle(TEXT("advapi32.dll")); + + if (!hAdvapi32) + hAdvapi32 = LoadLibrary(TEXT("advapi32.dll")); + + auto OpenProcessToken_f = auto OpenProcessToken_f = + reinterpret_cast(GetProcAddress(hAdvapi32, "OpenProcessToken")); + if (!OpenProcessToken_f) + return nullptr; + auto LookupPrivilegeValueA_f = + reinterpret_cast(GetProcAddress(hAdvapi32, "LookupPrivilegeValueA")); + if (!LookupPrivilegeValueA_f) + return nullptr; + auto AdjustTokenPrivileges_f = + reinterpret_cast(GetProcAddress(hAdvapi32, "AdjustTokenPrivileges")); + if (!AdjustTokenPrivileges_f) + return nullptr; + // We need SeLockMemoryPrivilege, so try to enable it for the process + + if (!OpenProcessToken_f( // OpenProcessToken() + GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken)) + return nullptr; + + if (LookupPrivilegeValueA_f(nullptr, "SeLockMemoryPrivilege", &luid)) + { + TOKEN_PRIVILEGES tp{}; + TOKEN_PRIVILEGES prevTp{}; + DWORD prevTpLen = 0; + + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = luid; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + // Try to enable SeLockMemoryPrivilege. Note that even if + // AdjustTokenPrivileges() succeeds, we still need to query GetLastError() + // to ensure that the privileges were actually obtained. + + if (AdjustTokenPrivileges_f(hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp, + &prevTpLen) + && GetLastError() == ERROR_SUCCESS) + { + // Round up size to full pages and allocate + allocSize = (allocSize + largePageSize - 1) & ~size_t(largePageSize - 1); + mem = VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, + PAGE_READWRITE); + + // Privilege no longer needed, restore previous state + AdjustTokenPrivileges_f(hProcessToken, FALSE, &prevTp, 0, nullptr, nullptr); + } + } + + CloseHandle(hProcessToken); + + return mem; + + #endif +} + +void* aligned_large_pages_alloc(size_t allocSize) { + + // Try to allocate large pages + void* mem = aligned_large_pages_alloc_windows(allocSize); + + // Fall back to regular, page-aligned, allocation if necessary + if (!mem) + mem = VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + + return mem; +} + +#else + +void* aligned_large_pages_alloc(size_t allocSize) { + + #if defined(__linux__) + constexpr size_t alignment = 2 * 1024 * 1024; // 2MB page size assumed + #else + constexpr size_t alignment = 4096; // small page size assumed + #endif + + // Round up to multiples of alignment + size_t size = ((allocSize + alignment - 1) / alignment) * alignment; + void* mem = std_aligned_alloc(alignment, size); + #if defined(MADV_HUGEPAGE) + madvise(mem, size, MADV_HUGEPAGE); + #endif + return mem; +} + +#endif + +bool has_large_pages() { + +#if defined(_WIN32) + + constexpr size_t page_size = 2 * 1024 * 1024; // 2MB page size assumed + void* mem = aligned_large_pages_alloc_windows(page_size); + if (mem == nullptr) + { + return false; + } + else + { + aligned_large_pages_free(mem); + return true; + } + +#elif defined(__linux__) + + #if defined(MADV_HUGEPAGE) + return true; + #else + return false; + #endif + +#else + + return false; + +#endif +} + +// aligned_large_pages_free() will free the previously memory allocated +// by aligned_large_pages_alloc(). The effect is a nop if mem == nullptr. + +#if defined(_WIN32) + +void aligned_large_pages_free(void* mem) { + + if (mem && !VirtualFree(mem, 0, MEM_RELEASE)) + { + DWORD err = GetLastError(); + std::cerr << "Failed to free large page memory. Error code: 0x" << std::hex << err + << std::dec << std::endl; + exit(EXIT_FAILURE); + } +} + +#else + +void aligned_large_pages_free(void* mem) { std_aligned_free(mem); } + +#endif +} // namespace NNUEParser \ No newline at end of file diff --git a/src/memory.h b/src/memory.h new file mode 100644 index 0000000..f90b368 --- /dev/null +++ b/src/memory.h @@ -0,0 +1,218 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef MEMORY_H_INCLUDED +#define MEMORY_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#define ASSERT_ALIGNED(ptr, alignment) assert(reinterpret_cast(ptr) % alignment == 0) + +namespace NNUEParser { + +void* std_aligned_alloc(size_t alignment, size_t size); +void std_aligned_free(void* ptr); + +// Memory aligned by page size, min alignment: 4096 bytes +void* aligned_large_pages_alloc(size_t size); +void aligned_large_pages_free(void* mem); + +bool has_large_pages(); + +// Frees memory which was placed there with placement new. +// Works for both single objects and arrays of unknown bound. +template +void memory_deleter(T* ptr, FREE_FUNC free_func) { + if (!ptr) + return; + + // Explicitly needed to call the destructor + if constexpr (!std::is_trivially_destructible_v) + ptr->~T(); + + free_func(ptr); +} + +// Frees memory which was placed there with placement new. +// Works for both single objects and arrays of unknown bound. +template +void memory_deleter_array(T* ptr, FREE_FUNC free_func) { + if (!ptr) + return; + + // Move back on the pointer to where the size is allocated + const size_t array_offset = std::max(sizeof(size_t), alignof(T)); + char* raw_memory = reinterpret_cast(ptr) - array_offset; + + if constexpr (!std::is_trivially_destructible_v) + { + const size_t size = *reinterpret_cast(raw_memory); + + // Explicitly call the destructor for each element in reverse order + for (size_t i = size; i-- > 0;) + ptr[i].~T(); + } + + free_func(raw_memory); +} + +// Allocates memory for a single object and places it there with placement new +template +inline std::enable_if_t, T*> memory_allocator(ALLOC_FUNC alloc_func, + Args&&... args) { + void* raw_memory = alloc_func(sizeof(T)); + ASSERT_ALIGNED(raw_memory, alignof(T)); + return new (raw_memory) T(std::forward(args)...); +} + +// Allocates memory for an array of unknown bound and places it there with +// placement new +template +inline std::enable_if_t, std::remove_extent_t*> +memory_allocator(ALLOC_FUNC alloc_func, size_t num) { + using ElementType = std::remove_extent_t; + + const size_t array_offset = std::max(sizeof(size_t), alignof(ElementType)); + + // Save the array size in the memory location + char* raw_memory = + reinterpret_cast(alloc_func(array_offset + num * sizeof(ElementType))); + ASSERT_ALIGNED(raw_memory, alignof(ElementType)); + + new (raw_memory) size_t(num); + + for (size_t i = 0; i < num; ++i) + new (raw_memory + array_offset + i * sizeof(ElementType)) ElementType(); + + // Need to return the pointer at the start of the array so that + // the indexing in unique_ptr works. + return reinterpret_cast(raw_memory + array_offset); +} + +// +// +// aligned large page unique ptr +// +// + +template +struct LargePageDeleter { + void operator()(T* ptr) const { return memory_deleter(ptr, aligned_large_pages_free); } +}; + +template +struct LargePageArrayDeleter { + void operator()(T* ptr) const { return memory_deleter_array(ptr, aligned_large_pages_free); } +}; + +template +using LargePagePtr = + std::conditional_t, + std::unique_ptr>>, + std::unique_ptr>>; + +// make_unique_large_page for single objects +template +std::enable_if_t, LargePagePtr> make_unique_large_page(Args&&... args) { + static_assert(alignof(T) <= 4096, "aligned_large_pages_alloc() may fail for " + "such a big alignment requirement of T"); + + T* obj = memory_allocator(aligned_large_pages_alloc, std::forward(args)...); + + return LargePagePtr(obj); +} + +// make_unique_large_page for arrays of unknown bound +template +std::enable_if_t, LargePagePtr> make_unique_large_page(size_t num) { + using ElementType = std::remove_extent_t; + + static_assert(alignof(ElementType) <= 4096, + "aligned_large_pages_alloc() may fail for such a big alignment " + "requirement of T"); + + ElementType* memory = memory_allocator(aligned_large_pages_alloc, num); + + return LargePagePtr(memory); +} + +// +// +// aligned unique ptr +// +// + +template +struct AlignedDeleter { + void operator()(T* ptr) const { return memory_deleter(ptr, std_aligned_free); } +}; + +template +struct AlignedArrayDeleter { + void operator()(T* ptr) const { return memory_deleter_array(ptr, std_aligned_free); } +}; + +template +using AlignedPtr = + std::conditional_t, + std::unique_ptr>>, + std::unique_ptr>>; + +// make_unique_aligned for single objects +template +std::enable_if_t, AlignedPtr> make_unique_aligned(Args&&... args) { + const auto func = [](size_t size) { return std_aligned_alloc(alignof(T), size); }; + T* obj = memory_allocator(func, std::forward(args)...); + + return AlignedPtr(obj); +} + +// make_unique_aligned for arrays of unknown bound +template +std::enable_if_t, AlignedPtr> make_unique_aligned(size_t num) { + using ElementType = std::remove_extent_t; + + const auto func = [](size_t size) { return std_aligned_alloc(alignof(ElementType), size); }; + ElementType* memory = memory_allocator(func, num); + + return AlignedPtr(memory); +} + +// Get the first aligned element of an array. +// ptr must point to an array of size at least `sizeof(T) * N + alignment` +// bytes, where N is the number of elements in the array. +template +T* align_ptr_up(T* ptr) { + static_assert(alignof(T) <= Alignment); + + const uintptr_t ptrint = reinterpret_cast(reinterpret_cast(ptr)); + return reinterpret_cast( + reinterpret_cast((ptrint + (Alignment - 1)) / Alignment * Alignment)); +} + +} // namespace NNUEParser +namespace Stockfish { +using namespace NNUEParser; +} +#endif // #ifndef MEMORY_H_INCLUDED diff --git a/src/misc.h b/src/misc.h new file mode 100644 index 0000000..6bcb293 --- /dev/null +++ b/src/misc.h @@ -0,0 +1,62 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef MISC_H_INCLUDED +#define MISC_H_INCLUDED + +namespace Stockfish { // misc.h, /IsLittleEndian +// True if and only if the binary is compiled on a little-endian machine +static inline const std::uint16_t Le = 1; +static inline const bool IsLittleEndian = *reinterpret_cast(&Le) == 1; +template +class ValueList { + + public: + std::size_t size() const { return size_; } + void push_back(const T& value) { values_[size_++] = value; } + const T* begin() const { return values_; } + const T* end() const { return values_ + size_; } + const T& operator[](int index) const { return values_[index]; } + + private: + T values_[MaxSize]; + std::size_t size_ = 0; +}; + +#if defined(__GNUC__) && !defined(__clang__) + #if __GNUC__ >= 13 + #define sf_assume(cond) __attribute__((assume(cond))) + #else + #define sf_assume(cond) \ + do \ + { \ + if (!(cond)) \ + __builtin_unreachable(); \ + } while (0) + #endif +#else + // do nothing for other compilers + #define sf_assume(cond) +#endif + +#define sync_cout std::cout +#define sync_endl std::endl + +} // namespace Stockfish + +#endif // #ifndef MISC_H_INCLUDED diff --git a/src/movepick.cpp b/src/movepick.cpp new file mode 100644 index 0000000..d5cda9c --- /dev/null +++ b/src/movepick.cpp @@ -0,0 +1,200 @@ +#include "movepick.hpp" + +namespace movepick { +inline int piece_value(chess::PieceType pt) { // fast and static check + switch (pt) + { + case (int) chess::PieceType::PAWN : + return Stockfish::PawnValue; + case (int) chess::PieceType::KNIGHT : + return Stockfish::KnightValue; + case (int) chess::PieceType::BISHOP : + return Stockfish::BishopValue; + case (int) chess::PieceType::ROOK : + return Stockfish::RookValue; + case (int) chess::PieceType::QUEEN : + return Stockfish::QueenValue; + default : + return 0; // includes king + } +} +// 1. SEE + +// Return type changed to chess::Square for consistency +static chess::Square select_least_valuable(chess::Board& board, chess::Bitboard bb) { + if (bb.empty()) + return chess::Square::NO_SQ; // Use NO_SQ sentinel for no square + + int least_value = 0x7fffffff; + chess::Square least_valuable_square = chess::Square::NO_SQ; + + while (bb) + { + int idx = bb.pop(); // idx is int index of bitboard square + chess::Square sq(idx); + int value = piece_value(board.at(sq)); + if (value < least_value) + { + least_value = value; + least_valuable_square = sq; + } + } + return least_valuable_square; +} + +int SEE(chess::Board& board, chess::Move move) { + constexpr int max_gain_size = Stockfish::MAX_PLY + 1; // To avoid overflow in gain[d + 1] + std::array gain{}; + int d = 0; + chess::Color side = board.sideToMove(); + chess::Bitboard occ = board.occ(); + + chess::Square target_sq = move.to(); + + // Initial gain setup: + if (move.typeOf() & chess::Move::PROMOTION) + { + gain[0] = piece_value(move.promotionType()); + if (board.isCapture(move)) + gain[0] += piece_value(board.at(target_sq)); + } + else if (move.typeOf() == chess::Move::ENPASSANT) + { + // We'll treat it normally below, so just assign captured pawn value now: + gain[0] = piece_value(chess::PieceType::PAWN); + } + else + { + gain[0] = piece_value(board.at(target_sq)); + } + + chess::Square from_sq = move.from(); + + // Remove the moving piece from occupancy: + occ.clear(from_sq.index()); + + // Special case for en passant: remove the captured pawn square here: + if (move.typeOf() == chess::Move::ENPASSANT) + { + chess::Square ep_captured_sq(target_sq.file(), + from_sq.rank()); // square behind target + occ.clear(ep_captured_sq.index()); + } + + chess::Square next_attacker_sq = from_sq; + + while (++d < max_gain_size) + { + side = ~side; + + // Find all attackers of target square from side to move: + chess::Bitboard attackers = chess::attacks::attackers(board, side, target_sq) & occ; + + // Include x-ray attacks for sliding pieces: + chess::Bitboard rook_attacks = chess::attacks::rook(target_sq, occ); + chess::Bitboard bishop_attacks = chess::attacks::bishop(target_sq, occ); + + attackers |= rook_attacks + & (board.pieces(chess::PieceType::ROOK, side) + | board.pieces(chess::PieceType::QUEEN, side)); + attackers |= bishop_attacks + & (board.pieces(chess::PieceType::BISHOP, side) + | board.pieces(chess::PieceType::QUEEN, side)); + + if (attackers.empty()) + break; + + next_attacker_sq = select_least_valuable(board, attackers); + if (next_attacker_sq == chess::Square::NO_SQ) + break; + + int captured_value = piece_value(board.at(next_attacker_sq)); + gain[d] = -gain[d - 1] + captured_value; + + if (std::max(-gain[d - 1], gain[d]) < 0) + break; + + occ.clear(next_attacker_sq.index()); + } + + while (--d > 0) + { + gain[d] = std::max(-gain[d + 1], gain[d]); + } + + return gain[0]; +} + +// 2. Killer moves +chess::Move killerMoves[Stockfish::MAX_PLY][2] = {}; + +void updateKillerMoves(chess::Move m, int ply) { + if (killerMoves[ply][0] != m) + { + killerMoves[ply][1] = killerMoves[ply][0]; + killerMoves[ply][0] = m; + } +} + +// 3. History heuristic +int historyHeuristic[64][64] = {}; // from-square to to-square + +void updateHistoryHeuristic(chess::Move m, int depth) { + historyHeuristic[m.from().index()][m.to().index()] += depth * depth; +} + +void orderMoves(chess::Board& board, chess::Movelist& moves, chess::Move ttMove, int ply) { + for (auto& move : moves) + { + if (move == ttMove) + move.setScore(10000); + else if (board.isCapture(move)) + move.setScore(SEE(board, move)); + else if (move == killerMoves[ply][0]) + move.setScore(8500); + else if (move == killerMoves[ply][1]) + move.setScore(8000); + else + move.setScore(historyHeuristic[move.from().index()][move.to().index()]); + } + // Sort moves by score in descending order + std::stable_sort(moves.begin(), moves.end(), [](const chess::Move& a, const chess::Move& b) { + return a.score() > b.score(); + }); +} + +void qOrderMoves(chess::Board& board, chess::Movelist& moves) { + // clang-format off + int16_t promotion_table[4][8] = { + {-58, -38, -13, -28, -31, -27, -63, -99}, // N + {-14, -21, -11, -8, -7, -9, -17, -24}, // B + { 13, 10, 18, 15, 12, 12, 8, 5}, // R + { -9, 22, 22, 27, 27, 19, 10, 20}, // Q + }; + // clang-format on + + for (auto& move : moves) + { + if (board.isCapture(move)) // Simple SEE + { + move.setScore(SEE(board, move)); + } + else if (move.typeOf() & chess::Move::PROMOTION) + { + int promoValue = piece_value(move.promotionType()); + int captureValue = + board.isCapture(move) ? piece_value(board.at(move.to())) : 0; + move.setScore(promoValue + captureValue + + promotion_table[move.promotionType() - 1][move.to().file()]); + } + else + { + move.setScore(historyHeuristic[move.from().index()][move.to().index()]); + } + } + // Sort moves by score in descending order + std::stable_sort(moves.begin(), moves.end(), [](const chess::Move& a, const chess::Move& b) { + return a.score() > b.score(); + }); +} +} // namespace movepick diff --git a/src/movepick.hpp b/src/movepick.hpp new file mode 100644 index 0000000..d8b00e4 --- /dev/null +++ b/src/movepick.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "chess.hpp" +#include "types.h" +namespace movepick { +// Static Exchange Evaluation - evaluates material exchange on a square +int SEE(chess::Board&, chess::Move); +// Updates killer moves table for move ordering +void updateKillerMoves(chess::Move, int); +// Updates history heuristic table for move ordering +void updateHistoryHeuristic(chess::Move, int); +// Orders moves for regular search (with TT move, killers at given ply) +void orderMoves(chess::Board&, chess::Movelist&, chess::Move, int); +// Orders moves for quiescence search (captures/promotions) +void qOrderMoves(chess::Board&, chess::Movelist&); +// Killer moves table: [ply][slot] for move ordering heuristic +extern chess::Move killerMoves[Stockfish::MAX_PLY][2]; +} // namespace movepick diff --git a/src/nnue/features/half_ka_v2_hm.cpp b/src/nnue/features/half_ka_v2_hm.cpp new file mode 100644 index 0000000..70e9bee --- /dev/null +++ b/src/nnue/features/half_ka_v2_hm.cpp @@ -0,0 +1,86 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +//Definition of input features HalfKAv2_hm of NNUE evaluation function + +#include "half_ka_v2_hm.h" + +#include "../../bitboard.h" +#include "../../position.h" +#include "../../types.h" +#include "../nnue_common.h" + +namespace Stockfish::Eval::NNUE::Features { + +// Index of a feature for a given king position and another piece on some square +template +inline IndexType HalfKAv2_hm::make_index(Square s, Piece pc, Square ksq) { + return IndexType((int(s) ^ OrientTBL[Perspective][ksq]) + PieceSquareIndex[Perspective][pc] + + KingBuckets[Perspective][ksq]); +} + +// Get a list of indices for active features +template +void HalfKAv2_hm::append_active_indices(const Position& pos, IndexList& active) { + Square ksq = pos.square(Perspective); + Bitboard bb = pos.pieces(); + while (bb) + { + Square s = pop_lsb(bb); + active.push_back(make_index(s, pos.piece_on(s), ksq)); + } +} + +// Explicit template instantiations +template void HalfKAv2_hm::append_active_indices(const Position& pos, IndexList& active); +template void HalfKAv2_hm::append_active_indices(const Position& pos, IndexList& active); +template IndexType HalfKAv2_hm::make_index(Square s, Piece pc, Square ksq); +template IndexType HalfKAv2_hm::make_index(Square s, Piece pc, Square ksq); + +// Get a list of indices for recently changed features +template +void HalfKAv2_hm::append_changed_indices(Square ksq, + const DirtyPiece& dp, + IndexList& removed, + IndexList& added) { + removed.push_back(make_index(dp.from, dp.pc, ksq)); + if (dp.to != SQ_NONE) + added.push_back(make_index(dp.to, dp.pc, ksq)); + + if (dp.remove_sq != SQ_NONE) + removed.push_back(make_index(dp.remove_sq, dp.remove_pc, ksq)); + + if (dp.add_sq != SQ_NONE) + added.push_back(make_index(dp.add_sq, dp.add_pc, ksq)); +} + +// Explicit template instantiations +template void HalfKAv2_hm::append_changed_indices(Square ksq, + const DirtyPiece& dp, + IndexList& removed, + IndexList& added); +template void HalfKAv2_hm::append_changed_indices(Square ksq, + const DirtyPiece& dp, + IndexList& removed, + IndexList& added); + +bool HalfKAv2_hm::requires_refresh(const DirtyPiece& dirtyPiece, Color perspective) { + return dirtyPiece.pc == make_piece(perspective, KING); +} + +} // namespace Stockfish::Eval::NNUE::Features diff --git a/src/nnue/features/half_ka_v2_hm.h b/src/nnue/features/half_ka_v2_hm.h new file mode 100644 index 0000000..af3bf48 --- /dev/null +++ b/src/nnue/features/half_ka_v2_hm.h @@ -0,0 +1,144 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +//Definition of input features HalfKAv2_hm of NNUE evaluation function + +#ifndef NNUE_FEATURES_HALF_KA_V2_HM_H_INCLUDED +#define NNUE_FEATURES_HALF_KA_V2_HM_H_INCLUDED + +#include + +#include "../../misc.h" +#include "../../types.h" +#include "../nnue_common.h" + +namespace Stockfish { +class Position; +} + +namespace Stockfish::Eval::NNUE::Features { + +// Feature HalfKAv2_hm: Combination of the position of own king and the +// position of pieces. Position mirrored such that king is always on e..h files. +class HalfKAv2_hm { + + // Unique number for each piece type on each square + enum { + PS_NONE = 0, + PS_W_PAWN = 0, + PS_B_PAWN = 1 * SQUARE_NB, + PS_W_KNIGHT = 2 * SQUARE_NB, + PS_B_KNIGHT = 3 * SQUARE_NB, + PS_W_BISHOP = 4 * SQUARE_NB, + PS_B_BISHOP = 5 * SQUARE_NB, + PS_W_ROOK = 6 * SQUARE_NB, + PS_B_ROOK = 7 * SQUARE_NB, + PS_W_QUEEN = 8 * SQUARE_NB, + PS_B_QUEEN = 9 * SQUARE_NB, + PS_KING = 10 * SQUARE_NB, + PS_NB = 11 * SQUARE_NB + }; + + static constexpr IndexType PieceSquareIndex[COLOR_NB][PIECE_NB] = { + // Convention: W - us, B - them + // Viewed from other side, W and B are reversed + {PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE, + PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_KING, PS_NONE}, + {PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_KING, PS_NONE, + PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE}}; + + public: + // Feature name + static constexpr const char* Name = "HalfKAv2_hm(Friend)"; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t HashValue = 0x7f234cb8u; + + // Number of feature dimensions + static constexpr IndexType Dimensions = + static_cast(SQUARE_NB) * static_cast(PS_NB) / 2; + +#define B(v) (v * PS_NB) + // clang-format off + static constexpr int KingBuckets[COLOR_NB][SQUARE_NB] = { + { B(28), B(29), B(30), B(31), B(31), B(30), B(29), B(28), + B(24), B(25), B(26), B(27), B(27), B(26), B(25), B(24), + B(20), B(21), B(22), B(23), B(23), B(22), B(21), B(20), + B(16), B(17), B(18), B(19), B(19), B(18), B(17), B(16), + B(12), B(13), B(14), B(15), B(15), B(14), B(13), B(12), + B( 8), B( 9), B(10), B(11), B(11), B(10), B( 9), B( 8), + B( 4), B( 5), B( 6), B( 7), B( 7), B( 6), B( 5), B( 4), + B( 0), B( 1), B( 2), B( 3), B( 3), B( 2), B( 1), B( 0) }, + { B( 0), B( 1), B( 2), B( 3), B( 3), B( 2), B( 1), B( 0), + B( 4), B( 5), B( 6), B( 7), B( 7), B( 6), B( 5), B( 4), + B( 8), B( 9), B(10), B(11), B(11), B(10), B( 9), B( 8), + B(12), B(13), B(14), B(15), B(15), B(14), B(13), B(12), + B(16), B(17), B(18), B(19), B(19), B(18), B(17), B(16), + B(20), B(21), B(22), B(23), B(23), B(22), B(21), B(20), + B(24), B(25), B(26), B(27), B(27), B(26), B(25), B(24), + B(28), B(29), B(30), B(31), B(31), B(30), B(29), B(28) } + }; + // clang-format on +#undef B + // clang-format off + // Orient a square according to perspective (rotates by 180 for black) + static constexpr int OrientTBL[COLOR_NB][SQUARE_NB] = { + { SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1, + SQ_H1, SQ_H1, SQ_H1, SQ_H1, SQ_A1, SQ_A1, SQ_A1, SQ_A1 }, + { SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, + SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, + SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, + SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, + SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, + SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, + SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8, + SQ_H8, SQ_H8, SQ_H8, SQ_H8, SQ_A8, SQ_A8, SQ_A8, SQ_A8 } + }; + // clang-format on + + // Maximum number of simultaneously active features. + static constexpr IndexType MaxActiveDimensions = 32; + using IndexList = ValueList; + + // Index of a feature for a given king position and another piece on some square + template + static IndexType make_index(Square s, Piece pc, Square ksq); + + // Get a list of indices for active features + template + static void append_active_indices(const Position& pos, IndexList& active); + + // Get a list of indices for recently changed features + template + static void + append_changed_indices(Square ksq, const DirtyPiece& dp, IndexList& removed, IndexList& added); + + // Returns whether the change stored in this DirtyPiece means + // that a full accumulator refresh is required. + static bool requires_refresh(const DirtyPiece& dirtyPiece, Color perspective); +}; + +} // namespace Stockfish::Eval::NNUE::Features + +#endif // #ifndef NNUE_FEATURES_HALF_KA_V2_HM_H_INCLUDED diff --git a/src/nnue/features/half_ka_v2_hm.o b/src/nnue/features/half_ka_v2_hm.o new file mode 100644 index 0000000000000000000000000000000000000000..91a948335c772b338500b0596ef3e76c6e3f225d GIT binary patch literal 67976 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 B=lTEu literal 0 HcmV?d00001 diff --git a/src/nnue/layers/affine_transform.h b/src/nnue/layers/affine_transform.h new file mode 100644 index 0000000..b4440c1 --- /dev/null +++ b/src/nnue/layers/affine_transform.h @@ -0,0 +1,302 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Definition of layer AffineTransform of NNUE evaluation function + +#ifndef NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED +#define NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED + +#include +#include + +#include "../nnue_common.h" +#include "../simd.h" + +/* + This file contains the definition for a fully connected layer (aka affine transform). + + - expected use-case is for when PaddedInputDimensions == 32 and InputDimensions <= 32. + - that's why AVX512 is hard to implement + - expected use-case is small layers + - inputs are processed in chunks of 4, weights are respectively transposed + - accumulation happens directly to int32s +*/ + +namespace Stockfish::Eval::NNUE::Layers { + +#if defined(USE_SSSE3) || defined(USE_NEON_DOTPROD) + #define ENABLE_SEQ_OPT +#endif + +// Fallback implementation for older/other architectures. +// Requires the input to be padded to at least 16 values. +#ifndef ENABLE_SEQ_OPT + +template +static void affine_transform_non_ssse3(std::int32_t* output, + const std::int8_t* weights, + const std::int32_t* biases, + const std::uint8_t* input) { + #if defined(USE_SSE2) || defined(USE_NEON) + #if defined(USE_SSE2) + // At least a multiple of 16, with SSE2. + constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 16) / 16; + const __m128i Zeros = _mm_setzero_si128(); + const auto inputVector = reinterpret_cast(input); + + #elif defined(USE_NEON) + constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 16) / 16; + const auto inputVector = reinterpret_cast(input); + #endif + + for (IndexType i = 0; i < OutputDimensions; ++i) + { + const IndexType offset = i * PaddedInputDimensions; + + #if defined(USE_SSE2) + __m128i sumLo = _mm_cvtsi32_si128(biases[i]); + __m128i sumHi = Zeros; + const auto row = reinterpret_cast(&weights[offset]); + for (IndexType j = 0; j < NumChunks; ++j) + { + __m128i row_j = _mm_load_si128(&row[j]); + __m128i input_j = _mm_load_si128(&inputVector[j]); + __m128i extendedRowLo = _mm_srai_epi16(_mm_unpacklo_epi8(row_j, row_j), 8); + __m128i extendedRowHi = _mm_srai_epi16(_mm_unpackhi_epi8(row_j, row_j), 8); + __m128i extendedInputLo = _mm_unpacklo_epi8(input_j, Zeros); + __m128i extendedInputHi = _mm_unpackhi_epi8(input_j, Zeros); + __m128i productLo = _mm_madd_epi16(extendedRowLo, extendedInputLo); + __m128i productHi = _mm_madd_epi16(extendedRowHi, extendedInputHi); + sumLo = _mm_add_epi32(sumLo, productLo); + sumHi = _mm_add_epi32(sumHi, productHi); + } + __m128i sum = _mm_add_epi32(sumLo, sumHi); + __m128i sumHigh_64 = _mm_shuffle_epi32(sum, _MM_SHUFFLE(1, 0, 3, 2)); + sum = _mm_add_epi32(sum, sumHigh_64); + __m128i sum_second_32 = _mm_shufflelo_epi16(sum, _MM_SHUFFLE(1, 0, 3, 2)); + sum = _mm_add_epi32(sum, sum_second_32); + output[i] = _mm_cvtsi128_si32(sum); + + #elif defined(USE_NEON) + + int32x4_t sum = {biases[i]}; + const auto row = reinterpret_cast(&weights[offset]); + for (IndexType j = 0; j < NumChunks; ++j) + { + int16x8_t product = vmull_s8(inputVector[j * 2], row[j * 2]); + product = vmlal_s8(product, inputVector[j * 2 + 1], row[j * 2 + 1]); + sum = vpadalq_s16(sum, product); + } + output[i] = SIMD::neon_m128_reduce_add_epi32(sum); + + #endif + } + #else + std::memcpy(output, biases, sizeof(std::int32_t) * OutputDimensions); + + // Traverse weights in transpose order to take advantage of input sparsity + for (IndexType i = 0; i < InputDimensions; ++i) + if (input[i]) + { + const std::int8_t* w = &weights[i]; + const int in = input[i]; + for (IndexType j = 0; j < OutputDimensions; ++j) + output[j] += w[j * PaddedInputDimensions] * in; + } + #endif +} + +#endif // !ENABLE_SEQ_OPT + +template +class AffineTransform { + public: + // Input/output type + using InputType = std::uint8_t; + using OutputType = std::int32_t; + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = InDims; + static constexpr IndexType OutputDimensions = OutDims; + + static constexpr IndexType PaddedInputDimensions = + ceil_to_multiple(InputDimensions, MaxSimdWidth); + static constexpr IndexType PaddedOutputDimensions = + ceil_to_multiple(OutputDimensions, MaxSimdWidth); + + using OutputBuffer = OutputType[PaddedOutputDimensions]; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value(std::uint32_t prevHash) { + std::uint32_t hashValue = 0xCC03DAE4u; + hashValue += OutputDimensions; + hashValue ^= prevHash >> 1; + hashValue ^= prevHash << 31; + return hashValue; + } + + static constexpr IndexType get_weight_index_scrambled(IndexType i) { + return (i / 4) % (PaddedInputDimensions / 4) * OutputDimensions * 4 + + i / PaddedInputDimensions * 4 + i % 4; + } + + static constexpr IndexType get_weight_index(IndexType i) { +#ifdef ENABLE_SEQ_OPT + return get_weight_index_scrambled(i); +#else + return i; +#endif + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + read_little_endian(stream, biases, OutputDimensions); + for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + weights[get_weight_index(i)] = read_little_endian(stream); + + return !stream.fail(); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) const { + write_little_endian(stream, biases, OutputDimensions); + + for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + write_little_endian(stream, weights[get_weight_index(i)]); + + return !stream.fail(); + } + // Forward propagation + void propagate(const InputType* input, OutputType* output) const { + +#ifdef ENABLE_SEQ_OPT + + if constexpr (OutputDimensions > 1) + { + #if defined(USE_AVX512) + using vec_t = __m512i; + #define vec_set_32 _mm512_set1_epi32 + #define vec_add_dpbusd_32 SIMD::m512_add_dpbusd_epi32 + #elif defined(USE_AVX2) + using vec_t = __m256i; + #define vec_set_32 _mm256_set1_epi32 + #define vec_add_dpbusd_32 SIMD::m256_add_dpbusd_epi32 + #elif defined(USE_SSSE3) + using vec_t = __m128i; + #define vec_set_32 _mm_set1_epi32 + #define vec_add_dpbusd_32 SIMD::m128_add_dpbusd_epi32 + #elif defined(USE_NEON_DOTPROD) + using vec_t = int32x4_t; + #define vec_set_32 vdupq_n_s32 + #define vec_add_dpbusd_32(acc, a, b) \ + SIMD::dotprod_m128_add_dpbusd_epi32(acc, vreinterpretq_s8_s32(a), \ + vreinterpretq_s8_s32(b)) + #endif + + static constexpr IndexType OutputSimdWidth = sizeof(vec_t) / sizeof(OutputType); + + static_assert(OutputDimensions % OutputSimdWidth == 0); + + constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 8) / 4; + constexpr IndexType NumRegs = OutputDimensions / OutputSimdWidth; + + const auto input32 = reinterpret_cast(input); + const vec_t* biasvec = reinterpret_cast(biases); + vec_t acc[NumRegs]; + for (IndexType k = 0; k < NumRegs; ++k) + acc[k] = biasvec[k]; + + for (IndexType i = 0; i < NumChunks; ++i) + { + const vec_t in0 = vec_set_32(input32[i]); + const auto col0 = + reinterpret_cast(&weights[i * OutputDimensions * 4]); + + for (IndexType k = 0; k < NumRegs; ++k) + vec_add_dpbusd_32(acc[k], in0, col0[k]); + } + + vec_t* outptr = reinterpret_cast(output); + for (IndexType k = 0; k < NumRegs; ++k) + outptr[k] = acc[k]; + + #undef vec_set_32 + #undef vec_add_dpbusd_32 + } + else if constexpr (OutputDimensions == 1) + { + // We cannot use AVX512 for the last layer because there are only 32 inputs + // and the buffer is not padded to 64 elements. + #if defined(USE_AVX2) + using vec_t = __m256i; + #define vec_setzero() _mm256_setzero_si256() + #define vec_add_dpbusd_32 SIMD::m256_add_dpbusd_epi32 + #define vec_hadd SIMD::m256_hadd + #elif defined(USE_SSSE3) + using vec_t = __m128i; + #define vec_setzero() _mm_setzero_si128() + #define vec_add_dpbusd_32 SIMD::m128_add_dpbusd_epi32 + #define vec_hadd SIMD::m128_hadd + #elif defined(USE_NEON_DOTPROD) + using vec_t = int32x4_t; + #define vec_setzero() vdupq_n_s32(0) + #define vec_add_dpbusd_32(acc, a, b) \ + SIMD::dotprod_m128_add_dpbusd_epi32(acc, vreinterpretq_s8_s32(a), \ + vreinterpretq_s8_s32(b)) + #define vec_hadd SIMD::neon_m128_hadd + #endif + + const auto inputVector = reinterpret_cast(input); + + static constexpr IndexType InputSimdWidth = sizeof(vec_t) / sizeof(InputType); + + static_assert(PaddedInputDimensions % InputSimdWidth == 0); + + constexpr IndexType NumChunks = PaddedInputDimensions / InputSimdWidth; + vec_t sum0 = vec_setzero(); + const auto row0 = reinterpret_cast(&weights[0]); + + for (int j = 0; j < int(NumChunks); ++j) + { + const vec_t in = inputVector[j]; + vec_add_dpbusd_32(sum0, in, row0[j]); + } + output[0] = vec_hadd(sum0, biases[0]); + + #undef vec_setzero + #undef vec_add_dpbusd_32 + #undef vec_hadd + } +#else + // Use old implementation for the other architectures. + affine_transform_non_ssse3( + output, weights, biases, input); +#endif + } + + private: + using BiasType = OutputType; + using WeightType = std::int8_t; + + alignas(CacheLineSize) BiasType biases[OutputDimensions]; + alignas(CacheLineSize) WeightType weights[OutputDimensions * PaddedInputDimensions]; +}; + +} // namespace Stockfish::Eval::NNUE::Layers + +#endif // #ifndef NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED diff --git a/src/nnue/layers/affine_transform_sparse_input.h b/src/nnue/layers/affine_transform_sparse_input.h new file mode 100644 index 0000000..7c74d5e --- /dev/null +++ b/src/nnue/layers/affine_transform_sparse_input.h @@ -0,0 +1,262 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Definition of layer AffineTransformSparseInput of NNUE evaluation function + +#ifndef NNUE_LAYERS_AFFINE_TRANSFORM_SPARSE_INPUT_H_INCLUDED +#define NNUE_LAYERS_AFFINE_TRANSFORM_SPARSE_INPUT_H_INCLUDED + +#include +#include +#include + +#include "../../bitboard.h" +#include "../simd.h" +#include "../nnue_common.h" + +/* + This file contains the definition for a fully connected layer (aka affine transform) with block sparse input. +*/ + +namespace Stockfish::Eval::NNUE::Layers { + +#if (USE_SSSE3 | (USE_NEON >= 8)) +static constexpr int lsb_index64[64] = { + 0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61, 54, 58, 35, 52, 50, 42, + 21, 44, 38, 32, 29, 23, 17, 11, 4, 62, 46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, + 31, 22, 10, 45, 25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63}; + +constexpr int constexpr_lsb(uint64_t bb) { + assert(bb != 0); + constexpr uint64_t debruijn64 = 0x03F79D71B4CB0A89ULL; + return lsb_index64[((bb ^ (bb - 1)) * debruijn64) >> 58]; +} + +alignas(CacheLineSize) static constexpr struct OffsetIndices { + + #if (USE_SSE41) + std::uint8_t offset_indices[256][8]; + #else + std::uint16_t offset_indices[256][8]; + #endif + + constexpr OffsetIndices() : + offset_indices() { + for (int i = 0; i < 256; ++i) + { + std::uint64_t j = i, k = 0; + while (j) + { + offset_indices[i][k++] = constexpr_lsb(j); + j &= j - 1; + } + while (k < 8) + offset_indices[i][k++] = 0; + } + } + +} Lookup; + +// Find indices of nonzero numbers in an int32_t array +template +void find_nnz(const std::int32_t* input, std::uint16_t* out, IndexType& count_out) { + using namespace SIMD; + + constexpr IndexType InputSimdWidth = sizeof(vec_uint_t) / sizeof(std::int32_t); + // Inputs are processed InputSimdWidth at a time and outputs are processed 8 at a time so we process in chunks of max(InputSimdWidth, 8) + constexpr IndexType ChunkSize = std::max(InputSimdWidth, 8); + constexpr IndexType NumChunks = InputDimensions / ChunkSize; + constexpr IndexType InputsPerChunk = ChunkSize / InputSimdWidth; + constexpr IndexType OutputsPerChunk = ChunkSize / 8; + + const auto inputVector = reinterpret_cast(input); + IndexType count = 0; + vec128_t base = vec128_zero; + const vec128_t increment = vec128_set_16(8); + for (IndexType i = 0; i < NumChunks; ++i) + { + // bitmask of nonzero values in this chunk + unsigned nnz = 0; + for (IndexType j = 0; j < InputsPerChunk; ++j) + { + const vec_uint_t inputChunk = inputVector[i * InputsPerChunk + j]; + nnz |= unsigned(vec_nnz(inputChunk)) << (j * InputSimdWidth); + } + for (IndexType j = 0; j < OutputsPerChunk; ++j) + { + const unsigned lookup = (nnz >> (j * 8)) & 0xFF; + const vec128_t offsets = + vec128_load(reinterpret_cast(&Lookup.offset_indices[lookup])); + vec128_storeu(reinterpret_cast(out + count), vec128_add(base, offsets)); + count += popcount(lookup); + base = vec128_add(base, increment); + } + } + count_out = count; +} + +#endif + +// Sparse input implementation +template +class AffineTransformSparseInput { + public: + // Input/output type + using InputType = std::uint8_t; + using OutputType = std::int32_t; + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = InDims; + static constexpr IndexType OutputDimensions = OutDims; + + static_assert(OutputDimensions % 16 == 0, + "Only implemented for OutputDimensions divisible by 16."); + + static constexpr IndexType PaddedInputDimensions = + ceil_to_multiple(InputDimensions, MaxSimdWidth); + static constexpr IndexType PaddedOutputDimensions = + ceil_to_multiple(OutputDimensions, MaxSimdWidth); + +#if (USE_SSSE3 | (USE_NEON >= 8)) + static constexpr IndexType ChunkSize = 4; +#else + static constexpr IndexType ChunkSize = 1; +#endif + + using OutputBuffer = OutputType[PaddedOutputDimensions]; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value(std::uint32_t prevHash) { + std::uint32_t hashValue = 0xCC03DAE4u; + hashValue += OutputDimensions; + hashValue ^= prevHash >> 1; + hashValue ^= prevHash << 31; + return hashValue; + } + + static constexpr IndexType get_weight_index_scrambled(IndexType i) { + return (i / ChunkSize) % (PaddedInputDimensions / ChunkSize) * OutputDimensions * ChunkSize + + i / PaddedInputDimensions * ChunkSize + i % ChunkSize; + } + + static constexpr IndexType get_weight_index(IndexType i) { +#if (USE_SSSE3 | (USE_NEON >= 8)) + return get_weight_index_scrambled(i); +#else + return i; +#endif + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + read_little_endian(stream, biases, OutputDimensions); + for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + weights[get_weight_index(i)] = read_little_endian(stream); + + return !stream.fail(); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) const { + write_little_endian(stream, biases, OutputDimensions); + + for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) + write_little_endian(stream, weights[get_weight_index(i)]); + + return !stream.fail(); + } + // Forward propagation + void propagate(const InputType* input, OutputType* output) const { + +#if (USE_SSSE3 | (USE_NEON >= 8)) + #if defined(USE_AVX512) + using invec_t = __m512i; + using outvec_t = __m512i; + #define vec_set_32 _mm512_set1_epi32 + #define vec_add_dpbusd_32 SIMD::m512_add_dpbusd_epi32 + #elif defined(USE_AVX2) + using invec_t = __m256i; + using outvec_t = __m256i; + #define vec_set_32 _mm256_set1_epi32 + #define vec_add_dpbusd_32 SIMD::m256_add_dpbusd_epi32 + #elif defined(USE_SSSE3) + using invec_t = __m128i; + using outvec_t = __m128i; + #define vec_set_32 _mm_set1_epi32 + #define vec_add_dpbusd_32 SIMD::m128_add_dpbusd_epi32 + #elif defined(USE_NEON_DOTPROD) + using invec_t = int8x16_t; + using outvec_t = int32x4_t; + #define vec_set_32(a) vreinterpretq_s8_u32(vdupq_n_u32(a)) + #define vec_add_dpbusd_32 SIMD::dotprod_m128_add_dpbusd_epi32 + #elif defined(USE_NEON) + using invec_t = int8x16_t; + using outvec_t = int32x4_t; + #define vec_set_32(a) vreinterpretq_s8_u32(vdupq_n_u32(a)) + #define vec_add_dpbusd_32 SIMD::neon_m128_add_dpbusd_epi32 + #endif + static constexpr IndexType OutputSimdWidth = sizeof(outvec_t) / sizeof(OutputType); + + constexpr IndexType NumChunks = ceil_to_multiple(InputDimensions, 8) / ChunkSize; + constexpr IndexType NumRegs = OutputDimensions / OutputSimdWidth; + std::uint16_t nnz[NumChunks]; + IndexType count; + + const auto input32 = reinterpret_cast(input); + + // Find indices of nonzero 32-bit blocks + find_nnz(input32, nnz, count); + + const outvec_t* biasvec = reinterpret_cast(biases); + outvec_t acc[NumRegs]; + for (IndexType k = 0; k < NumRegs; ++k) + acc[k] = biasvec[k]; + + for (IndexType j = 0; j < count; ++j) + { + const auto i = nnz[j]; + const invec_t in = vec_set_32(input32[i]); + const auto col = + reinterpret_cast(&weights[i * OutputDimensions * ChunkSize]); + for (IndexType k = 0; k < NumRegs; ++k) + vec_add_dpbusd_32(acc[k], in, col[k]); + } + + outvec_t* outptr = reinterpret_cast(output); + for (IndexType k = 0; k < NumRegs; ++k) + outptr[k] = acc[k]; + #undef vec_set_32 + #undef vec_add_dpbusd_32 +#else + // Use dense implementation for the other architectures. + affine_transform_non_ssse3( + output, weights, biases, input); +#endif + } + + private: + using BiasType = OutputType; + using WeightType = std::int8_t; + + alignas(CacheLineSize) BiasType biases[OutputDimensions]; + alignas(CacheLineSize) WeightType weights[OutputDimensions * PaddedInputDimensions]; +}; + +} // namespace Stockfish::Eval::NNUE::Layers + +#endif // #ifndef NNUE_LAYERS_AFFINE_TRANSFORM_SPARSE_INPUT_H_INCLUDED diff --git a/src/nnue/layers/clipped_relu.h b/src/nnue/layers/clipped_relu.h new file mode 100644 index 0000000..2ad5a86 --- /dev/null +++ b/src/nnue/layers/clipped_relu.h @@ -0,0 +1,164 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Definition of layer ClippedReLU of NNUE evaluation function + +#ifndef NNUE_LAYERS_CLIPPED_RELU_H_INCLUDED +#define NNUE_LAYERS_CLIPPED_RELU_H_INCLUDED + +#include +#include +#include + +#include "../nnue_common.h" + +namespace Stockfish::Eval::NNUE::Layers { + +// Clipped ReLU +template +class ClippedReLU { + public: + // Input/output type + using InputType = std::int32_t; + using OutputType = std::uint8_t; + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = InDims; + static constexpr IndexType OutputDimensions = InputDimensions; + static constexpr IndexType PaddedOutputDimensions = + ceil_to_multiple(OutputDimensions, 32); + + using OutputBuffer = OutputType[PaddedOutputDimensions]; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value(std::uint32_t prevHash) { + std::uint32_t hashValue = 0x538D24C7u; + hashValue += prevHash; + return hashValue; + } + + // Read network parameters + bool read_parameters(std::istream&) { return true; } + + // Write network parameters + bool write_parameters(std::ostream&) const { return true; } + + // Forward propagation + void propagate(const InputType* input, OutputType* output) const { + +#if defined(USE_AVX2) + if constexpr (InputDimensions % SimdWidth == 0) + { + constexpr IndexType NumChunks = InputDimensions / SimdWidth; + const __m256i Offsets = _mm256_set_epi32(7, 3, 6, 2, 5, 1, 4, 0); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m256i*>(output); + for (IndexType i = 0; i < NumChunks; ++i) + { + const __m256i words0 = + _mm256_srli_epi16(_mm256_packus_epi32(_mm256_load_si256(&in[i * 4 + 0]), + _mm256_load_si256(&in[i * 4 + 1])), + WeightScaleBits); + const __m256i words1 = + _mm256_srli_epi16(_mm256_packus_epi32(_mm256_load_si256(&in[i * 4 + 2]), + _mm256_load_si256(&in[i * 4 + 3])), + WeightScaleBits); + _mm256_store_si256(&out[i], _mm256_permutevar8x32_epi32( + _mm256_packs_epi16(words0, words1), Offsets)); + } + } + else + { + constexpr IndexType NumChunks = InputDimensions / (SimdWidth / 2); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m128i*>(output); + for (IndexType i = 0; i < NumChunks; ++i) + { + const __m128i words0 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])), + WeightScaleBits); + const __m128i words1 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])), + WeightScaleBits); + _mm_store_si128(&out[i], _mm_packs_epi16(words0, words1)); + } + } + constexpr IndexType Start = InputDimensions % SimdWidth == 0 + ? InputDimensions / SimdWidth * SimdWidth + : InputDimensions / (SimdWidth / 2) * (SimdWidth / 2); + +#elif defined(USE_SSE2) + constexpr IndexType NumChunks = InputDimensions / SimdWidth; + + #ifndef USE_SSE41 + const __m128i k0x80s = _mm_set1_epi8(-128); + #endif + + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m128i*>(output); + for (IndexType i = 0; i < NumChunks; ++i) + { + #if defined(USE_SSE41) + const __m128i words0 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])), + WeightScaleBits); + const __m128i words1 = _mm_srli_epi16( + _mm_packus_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])), + WeightScaleBits); + _mm_store_si128(&out[i], _mm_packs_epi16(words0, words1)); + #else + const __m128i words0 = _mm_srai_epi16( + _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])), + WeightScaleBits); + const __m128i words1 = _mm_srai_epi16( + _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])), + WeightScaleBits); + const __m128i packedbytes = _mm_packs_epi16(words0, words1); + _mm_store_si128(&out[i], _mm_subs_epi8(_mm_adds_epi8(packedbytes, k0x80s), k0x80s)); + #endif + } + constexpr IndexType Start = NumChunks * SimdWidth; + +#elif defined(USE_NEON) + constexpr IndexType NumChunks = InputDimensions / (SimdWidth / 2); + const int8x8_t Zero = {0}; + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast(output); + for (IndexType i = 0; i < NumChunks; ++i) + { + int16x8_t shifted; + const auto pack = reinterpret_cast(&shifted); + pack[0] = vqshrn_n_s32(in[i * 2 + 0], WeightScaleBits); + pack[1] = vqshrn_n_s32(in[i * 2 + 1], WeightScaleBits); + out[i] = vmax_s8(vqmovn_s16(shifted), Zero); + } + constexpr IndexType Start = NumChunks * (SimdWidth / 2); +#else + constexpr IndexType Start = 0; +#endif + + for (IndexType i = Start; i < InputDimensions; ++i) + { + output[i] = static_cast(std::clamp(input[i] >> WeightScaleBits, 0, 127)); + } + } +}; + +} // namespace Stockfish::Eval::NNUE::Layers + +#endif // NNUE_LAYERS_CLIPPED_RELU_H_INCLUDED diff --git a/src/nnue/layers/sqr_clipped_relu.h b/src/nnue/layers/sqr_clipped_relu.h new file mode 100644 index 0000000..d14f1e0 --- /dev/null +++ b/src/nnue/layers/sqr_clipped_relu.h @@ -0,0 +1,103 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Definition of layer ClippedReLU of NNUE evaluation function + +#ifndef NNUE_LAYERS_SQR_CLIPPED_RELU_H_INCLUDED +#define NNUE_LAYERS_SQR_CLIPPED_RELU_H_INCLUDED + +#include +#include +#include + +#include "../nnue_common.h" + +namespace Stockfish::Eval::NNUE::Layers { + +// Clipped ReLU +template +class SqrClippedReLU { + public: + // Input/output type + using InputType = std::int32_t; + using OutputType = std::uint8_t; + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = InDims; + static constexpr IndexType OutputDimensions = InputDimensions; + static constexpr IndexType PaddedOutputDimensions = + ceil_to_multiple(OutputDimensions, 32); + + using OutputBuffer = OutputType[PaddedOutputDimensions]; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value(std::uint32_t prevHash) { + std::uint32_t hashValue = 0x538D24C7u; + hashValue += prevHash; + return hashValue; + } + + // Read network parameters + bool read_parameters(std::istream&) { return true; } + + // Write network parameters + bool write_parameters(std::ostream&) const { return true; } + + // Forward propagation + void propagate(const InputType* input, OutputType* output) const { + +#if defined(USE_SSE2) + constexpr IndexType NumChunks = InputDimensions / 16; + + static_assert(WeightScaleBits == 6); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m128i*>(output); + for (IndexType i = 0; i < NumChunks; ++i) + { + __m128i words0 = + _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 0]), _mm_load_si128(&in[i * 4 + 1])); + __m128i words1 = + _mm_packs_epi32(_mm_load_si128(&in[i * 4 + 2]), _mm_load_si128(&in[i * 4 + 3])); + + // We shift by WeightScaleBits * 2 = 12 and divide by 128 + // which is an additional shift-right of 7, meaning 19 in total. + // MulHi strips the lower 16 bits so we need to shift out 3 more to match. + words0 = _mm_srli_epi16(_mm_mulhi_epi16(words0, words0), 3); + words1 = _mm_srli_epi16(_mm_mulhi_epi16(words1, words1), 3); + + _mm_store_si128(&out[i], _mm_packs_epi16(words0, words1)); + } + constexpr IndexType Start = NumChunks * 16; + +#else + constexpr IndexType Start = 0; +#endif + + for (IndexType i = Start; i < InputDimensions; ++i) + { + output[i] = static_cast( + // Really should be /127 but we need to make it fast so we right-shift + // by an extra 7 bits instead. Needs to be accounted for in the trainer. + std::min(127ll, ((long long) (input[i]) * input[i]) >> (2 * WeightScaleBits + 7))); + } + } +}; + +} // namespace Stockfish::Eval::NNUE::Layers + +#endif // NNUE_LAYERS_SQR_CLIPPED_RELU_H_INCLUDED diff --git a/src/nnue/network.cpp b/src/nnue/network.cpp new file mode 100644 index 0000000..957dc7b --- /dev/null +++ b/src/nnue/network.cpp @@ -0,0 +1,442 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "network.h" + +#include +#include +#include +#include +#include +#include +#include + +#define INCBIN_SILENCE_BITCODE_WARNING +#include "../incbin/incbin.h" + +#include "../evaluate.h" +#include "../memory.h" +#include "../misc.h" +#include "../position.h" +#include "../types.h" +#include "nnue_architecture.h" +#include "nnue_common.h" +#include "nnue_misc.h" + +// Macro to embed the default efficiently updatable neural network (NNUE) file +// data in the engine binary (using incbin.h, by Dale Weiler). +// This macro invocation will declare the following three variables +// const unsigned char gEmbeddedNNUEData[]; // a pointer to the embedded data +// const unsigned char *const gEmbeddedNNUEEnd; // a marker to the end +// const unsigned int gEmbeddedNNUESize; // the size of the embedded file +// Note that this does not work in Microsoft Visual Studio. +#if !defined(_MSC_VER) && !defined(NNUE_EMBEDDING_OFF) +INCBIN(EmbeddedNNUEBig, EvalFileDefaultNameBig); +INCBIN(EmbeddedNNUESmall, EvalFileDefaultNameSmall); +#else +const unsigned char gEmbeddedNNUEBigData[1] = {0x0}; +const unsigned char* const gEmbeddedNNUEBigEnd = &gEmbeddedNNUEBigData[1]; +const unsigned int gEmbeddedNNUEBigSize = 1; +const unsigned char gEmbeddedNNUESmallData[1] = {0x0}; +const unsigned char* const gEmbeddedNNUESmallEnd = &gEmbeddedNNUESmallData[1]; +const unsigned int gEmbeddedNNUESmallSize = 1; +#endif + +namespace { + +struct EmbeddedNNUE { + EmbeddedNNUE(const unsigned char* embeddedData, + const unsigned char* embeddedEnd, + const unsigned int embeddedSize) : + data(embeddedData), + end(embeddedEnd), + size(embeddedSize) {} + const unsigned char* data; + const unsigned char* end; + const unsigned int size; +}; + +using namespace Stockfish::Eval::NNUE; + +EmbeddedNNUE get_embedded(EmbeddedNNUEType type) { + if (type == EmbeddedNNUEType::BIG) + return EmbeddedNNUE(gEmbeddedNNUEBigData, gEmbeddedNNUEBigEnd, gEmbeddedNNUEBigSize); + else + return EmbeddedNNUE(gEmbeddedNNUESmallData, gEmbeddedNNUESmallEnd, gEmbeddedNNUESmallSize); +} + +} + + +namespace Stockfish::Eval::NNUE { + + +namespace Detail { + +// Read evaluation function parameters +template +bool read_parameters(std::istream& stream, T& reference) { + + std::uint32_t header; + header = read_little_endian(stream); + if (!stream || header != T::get_hash_value()) + return false; + return reference.read_parameters(stream); +} + +// Write evaluation function parameters +template +bool write_parameters(std::ostream& stream, T& reference) { + + write_little_endian(stream, T::get_hash_value()); + return reference.write_parameters(stream); +} + +} // namespace Detail + +template +Network::Network(const Network& other) : + evalFile(other.evalFile), + embeddedType(other.embeddedType) { + + if (other.featureTransformer) + featureTransformer = make_unique_large_page(*other.featureTransformer); + + network = make_unique_aligned(LayerStacks); + + if (!other.network) + return; + + for (std::size_t i = 0; i < LayerStacks; ++i) + network[i] = other.network[i]; +} + +template +Network& +Network::operator=(const Network& other) { + evalFile = other.evalFile; + embeddedType = other.embeddedType; + + if (other.featureTransformer) + featureTransformer = make_unique_large_page(*other.featureTransformer); + + network = make_unique_aligned(LayerStacks); + + if (!other.network) + return *this; + + for (std::size_t i = 0; i < LayerStacks; ++i) + network[i] = other.network[i]; + + return *this; +} + +template +void Network::load(const std::string& rootDirectory, std::string evalfilePath) { +#if defined(DEFAULT_NNUE_DIRECTORY) + std::vector dirs = {"", "", rootDirectory, + stringify(DEFAULT_NNUE_DIRECTORY)}; +#else + std::vector dirs = {"", "", rootDirectory}; +#endif + + if (evalfilePath.empty()) + evalfilePath = evalFile.defaultName; + + for (const auto& directory : dirs) + { + if (evalFile.current != evalfilePath) + { + if (directory != "") + { + load_user_net(directory, evalfilePath); + } + + if (directory == "" && evalfilePath == evalFile.defaultName) + { + load_internal(); + } + } + } +} + + +template +bool Network::save(const std::optional& filename) const { + std::string actualFilename; + std::string msg; + + if (filename.has_value()) + actualFilename = filename.value(); + else + { + if (evalFile.current != evalFile.defaultName) + { + msg = "Failed to export a net. " + "A non-embedded net can only be saved if the filename is specified"; + + sync_cout << msg << sync_endl; + return false; + } + + actualFilename = evalFile.defaultName; + } + + std::ofstream stream(actualFilename, std::ios_base::binary); + bool saved = save(stream, evalFile.current, evalFile.netDescription); + + msg = saved ? "Network saved successfully to " + actualFilename : "Failed to export a net"; + + sync_cout << msg << sync_endl; + return saved; +} + + +template +NetworkOutput +Network::evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache* cache) const { + + constexpr uint64_t alignment = CacheLineSize; + + alignas(alignment) + TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; + + ASSERT_ALIGNED(transformedFeatures, alignment); + + const int bucket = (pos.count() - 1) / 4; + const auto psqt = + featureTransformer->transform(pos, accumulatorStack, cache, transformedFeatures, bucket); + const auto positional = network[bucket].propagate(transformedFeatures); + return {static_cast(psqt / OutputScale), static_cast(positional / OutputScale)}; +} + + +template +void Network::verify(std::string evalfilePath, + const std::function& f) const { + if (evalfilePath.empty()) + evalfilePath = evalFile.defaultName; + + if (evalFile.current != evalfilePath) + { + if (f) + { + std::string msg1 = + "Network evaluation parameters compatible with the engine must be available."; + std::string msg2 = "The network file " + evalfilePath + " was not loaded successfully."; + std::string msg3 = "The UCI option EvalFile might need to specify the full path, " + "including the directory name, to the network file."; + std::string msg4 = "The default net can be downloaded from: " + "https://tests.stockfishchess.org/api/nn/" + + evalFile.defaultName; + std::string msg5 = "The engine will be terminated now."; + + std::string msg = "ERROR: " + msg1 + '\n' + "ERROR: " + msg2 + '\n' + "ERROR: " + msg3 + + '\n' + "ERROR: " + msg4 + '\n' + "ERROR: " + msg5 + '\n'; + + f(msg); + } + + exit(EXIT_FAILURE); + } + + if (f) + { + size_t size = sizeof(*featureTransformer) + sizeof(Arch) * LayerStacks; + f("NNUE evaluation using " + evalfilePath + " (" + std::to_string(size / (1024 * 1024)) + + "MiB, (" + std::to_string(featureTransformer->InputDimensions) + ", " + + std::to_string(network[0].TransformedFeatureDimensions) + ", " + + std::to_string(network[0].FC_0_OUTPUTS) + ", " + std::to_string(network[0].FC_1_OUTPUTS) + + ", 1))"); + } +} + + +template +NnueEvalTrace +Network::trace_evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache* cache) const { + + constexpr uint64_t alignment = CacheLineSize; + + alignas(alignment) + TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; + + ASSERT_ALIGNED(transformedFeatures, alignment); + + NnueEvalTrace t{}; + t.correctBucket = (pos.count() - 1) / 4; + for (IndexType bucket = 0; bucket < LayerStacks; ++bucket) + { + const auto materialist = + featureTransformer->transform(pos, accumulatorStack, cache, transformedFeatures, bucket); + const auto positional = network[bucket].propagate(transformedFeatures); + + t.psqt[bucket] = static_cast(materialist / OutputScale); + t.positional[bucket] = static_cast(positional / OutputScale); + } + + return t; +} + + +template +void Network::load_user_net(const std::string& dir, + const std::string& evalfilePath) { + std::ifstream stream(dir + evalfilePath, std::ios::binary); + auto description = load(stream); + + if (description.has_value()) + { + evalFile.current = evalfilePath; + evalFile.netDescription = description.value(); + } +} + + +template +void Network::load_internal() { + // C++ way to prepare a buffer for a memory stream + class MemoryBuffer: public std::basic_streambuf { + public: + MemoryBuffer(char* p, size_t n) { + setg(p, p, p + n); + setp(p, p + n); + } + }; + + const auto embedded = get_embedded(embeddedType); + + MemoryBuffer buffer(const_cast(reinterpret_cast(embedded.data)), + size_t(embedded.size)); + + std::istream stream(&buffer); + auto description = load(stream); + + if (description.has_value()) + { + evalFile.current = evalFile.defaultName; + evalFile.netDescription = description.value(); + } +} + + +template +void Network::initialize() { + featureTransformer = make_unique_large_page(); + network = make_unique_aligned(LayerStacks); +} + + +template +bool Network::save(std::ostream& stream, + const std::string& name, + const std::string& netDescription) const { + if (name.empty() || name == "None") + return false; + + return write_parameters(stream, netDescription); +} + + +template +std::optional Network::load(std::istream& stream) { + initialize(); + std::string description; + + return read_parameters(stream, description) ? std::make_optional(description) : std::nullopt; +} + + +// Read network header +template +bool Network::read_header(std::istream& stream, + std::uint32_t* hashValue, + std::string* desc) const { + std::uint32_t version, size; + + version = read_little_endian(stream); + *hashValue = read_little_endian(stream); + size = read_little_endian(stream); + if (!stream || version != Version) + return false; + desc->resize(size); + stream.read(&(*desc)[0], size); + return !stream.fail(); +} + + +// Write network header +template +bool Network::write_header(std::ostream& stream, + std::uint32_t hashValue, + const std::string& desc) const { + write_little_endian(stream, Version); + write_little_endian(stream, hashValue); + write_little_endian(stream, std::uint32_t(desc.size())); + stream.write(&desc[0], desc.size()); + return !stream.fail(); +} + + +template +bool Network::read_parameters(std::istream& stream, + std::string& netDescription) const { + std::uint32_t hashValue; + if (!read_header(stream, &hashValue, &netDescription)) + return false; + if (hashValue != Network::hash) + return false; + if (!Detail::read_parameters(stream, *featureTransformer)) + return false; + for (std::size_t i = 0; i < LayerStacks; ++i) + { + if (!Detail::read_parameters(stream, network[i])) + return false; + } + return stream && stream.peek() == std::ios::traits_type::eof(); +} + + +template +bool Network::write_parameters(std::ostream& stream, + const std::string& netDescription) const { + if (!write_header(stream, Network::hash, netDescription)) + return false; + if (!Detail::write_parameters(stream, *featureTransformer)) + return false; + for (std::size_t i = 0; i < LayerStacks; ++i) + { + if (!Detail::write_parameters(stream, network[i])) + return false; + } + return bool(stream); +} + +// Explicit template instantiations + +template class Network, + FeatureTransformer>; + +template class Network, + FeatureTransformer>; + +} // namespace Stockfish::Eval::NNUE diff --git a/src/nnue/network.h b/src/nnue/network.h new file mode 100644 index 0000000..c935882 --- /dev/null +++ b/src/nnue/network.h @@ -0,0 +1,137 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef NETWORK_H_INCLUDED +#define NETWORK_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../memory.h" +#include "../types.h" +#include "nnue_accumulator.h" +#include "nnue_architecture.h" +#include "nnue_common.h" +#include "nnue_feature_transformer.h" +#include "nnue_misc.h" + +namespace Stockfish { +class Position; +} + +namespace Stockfish::Eval::NNUE { + +enum class EmbeddedNNUEType { + BIG, + SMALL, +}; + +using NetworkOutput = std::tuple; + +template +class Network { + static constexpr IndexType FTDimensions = Arch::TransformedFeatureDimensions; + + public: + Network(EvalFile file, EmbeddedNNUEType type) : + evalFile(file), + embeddedType(type) {} + + Network(const Network& other); + Network(Network&& other) = default; + + Network& operator=(const Network& other); + Network& operator=(Network&& other) = default; + + void load(const std::string& rootDirectory, std::string evalfilePath); + bool save(const std::optional& filename) const; + + NetworkOutput evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache* cache) const; + + + void verify(std::string evalfilePath, const std::function&) const; + NnueEvalTrace trace_evaluate(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache* cache) const; + + private: + void load_user_net(const std::string&, const std::string&); + void load_internal(); + + void initialize(); + + bool save(std::ostream&, const std::string&, const std::string&) const; + std::optional load(std::istream&); + + bool read_header(std::istream&, std::uint32_t*, std::string*) const; + bool write_header(std::ostream&, std::uint32_t, const std::string&) const; + + bool read_parameters(std::istream&, std::string&) const; + bool write_parameters(std::ostream&, const std::string&) const; + + // Input feature converter + LargePagePtr featureTransformer; + + // Evaluation function + AlignedPtr network; + + EvalFile evalFile; + EmbeddedNNUEType embeddedType; + + // Hash value of evaluation function structure + static constexpr std::uint32_t hash = Transformer::get_hash_value() ^ Arch::get_hash_value(); + + template + friend struct AccumulatorCaches::Cache; + + friend class AccumulatorStack; +}; + +// Definitions of the network types +using SmallFeatureTransformer = FeatureTransformer; +using SmallNetworkArchitecture = + NetworkArchitecture; + +using BigFeatureTransformer = FeatureTransformer; +using BigNetworkArchitecture = NetworkArchitecture; + +using NetworkBig = Network; +using NetworkSmall = Network; + + +struct Networks { + Networks(NetworkBig&& nB, NetworkSmall&& nS) : + big(std::move(nB)), + small(std::move(nS)) {} + + NetworkBig big; + NetworkSmall small; +}; + + +} // namespace Stockfish + +#endif diff --git a/src/nnue/network.o b/src/nnue/network.o new file mode 100644 index 0000000000000000000000000000000000000000..d443d9ff3af5caa0b03951554c11ce8f300cf994 GIT binary patch literal 419632 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�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 |3}js{NiR4S=tH@0ad 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ⅈ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ΠT%@pMcwO)z$3ZiunE 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 literal 0 HcmV?d00001 diff --git a/src/nnue/nnue_accumulator.cpp b/src/nnue/nnue_accumulator.cpp new file mode 100644 index 0000000..d13105a --- /dev/null +++ b/src/nnue/nnue_accumulator.cpp @@ -0,0 +1,531 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "nnue_accumulator.h" + +#include +#include +#include +#include + +#include "../bitboard.h" +#include "../misc.h" +#include "../position.h" +#include "../types.h" +#include "nnue_architecture.h" +#include "nnue_feature_transformer.h" // IWYU pragma: keep +#include "simd.h" + +namespace Stockfish::Eval::NNUE { + +using namespace SIMD; + +namespace { + +template +void double_inc_update(const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& middle_state, + AccumulatorState& target_state, + const AccumulatorState& computed); + +template +void update_accumulator_incremental( + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& target_state, + const AccumulatorState& computed); + +template +void update_accumulator_refresh_cache(const FeatureTransformer& featureTransformer, + const Position& pos, + AccumulatorState& accumulatorState, + AccumulatorCaches::Cache& cache); + +} + +void AccumulatorState::reset(const DirtyPiece& dp) noexcept { + dirtyPiece = dp; + accumulatorBig.computed.fill(false); + accumulatorSmall.computed.fill(false); +} + +const AccumulatorState& AccumulatorStack::latest() const noexcept { return accumulators[size - 1]; } + +AccumulatorState& AccumulatorStack::mut_latest() noexcept { return accumulators[size - 1]; } + +void AccumulatorStack::reset() noexcept { + accumulators[0].reset({}); + size = 1; +} + +void AccumulatorStack::push(const DirtyPiece& dirtyPiece) noexcept { + assert(size + 1 < accumulators.size()); + accumulators[size].reset(dirtyPiece); + size++; +} + +void AccumulatorStack::pop() noexcept { + assert(size > 1); + size--; +} + +template +void AccumulatorStack::evaluate(const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept { + + evaluate_side(pos, featureTransformer, cache); + evaluate_side(pos, featureTransformer, cache); +} + +template +void AccumulatorStack::evaluate_side(const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept { + + const auto last_usable_accum = find_last_usable_accumulator(); + + if ((accumulators[last_usable_accum].template acc()).computed[Perspective]) + forward_update_incremental(pos, featureTransformer, last_usable_accum); + + else + { + update_accumulator_refresh_cache(featureTransformer, pos, mut_latest(), cache); + backward_update_incremental(pos, featureTransformer, last_usable_accum); + } +} + +// Find the earliest usable accumulator, this can either be a computed accumulator or the accumulator +// state just before a change that requires full refresh. +template +std::size_t AccumulatorStack::find_last_usable_accumulator() const noexcept { + + for (std::size_t curr_idx = size - 1; curr_idx > 0; curr_idx--) + { + if ((accumulators[curr_idx].template acc()).computed[Perspective]) + return curr_idx; + + if (FeatureSet::requires_refresh(accumulators[curr_idx].dirtyPiece, Perspective)) + return curr_idx; + } + + return 0; +} + +template +void AccumulatorStack::forward_update_incremental( + const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t begin) noexcept { + + assert(begin < accumulators.size()); + assert((accumulators[begin].acc()).computed[Perspective]); + + const Square ksq = pos.square(Perspective); + + for (std::size_t next = begin + 1; next < size; next++) + { + if (next + 1 < size) + { + DirtyPiece& dp1 = accumulators[next].dirtyPiece; + DirtyPiece& dp2 = accumulators[next + 1].dirtyPiece; + + if (dp1.to != SQ_NONE && dp1.to == dp2.remove_sq) + { + const Square captureSq = dp1.to; + dp1.to = dp2.remove_sq = SQ_NONE; + double_inc_update(featureTransformer, ksq, accumulators[next], + accumulators[next + 1], accumulators[next - 1]); + dp1.to = dp2.remove_sq = captureSq; + + next++; + continue; + } + } + update_accumulator_incremental( + featureTransformer, ksq, accumulators[next], accumulators[next - 1]); + } + + assert((latest().acc()).computed[Perspective]); +} + +template +void AccumulatorStack::backward_update_incremental( + const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t end) noexcept { + + assert(end < accumulators.size()); + assert(end < size); + assert((latest().acc()).computed[Perspective]); + + const Square ksq = pos.square(Perspective); + + for (std::int64_t next = std::int64_t(size) - 2; next >= std::int64_t(end); next--) + update_accumulator_incremental( + featureTransformer, ksq, accumulators[next], accumulators[next + 1]); + + assert((accumulators[end].acc()).computed[Perspective]); +} + +// Explicit template instantiations +template void AccumulatorStack::evaluate( + const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; +template void AccumulatorStack::evaluate( + const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; + + +namespace { + +template, bool> = true> +void fused_row_reduce(const ElementType* in, ElementType* out, const Ts* const... rows) { + constexpr IndexType size = Width * sizeof(ElementType) / sizeof(typename VectorWrapper::type); + + auto* vecIn = reinterpret_cast(in); + auto* vecOut = reinterpret_cast(out); + + for (IndexType i = 0; i < size; ++i) + vecOut[i] = fused( + vecIn[i], reinterpret_cast(rows)[i]...); +} + +template +struct AccumulatorUpdateContext { + const FeatureTransformer& featureTransformer; + const AccumulatorState& from; + AccumulatorState& to; + + AccumulatorUpdateContext(const FeatureTransformer& ft, + const AccumulatorState& accF, + AccumulatorState& accT) noexcept : + featureTransformer{ft}, + from{accF}, + to{accT} {} + + template, bool> = true> + void apply(const Ts... indices) { + auto to_weight_vector = [&](const IndexType index) { + return &featureTransformer.weights[index * Dimensions]; + }; + + auto to_psqt_weight_vector = [&](const IndexType index) { + return &featureTransformer.psqtWeights[index * PSQTBuckets]; + }; + + fused_row_reduce( + (from.acc()).accumulation[Perspective], + (to.acc()).accumulation[Perspective], to_weight_vector(indices)...); + + fused_row_reduce( + (from.acc()).psqtAccumulation[Perspective], + (to.acc()).psqtAccumulation[Perspective], to_psqt_weight_vector(indices)...); + } +}; + +template +auto make_accumulator_update_context(const FeatureTransformer& featureTransformer, + const AccumulatorState& accumulatorFrom, + AccumulatorState& accumulatorTo) noexcept { + return AccumulatorUpdateContext{featureTransformer, accumulatorFrom, + accumulatorTo}; +} + +template +void double_inc_update(const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& middle_state, + AccumulatorState& target_state, + const AccumulatorState& computed) { + + assert(computed.acc().computed[Perspective]); + assert(!middle_state.acc().computed[Perspective]); + assert(!target_state.acc().computed[Perspective]); + + FeatureSet::IndexList removed, added; + FeatureSet::append_changed_indices(ksq, middle_state.dirtyPiece, removed, added); + // you can't capture a piece that was just involved in castling since the rook ends up + // in a square that the king passed + assert(added.size() < 2); + FeatureSet::append_changed_indices(ksq, target_state.dirtyPiece, removed, added); + + assert(added.size() == 1); + assert(removed.size() == 2 || removed.size() == 3); + + // Workaround compiler warning for uninitialized variables, replicated on + // profile builds on windows with gcc 14.2.0. + // TODO remove once unneeded + sf_assume(added.size() == 1); + sf_assume(removed.size() == 2 || removed.size() == 3); + + auto updateContext = + make_accumulator_update_context(featureTransformer, computed, target_state); + + if (removed.size() == 2) + { + updateContext.template apply(added[0], removed[0], removed[1]); + } + else + { + updateContext.template apply(added[0], removed[0], removed[1], + removed[2]); + } + + target_state.acc().computed[Perspective] = true; +} + +template +void update_accumulator_incremental( + const FeatureTransformer& featureTransformer, + const Square ksq, + AccumulatorState& target_state, + const AccumulatorState& computed) { + + assert((computed.acc()).computed[Perspective]); + assert(!(target_state.acc()).computed[Perspective]); + + // The size must be enough to contain the largest possible update. + // That might depend on the feature set and generally relies on the + // feature set's update cost calculation to be correct and never allow + // updates with more added/removed features than MaxActiveDimensions. + // In this case, the maximum size of both feature addition and removal + // is 2, since we are incrementally updating one move at a time. + FeatureSet::IndexList removed, added; + if constexpr (Forward) + FeatureSet::append_changed_indices(ksq, target_state.dirtyPiece, removed, + added); + else + FeatureSet::append_changed_indices(ksq, computed.dirtyPiece, added, removed); + + assert(added.size() == 1 || added.size() == 2); + assert(removed.size() == 1 || removed.size() == 2); + assert((Forward && added.size() <= removed.size()) + || (!Forward && added.size() >= removed.size())); + + // Workaround compiler warning for uninitialized variables, replicated on + // profile builds on windows with gcc 14.2.0. + // TODO remove once unneeded + sf_assume(added.size() == 1 || added.size() == 2); + sf_assume(removed.size() == 1 || removed.size() == 2); + + auto updateContext = + make_accumulator_update_context(featureTransformer, computed, target_state); + + if ((Forward && removed.size() == 1) || (!Forward && added.size() == 1)) + { + assert(added.size() == 1 && removed.size() == 1); + updateContext.template apply(added[0], removed[0]); + } + else if (Forward && added.size() == 1) + { + assert(removed.size() == 2); + updateContext.template apply(added[0], removed[0], removed[1]); + } + else if (!Forward && removed.size() == 1) + { + assert(added.size() == 2); + updateContext.template apply(added[0], added[1], removed[0]); + } + else + { + assert(added.size() == 2 && removed.size() == 2); + updateContext.template apply(added[0], added[1], removed[0], + removed[1]); + } + + (target_state.acc()).computed[Perspective] = true; +} + +template +void update_accumulator_refresh_cache(const FeatureTransformer& featureTransformer, + const Position& pos, + AccumulatorState& accumulatorState, + AccumulatorCaches::Cache& cache) { + + using Tiling [[maybe_unused]] = SIMDTiling; + + const Square ksq = pos.square(Perspective); + auto& entry = cache[ksq][Perspective]; + FeatureSet::IndexList removed, added; + + for (Color c : {WHITE, BLACK}) + { + for (PieceType pt = PAWN; pt <= KING; ++pt) + { + const Piece piece = make_piece(c, pt); + const Bitboard oldBB = entry.byColorBB[c] & entry.byTypeBB[pt]; + const Bitboard newBB = pos.pieces(c, pt); + Bitboard toRemove = oldBB & ~newBB; + Bitboard toAdd = newBB & ~oldBB; + + while (toRemove) + { + Square sq = pop_lsb(toRemove); + removed.push_back(FeatureSet::make_index(sq, piece, ksq)); + } + while (toAdd) + { + Square sq = pop_lsb(toAdd); + added.push_back(FeatureSet::make_index(sq, piece, ksq)); + } + } + } + + auto& accumulator = accumulatorState.acc(); + accumulator.computed[Perspective] = true; + +#ifdef VECTOR + vec_t acc[Tiling::NumRegs]; + psqt_vec_t psqt[Tiling::NumPsqtRegs]; + + for (IndexType j = 0; j < Dimensions / Tiling::TileHeight; ++j) + { + auto* accTile = + reinterpret_cast(&accumulator.accumulation[Perspective][j * Tiling::TileHeight]); + auto* entryTile = reinterpret_cast(&entry.accumulation[j * Tiling::TileHeight]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = entryTile[k]; + + IndexType i = 0; + for (; i < std::min(removed.size(), added.size()); ++i) + { + IndexType indexR = removed[i]; + const IndexType offsetR = Dimensions * indexR + j * Tiling::TileHeight; + auto* columnR = reinterpret_cast(&featureTransformer.weights[offsetR]); + IndexType indexA = added[i]; + const IndexType offsetA = Dimensions * indexA + j * Tiling::TileHeight; + auto* columnA = reinterpret_cast(&featureTransformer.weights[offsetA]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = fused(acc[k], columnA[k], columnR[k]); + } + for (; i < removed.size(); ++i) + { + IndexType index = removed[i]; + const IndexType offset = Dimensions * index + j * Tiling::TileHeight; + auto* column = reinterpret_cast(&featureTransformer.weights[offset]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_sub_16(acc[k], column[k]); + } + for (; i < added.size(); ++i) + { + IndexType index = added[i]; + const IndexType offset = Dimensions * index + j * Tiling::TileHeight; + auto* column = reinterpret_cast(&featureTransformer.weights[offset]); + + for (IndexType k = 0; k < Tiling::NumRegs; ++k) + acc[k] = vec_add_16(acc[k], column[k]); + } + + for (IndexType k = 0; k < Tiling::NumRegs; k++) + vec_store(&entryTile[k], acc[k]); + for (IndexType k = 0; k < Tiling::NumRegs; k++) + vec_store(&accTile[k], acc[k]); + } + + for (IndexType j = 0; j < PSQTBuckets / Tiling::PsqtTileHeight; ++j) + { + auto* accTilePsqt = reinterpret_cast( + &accumulator.psqtAccumulation[Perspective][j * Tiling::PsqtTileHeight]); + auto* entryTilePsqt = + reinterpret_cast(&entry.psqtAccumulation[j * Tiling::PsqtTileHeight]); + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = entryTilePsqt[k]; + + for (IndexType i = 0; i < removed.size(); ++i) + { + IndexType index = removed[i]; + const IndexType offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = + reinterpret_cast(&featureTransformer.psqtWeights[offset]); + + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_sub_psqt_32(psqt[k], columnPsqt[k]); + } + for (IndexType i = 0; i < added.size(); ++i) + { + IndexType index = added[i]; + const IndexType offset = PSQTBuckets * index + j * Tiling::PsqtTileHeight; + auto* columnPsqt = + reinterpret_cast(&featureTransformer.psqtWeights[offset]); + + for (std::size_t k = 0; k < Tiling::NumPsqtRegs; ++k) + psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); + } + + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + vec_store_psqt(&entryTilePsqt[k], psqt[k]); + for (IndexType k = 0; k < Tiling::NumPsqtRegs; ++k) + vec_store_psqt(&accTilePsqt[k], psqt[k]); + } + +#else + + for (const auto index : removed) + { + const IndexType offset = Dimensions * index; + for (IndexType j = 0; j < Dimensions; ++j) + entry.accumulation[j] -= featureTransformer.weights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + entry.psqtAccumulation[k] -= featureTransformer.psqtWeights[index * PSQTBuckets + k]; + } + for (const auto index : added) + { + const IndexType offset = Dimensions * index; + for (IndexType j = 0; j < Dimensions; ++j) + entry.accumulation[j] += featureTransformer.weights[offset + j]; + + for (std::size_t k = 0; k < PSQTBuckets; ++k) + entry.psqtAccumulation[k] += featureTransformer.psqtWeights[index * PSQTBuckets + k]; + } + + // The accumulator of the refresh entry has been updated. + // Now copy its content to the actual accumulator we were refreshing. + + std::memcpy(accumulator.accumulation[Perspective], entry.accumulation, + sizeof(BiasType) * Dimensions); + + std::memcpy(accumulator.psqtAccumulation[Perspective], entry.psqtAccumulation, + sizeof(int32_t) * PSQTBuckets); +#endif + + for (Color c : {WHITE, BLACK}) + entry.byColorBB[c] = pos.pieces(c); + + for (PieceType pt = PAWN; pt <= KING; ++pt) + entry.byTypeBB[pt] = pos.pieces(pt); +} + +} + +} diff --git a/src/nnue/nnue_accumulator.h b/src/nnue/nnue_accumulator.h new file mode 100644 index 0000000..10aadc9 --- /dev/null +++ b/src/nnue/nnue_accumulator.h @@ -0,0 +1,187 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Class for difference calculation of NNUE evaluation function + +#ifndef NNUE_ACCUMULATOR_H_INCLUDED +#define NNUE_ACCUMULATOR_H_INCLUDED + +#include +#include +#include +#include +#include + +#include "../types.h" +#include "nnue_architecture.h" +#include "nnue_common.h" + +namespace Stockfish { +class Position; +} + +namespace Stockfish::Eval::NNUE { + +template +struct alignas(CacheLineSize) Accumulator; + +template +class FeatureTransformer; + +// Class that holds the result of affine transformation of input features +template +struct alignas(CacheLineSize) Accumulator { + std::int16_t accumulation[COLOR_NB][Size]; + std::int32_t psqtAccumulation[COLOR_NB][PSQTBuckets]; + std::array computed; +}; + + +// AccumulatorCaches struct provides per-thread accumulator caches, where each +// cache contains multiple entries for each of the possible king squares. +// When the accumulator needs to be refreshed, the cached entry is used to more +// efficiently update the accumulator, instead of rebuilding it from scratch. +// This idea, was first described by Luecx (author of Koivisto) and +// is commonly referred to as "Finny Tables". +struct AccumulatorCaches { + + template + AccumulatorCaches(const Networks& networks) { + clear(networks); + } + + template + struct alignas(CacheLineSize) Cache { + + struct alignas(CacheLineSize) Entry { + BiasType accumulation[Size]; + PSQTWeightType psqtAccumulation[PSQTBuckets]; + Bitboard byColorBB[COLOR_NB]; + Bitboard byTypeBB[PIECE_TYPE_NB]; + + // To initialize a refresh entry, we set all its bitboards empty, + // so we put the biases in the accumulation, without any weights on top + void clear(const BiasType* biases) { + + std::memcpy(accumulation, biases, sizeof(accumulation)); + std::memset((uint8_t*) this + offsetof(Entry, psqtAccumulation), 0, + sizeof(Entry) - offsetof(Entry, psqtAccumulation)); + } + }; + + template + void clear(const Network& network) { + for (auto& entries1D : entries) + for (auto& entry : entries1D) + entry.clear(network.featureTransformer->biases); + } + + std::array& operator[](Square sq) { return entries[sq]; } + + std::array, SQUARE_NB> entries; + }; + + template + void clear(const Networks& networks) { + big.clear(networks.big); + small.clear(networks.small); + } + + Cache big; + Cache small; +}; + + +struct AccumulatorState { + Accumulator accumulatorBig; + Accumulator accumulatorSmall; + DirtyPiece dirtyPiece; + + template + auto& acc() noexcept { + static_assert(Size == TransformedFeatureDimensionsBig + || Size == TransformedFeatureDimensionsSmall, + "Invalid size for accumulator"); + + if constexpr (Size == TransformedFeatureDimensionsBig) + return accumulatorBig; + else if constexpr (Size == TransformedFeatureDimensionsSmall) + return accumulatorSmall; + } + + template + const auto& acc() const noexcept { + static_assert(Size == TransformedFeatureDimensionsBig + || Size == TransformedFeatureDimensionsSmall, + "Invalid size for accumulator"); + + if constexpr (Size == TransformedFeatureDimensionsBig) + return accumulatorBig; + else if constexpr (Size == TransformedFeatureDimensionsSmall) + return accumulatorSmall; + } + + void reset(const DirtyPiece& dp) noexcept; +}; + + +class AccumulatorStack { + public: + AccumulatorStack() : + accumulators(MAX_PLY + 1), + size{1} {} + + [[nodiscard]] const AccumulatorState& latest() const noexcept; + + void reset() noexcept; + void push(const DirtyPiece& dirtyPiece) noexcept; + void pop() noexcept; + + template + void evaluate(const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; + + private: + [[nodiscard]] AccumulatorState& mut_latest() noexcept; + + template + void evaluate_side(const Position& pos, + const FeatureTransformer& featureTransformer, + AccumulatorCaches::Cache& cache) noexcept; + + template + [[nodiscard]] std::size_t find_last_usable_accumulator() const noexcept; + + template + void forward_update_incremental(const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t begin) noexcept; + + template + void backward_update_incremental(const Position& pos, + const FeatureTransformer& featureTransformer, + const std::size_t end) noexcept; + + std::vector accumulators; + std::size_t size; +}; + +} // namespace Stockfish::Eval::NNUE + +#endif // NNUE_ACCUMULATOR_H_INCLUDED diff --git a/src/nnue/nnue_accumulator.o b/src/nnue/nnue_accumulator.o new file mode 100644 index 0000000000000000000000000000000000000000..587a4e5d1b8fea574f7240213d84f8f066e1188c GIT binary patch literal 235480 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

{>HGc0b?pY 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. +*/ + +// Input features and network structure used in NNUE evaluation function + +#ifndef NNUE_ARCHITECTURE_H_INCLUDED +#define NNUE_ARCHITECTURE_H_INCLUDED + +#include +#include +#include + +#include "features/half_ka_v2_hm.h" +#include "layers/affine_transform.h" +#include "layers/affine_transform_sparse_input.h" +#include "layers/clipped_relu.h" +#include "layers/sqr_clipped_relu.h" +#include "nnue_common.h" + +namespace Stockfish::Eval::NNUE { + +// Input features used in evaluation function +using FeatureSet = Features::HalfKAv2_hm; + +// Number of input feature dimensions after conversion +constexpr IndexType TransformedFeatureDimensionsBig = 3072; +constexpr int L2Big = 15; +constexpr int L3Big = 32; + +constexpr IndexType TransformedFeatureDimensionsSmall = 128; +constexpr int L2Small = 15; +constexpr int L3Small = 32; + +constexpr IndexType PSQTBuckets = 8; +constexpr IndexType LayerStacks = 8; + +// If vector instructions are enabled, we update and refresh the +// accumulator tile by tile such that each tile fits in the CPU's +// vector registers. +static_assert(PSQTBuckets % 8 == 0, + "Per feature PSQT values cannot be processed at granularity lower than 8 at a time."); + +template +struct NetworkArchitecture { + static constexpr IndexType TransformedFeatureDimensions = L1; + static constexpr int FC_0_OUTPUTS = L2; + static constexpr int FC_1_OUTPUTS = L3; + + Layers::AffineTransformSparseInput fc_0; + Layers::SqrClippedReLU ac_sqr_0; + Layers::ClippedReLU ac_0; + Layers::AffineTransform fc_1; + Layers::ClippedReLU ac_1; + Layers::AffineTransform fc_2; + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value() { + // input slice hash + std::uint32_t hashValue = 0xEC42E90Du; + hashValue ^= TransformedFeatureDimensions * 2; + + hashValue = decltype(fc_0)::get_hash_value(hashValue); + hashValue = decltype(ac_0)::get_hash_value(hashValue); + hashValue = decltype(fc_1)::get_hash_value(hashValue); + hashValue = decltype(ac_1)::get_hash_value(hashValue); + hashValue = decltype(fc_2)::get_hash_value(hashValue); + + return hashValue; + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + return fc_0.read_parameters(stream) && ac_0.read_parameters(stream) + && fc_1.read_parameters(stream) && ac_1.read_parameters(stream) + && fc_2.read_parameters(stream); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) const { + return fc_0.write_parameters(stream) && ac_0.write_parameters(stream) + && fc_1.write_parameters(stream) && ac_1.write_parameters(stream) + && fc_2.write_parameters(stream); + } + + std::int32_t propagate(const TransformedFeatureType* transformedFeatures) { + struct alignas(CacheLineSize) Buffer { + alignas(CacheLineSize) typename decltype(fc_0)::OutputBuffer fc_0_out; + alignas(CacheLineSize) typename decltype(ac_sqr_0)::OutputType + ac_sqr_0_out[ceil_to_multiple(FC_0_OUTPUTS * 2, 32)]; + alignas(CacheLineSize) typename decltype(ac_0)::OutputBuffer ac_0_out; + alignas(CacheLineSize) typename decltype(fc_1)::OutputBuffer fc_1_out; + alignas(CacheLineSize) typename decltype(ac_1)::OutputBuffer ac_1_out; + alignas(CacheLineSize) typename decltype(fc_2)::OutputBuffer fc_2_out; + + Buffer() { std::memset(this, 0, sizeof(*this)); } + }; + +#if defined(__clang__) && (__APPLE__) + // workaround for a bug reported with xcode 12 + static thread_local auto tlsBuffer = std::make_unique(); + // Access TLS only once, cache result. + Buffer& buffer = *tlsBuffer; +#else + alignas(CacheLineSize) static thread_local Buffer buffer; +#endif + + fc_0.propagate(transformedFeatures, buffer.fc_0_out); + ac_sqr_0.propagate(buffer.fc_0_out, buffer.ac_sqr_0_out); + ac_0.propagate(buffer.fc_0_out, buffer.ac_0_out); + std::memcpy(buffer.ac_sqr_0_out + FC_0_OUTPUTS, buffer.ac_0_out, + FC_0_OUTPUTS * sizeof(typename decltype(ac_0)::OutputType)); + fc_1.propagate(buffer.ac_sqr_0_out, buffer.fc_1_out); + ac_1.propagate(buffer.fc_1_out, buffer.ac_1_out); + fc_2.propagate(buffer.ac_1_out, buffer.fc_2_out); + + // buffer.fc_0_out[FC_0_OUTPUTS] is such that 1.0 is equal to 127*(1<. +*/ + +// Constants used in NNUE evaluation function + +#ifndef NNUE_COMMON_H_INCLUDED +#define NNUE_COMMON_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include "../misc.h" + +#if defined(USE_AVX2) + #include + +#elif defined(USE_SSE41) + #include + +#elif defined(USE_SSSE3) + #include + +#elif defined(USE_SSE2) + #include + +#elif defined(USE_NEON) + #include +#endif + +namespace Stockfish::Eval::NNUE { + +using BiasType = std::int16_t; +using WeightType = std::int16_t; +using PSQTWeightType = std::int32_t; +using IndexType = std::uint32_t; + +// Version of the evaluation file +constexpr std::uint32_t Version = 0x7AF32F20u; + +// Constant used in evaluation value calculation +constexpr int OutputScale = 16; +constexpr int WeightScaleBits = 6; + +// Size of cache line (in bytes) +constexpr std::size_t CacheLineSize = 64; + +constexpr const char Leb128MagicString[] = "COMPRESSED_LEB128"; +constexpr const std::size_t Leb128MagicStringSize = sizeof(Leb128MagicString) - 1; + +// SIMD width (in bytes) +#if defined(USE_AVX2) +constexpr std::size_t SimdWidth = 32; + +#elif defined(USE_SSE2) +constexpr std::size_t SimdWidth = 16; + +#elif defined(USE_NEON) +constexpr std::size_t SimdWidth = 16; +#endif + +constexpr std::size_t MaxSimdWidth = 32; + +// Type of input feature after conversion +using TransformedFeatureType = std::uint8_t; +using IndexType = std::uint32_t; + +// Round n up to be a multiple of base +template +constexpr IntType ceil_to_multiple(IntType n, IntType base) { + return (n + base - 1) / base * base; +} + + +// Utility to read an integer (signed or unsigned, any size) +// from a stream in little-endian order. We swap the byte order after the read if +// necessary to return a result with the byte ordering of the compiling machine. +template +inline IntType read_little_endian(std::istream& stream) { + IntType result; + + if (IsLittleEndian) + stream.read(reinterpret_cast(&result), sizeof(IntType)); + else + { + std::uint8_t u[sizeof(IntType)]; + std::make_unsigned_t v = 0; + + stream.read(reinterpret_cast(u), sizeof(IntType)); + for (std::size_t i = 0; i < sizeof(IntType); ++i) + v = (v << 8) | u[sizeof(IntType) - i - 1]; + + std::memcpy(&result, &v, sizeof(IntType)); + } + + return result; +} + + +// Utility to write an integer (signed or unsigned, any size) +// to a stream in little-endian order. We swap the byte order before the write if +// necessary to always write in little-endian order, independently of the byte +// ordering of the compiling machine. +template +inline void write_little_endian(std::ostream& stream, IntType value) { + + if (IsLittleEndian) + stream.write(reinterpret_cast(&value), sizeof(IntType)); + else + { + std::uint8_t u[sizeof(IntType)]; + std::make_unsigned_t v = value; + + std::size_t i = 0; + // if constexpr to silence the warning about shift by 8 + if constexpr (sizeof(IntType) > 1) + { + for (; i + 1 < sizeof(IntType); ++i) + { + u[i] = std::uint8_t(v); + v >>= 8; + } + } + u[i] = std::uint8_t(v); + + stream.write(reinterpret_cast(u), sizeof(IntType)); + } +} + + +// Read integers in bulk from a little-endian stream. +// This reads N integers from stream s and puts them in array out. +template +inline void read_little_endian(std::istream& stream, IntType* out, std::size_t count) { + if (IsLittleEndian) + stream.read(reinterpret_cast(out), sizeof(IntType) * count); + else + for (std::size_t i = 0; i < count; ++i) + out[i] = read_little_endian(stream); +} + + +// Write integers in bulk to a little-endian stream. +// This takes N integers from array values and writes them on stream s. +template +inline void write_little_endian(std::ostream& stream, const IntType* values, std::size_t count) { + if (IsLittleEndian) + stream.write(reinterpret_cast(values), sizeof(IntType) * count); + else + for (std::size_t i = 0; i < count; ++i) + write_little_endian(stream, values[i]); +} + + +// Read N signed integers from the stream s, putting them in the array out. +// The stream is assumed to be compressed using the signed LEB128 format. +// See https://en.wikipedia.org/wiki/LEB128 for a description of the compression scheme. +template +inline void read_leb_128(std::istream& stream, IntType* out, std::size_t count) { + + // Check the presence of our LEB128 magic string + char leb128MagicString[Leb128MagicStringSize]; + stream.read(leb128MagicString, Leb128MagicStringSize); + assert(strncmp(Leb128MagicString, leb128MagicString, Leb128MagicStringSize) == 0); + + static_assert(std::is_signed_v, "Not implemented for unsigned types"); + + const std::uint32_t BUF_SIZE = 4096; + std::uint8_t buf[BUF_SIZE]; + + auto bytes_left = read_little_endian(stream); + + std::uint32_t buf_pos = BUF_SIZE; + for (std::size_t i = 0; i < count; ++i) + { + IntType result = 0; + size_t shift = 0; + do + { + if (buf_pos == BUF_SIZE) + { + stream.read(reinterpret_cast(buf), std::min(bytes_left, BUF_SIZE)); + buf_pos = 0; + } + + std::uint8_t byte = buf[buf_pos++]; + --bytes_left; + result |= (byte & 0x7f) << shift; + shift += 7; + + if ((byte & 0x80) == 0) + { + out[i] = (sizeof(IntType) * 8 <= shift || (byte & 0x40) == 0) + ? result + : result | ~((1 << shift) - 1); + break; + } + } while (shift < sizeof(IntType) * 8); + } + + assert(bytes_left == 0); +} + + +// Write signed integers to a stream with LEB128 compression. +// This takes N integers from array values, compresses them with +// the LEB128 algorithm and writes the result on the stream s. +// See https://en.wikipedia.org/wiki/LEB128 for a description of the compression scheme. +template +inline void write_leb_128(std::ostream& stream, const IntType* values, std::size_t count) { + + // Write our LEB128 magic string + stream.write(Leb128MagicString, Leb128MagicStringSize); + + static_assert(std::is_signed_v, "Not implemented for unsigned types"); + + std::uint32_t byte_count = 0; + for (std::size_t i = 0; i < count; ++i) + { + IntType value = values[i]; + std::uint8_t byte; + do + { + byte = value & 0x7f; + value >>= 7; + ++byte_count; + } while ((byte & 0x40) == 0 ? value != 0 : value != -1); + } + + write_little_endian(stream, byte_count); + + const std::uint32_t BUF_SIZE = 4096; + std::uint8_t buf[BUF_SIZE]; + std::uint32_t buf_pos = 0; + + auto flush = [&]() { + if (buf_pos > 0) + { + stream.write(reinterpret_cast(buf), buf_pos); + buf_pos = 0; + } + }; + + auto write = [&](std::uint8_t byte) { + buf[buf_pos++] = byte; + if (buf_pos == BUF_SIZE) + flush(); + }; + + for (std::size_t i = 0; i < count; ++i) + { + IntType value = values[i]; + while (true) + { + std::uint8_t byte = value & 0x7f; + value >>= 7; + if ((byte & 0x40) == 0 ? value == 0 : value == -1) + { + write(byte); + break; + } + write(byte | 0x80); + } + } + + flush(); +} + +} // namespace Stockfish::Eval::NNUE + +#endif // #ifndef NNUE_COMMON_H_INCLUDED diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h new file mode 100644 index 0000000..37634cb --- /dev/null +++ b/src/nnue/nnue_feature_transformer.h @@ -0,0 +1,312 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// A class that converts the input features of the NNUE evaluation function + +#ifndef NNUE_FEATURE_TRANSFORMER_H_INCLUDED +#define NNUE_FEATURE_TRANSFORMER_H_INCLUDED + +#include +#include +#include +#include + +#include "../position.h" +#include "../types.h" +#include "nnue_accumulator.h" +#include "nnue_architecture.h" +#include "nnue_common.h" +#include "simd.h" + +namespace Stockfish::Eval::NNUE { + +// Returns the inverse of a permutation +template +constexpr std::array +invert_permutation(const std::array& order) { + std::array inverse{}; + for (std::size_t i = 0; i < order.size(); i++) + inverse[order[i]] = i; + return inverse; +} + +// Divide a byte region of size TotalSize to chunks of size +// BlockSize, and permute the blocks by a given order +template +void permute(T (&data)[N], const std::array& order) { + constexpr std::size_t TotalSize = N * sizeof(T); + + static_assert(TotalSize % (BlockSize * OrderSize) == 0, + "ChunkSize * OrderSize must perfectly divide TotalSize"); + + constexpr std::size_t ProcessChunkSize = BlockSize * OrderSize; + + std::array buffer{}; + + std::byte* const bytes = reinterpret_cast(data); + + for (std::size_t i = 0; i < TotalSize; i += ProcessChunkSize) + { + std::byte* const values = &bytes[i]; + + for (std::size_t j = 0; j < OrderSize; j++) + { + auto* const buffer_chunk = &buffer[j * BlockSize]; + auto* const value_chunk = &values[order[j] * BlockSize]; + + std::copy(value_chunk, value_chunk + BlockSize, buffer_chunk); + } + + std::copy(std::begin(buffer), std::end(buffer), values); + } +} + +// Input feature converter +template +class FeatureTransformer { + + // Number of output dimensions for one side + static constexpr IndexType HalfDimensions = TransformedFeatureDimensions; + + public: + // Output type + using OutputType = TransformedFeatureType; + + // Number of input/output dimensions + static constexpr IndexType InputDimensions = FeatureSet::Dimensions; + static constexpr IndexType OutputDimensions = HalfDimensions; + + // Size of forward propagation buffer + static constexpr std::size_t BufferSize = OutputDimensions * sizeof(OutputType); + + // Store the order by which 128-bit blocks of a 1024-bit data must + // be permuted so that calling packus on adjacent vectors of 16-bit + // integers loaded from the data results in the pre-permutation order + static constexpr auto PackusEpi16Order = []() -> std::array { +#if defined(USE_AVX512) + // _mm512_packus_epi16 after permutation: + // | 0 | 2 | 4 | 6 | // Vector 0 + // | 1 | 3 | 5 | 7 | // Vector 1 + // | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | // Packed Result + return {0, 2, 4, 6, 1, 3, 5, 7}; +#elif defined(USE_AVX2) + // _mm256_packus_epi16 after permutation: + // | 0 | 2 | | 4 | 6 | // Vector 0, 2 + // | 1 | 3 | | 5 | 7 | // Vector 1, 3 + // | 0 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | // Packed Result + return {0, 2, 1, 3, 4, 6, 5, 7}; +#else + return {0, 1, 2, 3, 4, 5, 6, 7}; +#endif + }(); + + static constexpr auto InversePackusEpi16Order = invert_permutation(PackusEpi16Order); + + // Hash value embedded in the evaluation file + static constexpr std::uint32_t get_hash_value() { + return FeatureSet::HashValue ^ (OutputDimensions * 2); + } + + void permute_weights() { + permute<16>(biases, PackusEpi16Order); + permute<16>(weights, PackusEpi16Order); + } + + void unpermute_weights() { + permute<16>(biases, InversePackusEpi16Order); + permute<16>(weights, InversePackusEpi16Order); + } + + inline void scale_weights(bool read) { + for (IndexType j = 0; j < InputDimensions; ++j) + { + WeightType* w = &weights[j * HalfDimensions]; + for (IndexType i = 0; i < HalfDimensions; ++i) + w[i] = read ? w[i] * 2 : w[i] / 2; + } + + for (IndexType i = 0; i < HalfDimensions; ++i) + biases[i] = read ? biases[i] * 2 : biases[i] / 2; + } + + // Read network parameters + bool read_parameters(std::istream& stream) { + + read_leb_128(stream, biases, HalfDimensions); + read_leb_128(stream, weights, HalfDimensions * InputDimensions); + read_leb_128(stream, psqtWeights, PSQTBuckets * InputDimensions); + + permute_weights(); + scale_weights(true); + return !stream.fail(); + } + + // Write network parameters + bool write_parameters(std::ostream& stream) { + + unpermute_weights(); + scale_weights(false); + + write_leb_128(stream, biases, HalfDimensions); + write_leb_128(stream, weights, HalfDimensions * InputDimensions); + write_leb_128(stream, psqtWeights, PSQTBuckets * InputDimensions); + + permute_weights(); + scale_weights(true); + return !stream.fail(); + } + + // Convert input features + std::int32_t transform(const Position& pos, + AccumulatorStack& accumulatorStack, + AccumulatorCaches::Cache* cache, + OutputType* output, + int bucket) const { + + using namespace SIMD; + + accumulatorStack.evaluate(pos, *this, *cache); + const auto& accumulatorState = accumulatorStack.latest(); + + const Color perspectives[2] = {pos.side_to_move(), ~pos.side_to_move()}; + const auto& psqtAccumulation = (accumulatorState.acc()).psqtAccumulation; + const auto psqt = + (psqtAccumulation[perspectives[0]][bucket] - psqtAccumulation[perspectives[1]][bucket]) + / 2; + + const auto& accumulation = (accumulatorState.acc()).accumulation; + + for (IndexType p = 0; p < 2; ++p) + { + const IndexType offset = (HalfDimensions / 2) * p; + +#if defined(VECTOR) + + constexpr IndexType OutputChunkSize = MaxChunkSize; + static_assert((HalfDimensions / 2) % OutputChunkSize == 0); + constexpr IndexType NumOutputChunks = HalfDimensions / 2 / OutputChunkSize; + + const vec_t Zero = vec_zero(); + const vec_t One = vec_set_16(127 * 2); + + const vec_t* in0 = reinterpret_cast(&(accumulation[perspectives[p]][0])); + const vec_t* in1 = + reinterpret_cast(&(accumulation[perspectives[p]][HalfDimensions / 2])); + vec_t* out = reinterpret_cast(output + offset); + + // Per the NNUE architecture, here we want to multiply pairs of + // clipped elements and divide the product by 128. To do this, + // we can naively perform min/max operation to clip each of the + // four int16 vectors, mullo pairs together, then pack them into + // one int8 vector. However, there exists a faster way. + + // The idea here is to use the implicit clipping from packus to + // save us two vec_max_16 instructions. This clipping works due + // to the fact that any int16 integer below zero will be zeroed + // on packus. + + // Consider the case where the second element is negative. + // If we do standard clipping, that element will be zero, which + // means our pairwise product is zero. If we perform packus and + // remove the lower-side clip for the second element, then our + // product before packus will be negative, and is zeroed on pack. + // The two operation produce equivalent results, but the second + // one (using packus) saves one max operation per pair. + + // But here we run into a problem: mullo does not preserve the + // sign of the multiplication. We can get around this by doing + // mulhi, which keeps the sign. But that requires an additional + // tweak. + + // mulhi cuts off the last 16 bits of the resulting product, + // which is the same as performing a rightward shift of 16 bits. + // We can use this to our advantage. Recall that we want to + // divide the final product by 128, which is equivalent to a + // 7-bit right shift. Intuitively, if we shift the clipped + // value left by 9, and perform mulhi, which shifts the product + // right by 16 bits, then we will net a right shift of 7 bits. + // However, this won't work as intended. Since we clip the + // values to have a maximum value of 127, shifting it by 9 bits + // might occupy the signed bit, resulting in some positive + // values being interpreted as negative after the shift. + + // There is a way, however, to get around this limitation. When + // loading the network, scale accumulator weights and biases by + // 2. To get the same pairwise multiplication result as before, + // we need to divide the product by 128 * 2 * 2 = 512, which + // amounts to a right shift of 9 bits. So now we only have to + // shift left by 7 bits, perform mulhi (shifts right by 16 bits) + // and net a 9 bit right shift. Since we scaled everything by + // two, the values are clipped at 127 * 2 = 254, which occupies + // 8 bits. Shifting it by 7 bits left will no longer occupy the + // signed bit, so we are safe. + + // Note that on NEON processors, we shift left by 6 instead + // because the instruction "vqdmulhq_s16" also doubles the + // return value after the multiplication, adding an extra shift + // to the left by 1, so we compensate by shifting less before + // the multiplication. + + constexpr int shift = + #if defined(USE_SSE2) + 7; + #else + 6; + #endif + + for (IndexType j = 0; j < NumOutputChunks; ++j) + { + const vec_t sum0a = + vec_slli_16(vec_max_16(vec_min_16(in0[j * 2 + 0], One), Zero), shift); + const vec_t sum0b = + vec_slli_16(vec_max_16(vec_min_16(in0[j * 2 + 1], One), Zero), shift); + const vec_t sum1a = vec_min_16(in1[j * 2 + 0], One); + const vec_t sum1b = vec_min_16(in1[j * 2 + 1], One); + + const vec_t pa = vec_mulhi_16(sum0a, sum1a); + const vec_t pb = vec_mulhi_16(sum0b, sum1b); + + out[j] = vec_packus_16(pa, pb); + } + +#else + + for (IndexType j = 0; j < HalfDimensions / 2; ++j) + { + BiasType sum0 = accumulation[static_cast(perspectives[p])][j + 0]; + BiasType sum1 = + accumulation[static_cast(perspectives[p])][j + HalfDimensions / 2]; + sum0 = std::clamp(sum0, 0, 127 * 2); + sum1 = std::clamp(sum1, 0, 127 * 2); + output[offset + j] = static_cast(unsigned(sum0 * sum1) / 512); + } + +#endif + } + + return psqt; + } // end of function transform() + + alignas(CacheLineSize) BiasType biases[HalfDimensions]; + alignas(CacheLineSize) WeightType weights[HalfDimensions * InputDimensions]; + alignas(CacheLineSize) PSQTWeightType psqtWeights[InputDimensions * PSQTBuckets]; +}; + +} // namespace Stockfish::Eval::NNUE + +#endif // #ifndef NNUE_FEATURE_TRANSFORMER_H_INCLUDED diff --git a/src/nnue/nnue_misc.cpp b/src/nnue/nnue_misc.cpp new file mode 100644 index 0000000..c998740 --- /dev/null +++ b/src/nnue/nnue_misc.cpp @@ -0,0 +1,193 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// Code for calculating NNUE evaluation function + +#include "nnue_misc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../position.h" +#include "../types.h" +#include "../uci.h" +#include "network.h" +#include "nnue_accumulator.h" + +namespace Stockfish::Eval::NNUE { + + +constexpr std::string_view PieceToChar(" PNBRQK pnbrqk"); + + +namespace { +// Converts a Value into (centi)pawns and writes it in a buffer. +// The buffer must have capacity for at least 5 chars. +void format_cp_compact(Value v, char* buffer, const Position& pos) { + + buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' '); + + int cp = std::abs(UCIEngine::to_cp(v, pos)); + if (cp >= 10000) + { + buffer[1] = '0' + cp / 10000; + cp %= 10000; + buffer[2] = '0' + cp / 1000; + cp %= 1000; + buffer[3] = '0' + cp / 100; + buffer[4] = ' '; + } + else if (cp >= 1000) + { + buffer[1] = '0' + cp / 1000; + cp %= 1000; + buffer[2] = '0' + cp / 100; + cp %= 100; + buffer[3] = '.'; + buffer[4] = '0' + cp / 10; + } + else + { + buffer[1] = '0' + cp / 100; + cp %= 100; + buffer[2] = '.'; + buffer[3] = '0' + cp / 10; + cp %= 10; + buffer[4] = '0' + cp / 1; + } +} + + +// Converts a Value into pawns, always keeping two decimals +void format_cp_aligned_dot(Value v, std::stringstream& stream, const Position& pos) { + + const double pawns = std::abs(0.01 * UCIEngine::to_cp(v, pos)); + + stream << (v < 0 ? '-' + : v > 0 ? '+' + : ' ') + << std::setiosflags(std::ios::fixed) << std::setw(6) << std::setprecision(2) << pawns; +} +} + + +// Returns a string with the value of each piece on a board, +// and a table for (PSQT, Layers) values bucket by bucket. +std::string +trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::AccumulatorCaches& caches) { + + std::stringstream ss; + + char board[3 * 8 + 1][8 * 8 + 2]; + std::memset(board, ' ', sizeof(board)); + for (int row = 0; row < 3 * 8 + 1; ++row) + board[row][8 * 8 + 1] = '\0'; + + // A lambda to output one box of the board + auto writeSquare = [&board, &pos](File file, Rank rank, Piece pc, Value value) { + const int x = int(file) * 8; + const int y = (7 - int(rank)) * 3; + for (int i = 1; i < 8; ++i) + board[y][x + i] = board[y + 3][x + i] = '-'; + for (int i = 1; i < 3; ++i) + board[y + i][x] = board[y + i][x + 8] = '|'; + board[y][x] = board[y][x + 8] = board[y + 3][x + 8] = board[y + 3][x] = '+'; + if (pc != NO_PIECE) + board[y + 1][x + 4] = PieceToChar[pc]; + if (is_valid(value)) + format_cp_compact(value, &board[y + 2][x + 2], pos); + }; + + AccumulatorStack accumulators; + + // We estimate the value of each piece by doing a differential evaluation from + // the current base eval, simulating the removal of the piece from its square. + auto [psqt, positional] = networks.big.evaluate(pos, accumulators, &caches.big); + Value base = psqt + positional; + base = pos.side_to_move() == WHITE ? base : -base; + + for (File f = FILE_A; f <= FILE_H; ++f) + for (Rank r = RANK_1; r <= RANK_8; ++r) + { + Square sq = make_square(f, r); + Piece pc = pos.piece_on(sq); + Value v = VALUE_NONE; + + if (pc != NO_PIECE && type_of(pc) != KING) + { + pos.remove_piece(sq); + + accumulators.reset(); + std::tie(psqt, positional) = networks.big.evaluate(pos, accumulators, &caches.big); + Value eval = psqt + positional; + eval = pos.side_to_move() == WHITE ? eval : -eval; + v = base - eval; + + pos.put_piece(pc, sq); + } + + writeSquare(f, r, pc, v); + } + + ss << " NNUE derived piece values:\n"; + for (int row = 0; row < 3 * 8 + 1; ++row) + ss << board[row] << '\n'; + ss << '\n'; + + accumulators.reset(); + auto t = networks.big.trace_evaluate(pos, accumulators, &caches.big); + + ss << " NNUE network contributions " + << (pos.side_to_move() == WHITE ? "(White to move)" : "(Black to move)") << std::endl + << "+------------+------------+------------+------------+\n" + << "| Bucket | Material | Positional | Total |\n" + << "| | (PSQT) | (Layers) | |\n" + << "+------------+------------+------------+------------+\n"; + + for (std::size_t bucket = 0; bucket < LayerStacks; ++bucket) + { + ss << "| " << bucket << " " // + << " | "; + format_cp_aligned_dot(t.psqt[bucket], ss, pos); + ss << " " // + << " | "; + format_cp_aligned_dot(t.positional[bucket], ss, pos); + ss << " " // + << " | "; + format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], ss, pos); + ss << " " // + << " |"; + if (bucket == t.correctBucket) + ss << " <-- this bucket is used"; + ss << '\n'; + } + + ss << "+------------+------------+------------+------------+\n"; + + return ss.str(); +} + + +} // namespace Stockfish::Eval::NNUE diff --git a/src/nnue/nnue_misc.h b/src/nnue/nnue_misc.h new file mode 100644 index 0000000..0221216 --- /dev/null +++ b/src/nnue/nnue_misc.h @@ -0,0 +1,61 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef NNUE_MISC_H_INCLUDED +#define NNUE_MISC_H_INCLUDED + +#include +#include + +#include "../types.h" +#include "nnue_architecture.h" + +namespace Stockfish { + +class Position; + +namespace Eval::NNUE { + +struct EvalFile { + // Default net name, will use one of the EvalFileDefaultName* macros defined + // in evaluate.h + std::string defaultName; + // Selected net name, either via uci option or default + std::string current; + // Net description extracted from the net file + std::string netDescription; +}; + + +struct NnueEvalTrace { + static_assert(LayerStacks == PSQTBuckets); + + Value psqt[LayerStacks]; + Value positional[LayerStacks]; + std::size_t correctBucket; +}; + +struct Networks; +struct AccumulatorCaches; + +std::string trace(Position& pos, const Networks& networks, AccumulatorCaches& caches); + +} // namespace Stockfish::Eval::NNUE +} // namespace Stockfish + +#endif // #ifndef NNUE_MISC_H_INCLUDED diff --git a/src/nnue/simd.h b/src/nnue/simd.h new file mode 100644 index 0000000..5602230 --- /dev/null +++ b/src/nnue/simd.h @@ -0,0 +1,418 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef NNUE_SIMD_H_INCLUDED +#define NNUE_SIMD_H_INCLUDED + +#if defined(USE_AVX2) + #include + +#elif defined(USE_SSE41) + #include + +#elif defined(USE_SSSE3) + #include + +#elif defined(USE_SSE2) + #include + +#elif defined(USE_NEON) + #include +#endif + +#include "../types.h" +#include "nnue_common.h" + +namespace Stockfish::Eval::NNUE::SIMD { + +// If vector instructions are enabled, we update and refresh the +// accumulator tile by tile such that each tile fits in the CPU's +// vector registers. +#define VECTOR + +#ifdef USE_AVX512 +using vec_t = __m512i; +using vec128_t = __m128i; +using psqt_vec_t = __m256i; +using vec_uint_t = __m512i; + #define vec_load(a) _mm512_load_si512(a) + #define vec_store(a, b) _mm512_store_si512(a, b) + #define vec_add_16(a, b) _mm512_add_epi16(a, b) + #define vec_sub_16(a, b) _mm512_sub_epi16(a, b) + #define vec_mulhi_16(a, b) _mm512_mulhi_epi16(a, b) + #define vec_zero() _mm512_setzero_epi32() + #define vec_set_16(a) _mm512_set1_epi16(a) + #define vec_max_16(a, b) _mm512_max_epi16(a, b) + #define vec_min_16(a, b) _mm512_min_epi16(a, b) + #define vec_slli_16(a, b) _mm512_slli_epi16(a, b) + // Inverse permuted at load time + #define vec_packus_16(a, b) _mm512_packus_epi16(a, b) + #define vec_load_psqt(a) _mm256_load_si256(a) + #define vec_store_psqt(a, b) _mm256_store_si256(a, b) + #define vec_add_psqt_32(a, b) _mm256_add_epi32(a, b) + #define vec_sub_psqt_32(a, b) _mm256_sub_epi32(a, b) + #define vec_zero_psqt() _mm256_setzero_si256() + + #ifdef USE_SSSE3 + #define vec_nnz(a) _mm512_cmpgt_epi32_mask(a, _mm512_setzero_si512()) + #endif + + #define vec128_zero _mm_setzero_si128() + #define vec128_set_16(a) _mm_set1_epi16(a) + #if (USE_SSE41) + #define vec128_load(a) _mm_cvtepu8_epi16(_mm_loadl_epi64(a)) + #else + #define vec128_load(a) _mm_load_si128(a) + #endif + #define vec128_storeu(a, b) _mm_storeu_si128(a, b) + #define vec128_add(a, b) _mm_add_epi16(a, b) + #define NumRegistersSIMD 16 + #define MaxChunkSize 64 + +#elif USE_AVX2 +using vec_t = __m256i; +using vec128_t = __m128i; +using psqt_vec_t = __m256i; +using vec_uint_t = __m256i; + #define vec_load(a) _mm256_load_si256(a) + #define vec_store(a, b) _mm256_store_si256(a, b) + #define vec_add_16(a, b) _mm256_add_epi16(a, b) + #define vec_sub_16(a, b) _mm256_sub_epi16(a, b) + #define vec_mulhi_16(a, b) _mm256_mulhi_epi16(a, b) + #define vec_zero() _mm256_setzero_si256() + #define vec_set_16(a) _mm256_set1_epi16(a) + #define vec_max_16(a, b) _mm256_max_epi16(a, b) + #define vec_min_16(a, b) _mm256_min_epi16(a, b) + #define vec_slli_16(a, b) _mm256_slli_epi16(a, b) + // Inverse permuted at load time + #define vec_packus_16(a, b) _mm256_packus_epi16(a, b) + #define vec_load_psqt(a) _mm256_load_si256(a) + #define vec_store_psqt(a, b) _mm256_store_si256(a, b) + #define vec_add_psqt_32(a, b) _mm256_add_epi32(a, b) + #define vec_sub_psqt_32(a, b) _mm256_sub_epi32(a, b) + #define vec_zero_psqt() _mm256_setzero_si256() + + #ifdef USE_SSSE3 + #if defined(USE_VNNI) && !defined(USE_AVXVNNI) + #define vec_nnz(a) _mm256_cmpgt_epi32_mask(a, _mm256_setzero_si256()) + #else + #define vec_nnz(a) \ + _mm256_movemask_ps( \ + _mm256_castsi256_ps(_mm256_cmpgt_epi32(a, _mm256_setzero_si256()))) + #endif + #endif + + #define vec128_zero _mm_setzero_si128() + #define vec128_set_16(a) _mm_set1_epi16(a) + #if (USE_SSE41) + #define vec128_load(a) _mm_cvtepu8_epi16(_mm_loadl_epi64(a)) + #else + #define vec128_load(a) _mm_load_si128(a) + #endif + #define vec128_storeu(a, b) _mm_storeu_si128(a, b) + #define vec128_add(a, b) _mm_add_epi16(a, b) + + #define NumRegistersSIMD 16 + #define MaxChunkSize 32 + +#elif USE_SSE2 +using vec_t = __m128i; +using vec128_t = __m128i; +using psqt_vec_t = __m128i; +using vec_uint_t = __m128i; + #define vec_load(a) (*(a)) + #define vec_store(a, b) *(a) = (b) + #define vec_add_16(a, b) _mm_add_epi16(a, b) + #define vec_sub_16(a, b) _mm_sub_epi16(a, b) + #define vec_mulhi_16(a, b) _mm_mulhi_epi16(a, b) + #define vec_zero() _mm_setzero_si128() + #define vec_set_16(a) _mm_set1_epi16(a) + #define vec_max_16(a, b) _mm_max_epi16(a, b) + #define vec_min_16(a, b) _mm_min_epi16(a, b) + #define vec_slli_16(a, b) _mm_slli_epi16(a, b) + #define vec_packus_16(a, b) _mm_packus_epi16(a, b) + #define vec_load_psqt(a) (*(a)) + #define vec_store_psqt(a, b) *(a) = (b) + #define vec_add_psqt_32(a, b) _mm_add_epi32(a, b) + #define vec_sub_psqt_32(a, b) _mm_sub_epi32(a, b) + #define vec_zero_psqt() _mm_setzero_si128() + + #ifdef USE_SSSE3 + #define vec_nnz(a) \ + _mm_movemask_ps(_mm_castsi128_ps(_mm_cmpgt_epi32(a, _mm_setzero_si128()))) + #endif + + #define vec128_zero _mm_setzero_si128() + #define vec128_set_16(a) _mm_set1_epi16(a) + #if (USE_SSE41) + #define vec128_load(a) _mm_cvtepu8_epi16(_mm_loadl_epi64(a)) + #else + #define vec128_load(a) _mm_load_si128(a) + #endif + #define vec128_storeu(a, b) _mm_storeu_si128(a, b) + #define vec128_add(a, b) _mm_add_epi16(a, b) + + #define NumRegistersSIMD (Is64Bit ? 16 : 8) + #define MaxChunkSize 16 + +#elif USE_NEON +using vec_t = int16x8_t; +using psqt_vec_t = int32x4_t; +using vec128_t = uint16x8_t; +using vec_uint_t = uint32x4_t; + #define vec_load(a) (*(a)) + #define vec_store(a, b) *(a) = (b) + #define vec_add_16(a, b) vaddq_s16(a, b) + #define vec_sub_16(a, b) vsubq_s16(a, b) + #define vec_mulhi_16(a, b) vqdmulhq_s16(a, b) + #define vec_zero() vec_t{0} + #define vec_set_16(a) vdupq_n_s16(a) + #define vec_max_16(a, b) vmaxq_s16(a, b) + #define vec_min_16(a, b) vminq_s16(a, b) + #define vec_slli_16(a, b) vshlq_s16(a, vec_set_16(b)) + #define vec_packus_16(a, b) reinterpret_cast(vcombine_u8(vqmovun_s16(a), vqmovun_s16(b))) + #define vec_load_psqt(a) (*(a)) + #define vec_store_psqt(a, b) *(a) = (b) + #define vec_add_psqt_32(a, b) vaddq_s32(a, b) + #define vec_sub_psqt_32(a, b) vsubq_s32(a, b) + #define vec_zero_psqt() psqt_vec_t{0} + +static constexpr std::uint32_t Mask[4] = {1, 2, 4, 8}; + #define vec_nnz(a) vaddvq_u32(vandq_u32(vtstq_u32(a, a), vld1q_u32(Mask))) + #define vec128_zero vdupq_n_u16(0) + #define vec128_set_16(a) vdupq_n_u16(a) + #define vec128_load(a) vld1q_u16(reinterpret_cast(a)) + #define vec128_storeu(a, b) vst1q_u16(reinterpret_cast(a), b) + #define vec128_add(a, b) vaddq_u16(a, b) + + #define NumRegistersSIMD 16 + #define MaxChunkSize 16 + +#else + #undef VECTOR + +#endif + +struct Vec16Wrapper { +#ifdef VECTOR + using type = vec_t; + static type add(const type& lhs, const type& rhs) { return vec_add_16(lhs, rhs); } + static type sub(const type& lhs, const type& rhs) { return vec_sub_16(lhs, rhs); } +#else + using type = BiasType; + static type add(const type& lhs, const type& rhs) { return lhs + rhs; } + static type sub(const type& lhs, const type& rhs) { return lhs - rhs; } +#endif +}; + +struct Vec32Wrapper { +#ifdef VECTOR + using type = psqt_vec_t; + static type add(const type& lhs, const type& rhs) { return vec_add_psqt_32(lhs, rhs); } + static type sub(const type& lhs, const type& rhs) { return vec_sub_psqt_32(lhs, rhs); } +#else + using type = PSQTWeightType; + static type add(const type& lhs, const type& rhs) { return lhs + rhs; } + static type sub(const type& lhs, const type& rhs) { return lhs - rhs; } +#endif +}; + +enum UpdateOperation { + Add, + Sub +}; + +template = true> +typename VecWrapper::type fused(const typename VecWrapper::type& in) { + return in; +} + +template, bool> = true, + std::enable_if_t = true> +typename VecWrapper::type +fused(const typename VecWrapper::type& in, const T& operand, const Ts&... operands) { + switch (update_op) + { + case Add : + return fused(VecWrapper::add(in, operand), operands...); + case Sub : + return fused(VecWrapper::sub(in, operand), operands...); + default : + static_assert(update_op == Add || update_op == Sub, + "Only Add and Sub are currently supported."); + return typename VecWrapper::type(); + } +} + +#if defined(USE_AVX512) + +[[maybe_unused]] static int m512_hadd(__m512i sum, int bias) { + return _mm512_reduce_add_epi32(sum) + bias; +} + +[[maybe_unused]] static void m512_add_dpbusd_epi32(__m512i& acc, __m512i a, __m512i b) { + + #if defined(USE_VNNI) + acc = _mm512_dpbusd_epi32(acc, a, b); + #else + __m512i product0 = _mm512_maddubs_epi16(a, b); + product0 = _mm512_madd_epi16(product0, _mm512_set1_epi16(1)); + acc = _mm512_add_epi32(acc, product0); + #endif +} + +#endif + +#if defined(USE_AVX2) + +[[maybe_unused]] static int m256_hadd(__m256i sum, int bias) { + __m128i sum128 = _mm_add_epi32(_mm256_castsi256_si128(sum), _mm256_extracti128_si256(sum, 1)); + sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_BADC)); + sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_CDAB)); + return _mm_cvtsi128_si32(sum128) + bias; +} + +[[maybe_unused]] static void m256_add_dpbusd_epi32(__m256i& acc, __m256i a, __m256i b) { + + #if defined(USE_VNNI) + acc = _mm256_dpbusd_epi32(acc, a, b); + #else + __m256i product0 = _mm256_maddubs_epi16(a, b); + product0 = _mm256_madd_epi16(product0, _mm256_set1_epi16(1)); + acc = _mm256_add_epi32(acc, product0); + #endif +} + +#endif + +#if defined(USE_SSSE3) + +[[maybe_unused]] static int m128_hadd(__m128i sum, int bias) { + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0x4E)); //_MM_PERM_BADC + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, 0xB1)); //_MM_PERM_CDAB + return _mm_cvtsi128_si32(sum) + bias; +} + +[[maybe_unused]] static void m128_add_dpbusd_epi32(__m128i& acc, __m128i a, __m128i b) { + + __m128i product0 = _mm_maddubs_epi16(a, b); + product0 = _mm_madd_epi16(product0, _mm_set1_epi16(1)); + acc = _mm_add_epi32(acc, product0); +} + +#endif + +#if defined(USE_NEON_DOTPROD) + +[[maybe_unused]] static void +dotprod_m128_add_dpbusd_epi32(int32x4_t& acc, int8x16_t a, int8x16_t b) { + + acc = vdotq_s32(acc, a, b); +} +#endif + +#if defined(USE_NEON) + +[[maybe_unused]] static int neon_m128_reduce_add_epi32(int32x4_t s) { + #if USE_NEON >= 8 + return vaddvq_s32(s); + #else + return s[0] + s[1] + s[2] + s[3]; + #endif +} + +[[maybe_unused]] static int neon_m128_hadd(int32x4_t sum, int bias) { + return neon_m128_reduce_add_epi32(sum) + bias; +} + +#endif + +#if USE_NEON >= 8 +[[maybe_unused]] static void neon_m128_add_dpbusd_epi32(int32x4_t& acc, int8x16_t a, int8x16_t b) { + + int16x8_t product0 = vmull_s8(vget_low_s8(a), vget_low_s8(b)); + int16x8_t product1 = vmull_high_s8(a, b); + int16x8_t sum = vpaddq_s16(product0, product1); + acc = vpadalq_s16(acc, sum); +} +#endif + + +// Compute optimal SIMD register count for feature transformer accumulation. +template +class SIMDTiling { +#ifdef VECTOR + // We use __m* types as template arguments, which causes GCC to emit warnings + // about losing some attribute information. This is irrelevant to us as we + // only take their size, so the following pragma are harmless. + #if defined(__GNUC__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wignored-attributes" + #endif + + template + static constexpr int BestRegisterCount() { + constexpr std::size_t RegisterSize = sizeof(SIMDRegisterType); + constexpr std::size_t LaneSize = sizeof(LaneType); + + static_assert(RegisterSize >= LaneSize); + static_assert(MaxRegisters <= NumRegistersSIMD); + static_assert(MaxRegisters > 0); + static_assert(NumRegistersSIMD > 0); + static_assert(RegisterSize % LaneSize == 0); + static_assert((NumLanes * LaneSize) % RegisterSize == 0); + + const int ideal = (NumLanes * LaneSize) / RegisterSize; + if (ideal <= MaxRegisters) + return ideal; + + // Look for the largest divisor of the ideal register count that is smaller than MaxRegisters + for (int divisor = MaxRegisters; divisor > 1; --divisor) + if (ideal % divisor == 0) + return divisor; + + return 1; + } + + #if defined(__GNUC__) + #pragma GCC diagnostic pop + #endif + + public: + static constexpr int NumRegs = + BestRegisterCount(); + static constexpr int NumPsqtRegs = + BestRegisterCount(); + + static constexpr IndexType TileHeight = NumRegs * sizeof(vec_t) / 2; + static constexpr IndexType PsqtTileHeight = NumPsqtRegs * sizeof(psqt_vec_t) / 4; + + static_assert(HalfDimensions % TileHeight == 0, "TileHeight must divide HalfDimensions"); + static_assert(PSQTBuckets % PsqtTileHeight == 0, "PsqtTileHeight must divide PSQTBuckets"); +#endif +}; +} + +#endif diff --git a/src/position.h b/src/position.h new file mode 100644 index 0000000..3c1b340 --- /dev/null +++ b/src/position.h @@ -0,0 +1,239 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef POSITION_H_INCLUDED +#define POSITION_H_INCLUDED +#include "chess.hpp" +#include "types.h" +#include "nnue/nnue_accumulator.h" +namespace Stockfish { +class Position { + public: + Position(const chess::Board& board) { + b = board; + npm[1] = piece_value(chess::PieceType::KNIGHT) + * b.pieces(chess::PieceType::KNIGHT, chess::Color::BLACK).count() + + piece_value(chess::PieceType::BISHOP) + * b.pieces(chess::PieceType::BISHOP, chess::Color::BLACK).count() + + piece_value(chess::PieceType::ROOK) + * b.pieces(chess::PieceType::ROOK, chess::Color::BLACK).count() + + piece_value(chess::PieceType::QUEEN) + * b.pieces(chess::PieceType::QUEEN, chess::Color::BLACK).count(); + npm[0] = piece_value(chess::PieceType::KNIGHT) + * b.pieces(chess::PieceType::KNIGHT, chess::Color::WHITE).count() + + piece_value(chess::PieceType::BISHOP) + * b.pieces(chess::PieceType::BISHOP, chess::Color::WHITE).count() + + piece_value(chess::PieceType::ROOK) + * b.pieces(chess::PieceType::ROOK, chess::Color::WHITE).count() + + piece_value(chess::PieceType::QUEEN) + * b.pieces(chess::PieceType::QUEEN, chess::Color::WHITE).count(); + } + inline Stockfish::Bitboard pieces(Stockfish::Color c) const { return b.us(c).getBits(); } + inline Stockfish::Bitboard pieces(Stockfish::Color c, Stockfish::PieceType pt) const { + switch (pt) + { + case Stockfish::ALL_PIECES : + return pieces(c); + default : + return b.pieces(chess::PieceType(static_cast(pt - 1)), c) + .getBits(); + } + } + inline Stockfish::Bitboard pieces(Stockfish::PieceType pt) const { + switch (pt) + { + case Stockfish::ALL_PIECES : + return b.occ().getBits(); + default : + return b.pieces(chess::PieceType(static_cast(pt - 1))) + .getBits(); + } + } + inline Stockfish::Bitboard pieces() const { return b.occ().getBits(); } + template + inline int count() const { + switch (Pt) + { + case Stockfish::ALL_PIECES : + return b.occ().count(); + default : + return b.pieces(chess::PieceType(static_cast(Pt - 1))) + .count(); + } + } + template + inline int count(Stockfish::Color c) const { + switch (Pt) + { + case Stockfish::ALL_PIECES : + return b.occ().count(); + default : + return b.pieces(chess::PieceType(static_cast(Pt - 1)), c) + .count(); + } + } + template + inline Stockfish::Square square(Stockfish::Color c) const { + assert(count(c) == 1); + switch (Pt) + { + case Stockfish::ALL_PIECES : + return static_cast(b.occ().lsb()); + default : + return static_cast( + b.pieces(chess::PieceType(static_cast(Pt - 1)), c) + .lsb()); + } + } + inline Stockfish::Color side_to_move() const { + return (Stockfish::Color) static_cast(b.sideToMove()); + } + inline Stockfish::Piece piece_on(const Stockfish::Square sq) const { + return to_engine_piece(b.at(sq).internal()); + } + inline Stockfish::Value non_pawn_material() const { return npm[0] + npm[1]; } + inline Stockfish::Value non_pawn_material(Stockfish::Color c) const { return npm[(int) c]; } + inline int rule50_count() const { return b.halfMoveClock(); } + void do_move(chess::Move m) { + Stockfish::DirtyPiece dp; + dp.pc = to_engine_piece(b.at(m.from()).internal()); + dp.from = (Stockfish::Square) m.from().index(); + dp.to = (Stockfish::Square) m.to().index(); + dp.add_sq = Stockfish::SQ_NONE; + Stockfish::Color us = side_to_move(); + Stockfish::Color them = ~us; + Stockfish::Piece captured = m.typeOf() == m.ENPASSANT + ? Stockfish::make_piece(them, Stockfish::PAWN) + : piece_on(dp.to); + if (m.typeOf() == m.CASTLING) + { + const bool king_side = m.to() > m.from(); + const auto rookTo = chess::Square::castling_rook_square(king_side, side_to_move()); + const auto kingTo = chess::Square::castling_king_square(king_side, side_to_move()); + dp.to = (Stockfish::Square) kingTo.index(); + dp.remove_pc = dp.add_pc = make_piece(us, Stockfish::ROOK); + dp.remove_sq = (Stockfish::Square) m.to().index(); + dp.add_sq = (Stockfish::Square) rookTo.index(); + captured = Stockfish::NO_PIECE; + } + else if (captured) + { + Stockfish::Square capsq = + m.typeOf() == m.ENPASSANT + ? Stockfish::Square(m.to().index() - (us == Stockfish::WHITE ? 8 : -8)) + : dp.to; + dp.remove_pc = captured; + dp.remove_sq = capsq; + } + else + dp.remove_sq = Stockfish::SQ_NONE; + if (m.typeOf() == m.PROMOTION) + { + dp.add_pc = + make_piece(us, + static_cast( + 1 + m.promotionType())); // promotionType() returns a PieceType, + // which is 0 for NO_PIECE_TYPE, so we + // add 1 to get the Piece + // bonus: int + (OOP type) do the promotion of type integers because int can represent all the values in chess::PieceType (int8_t(m.to())); + + b.makeMove(m); // finally no + stack.push(dp); + //return dp; + } + void undo_move(chess::Move m) { + b.unmakeMove(m); + Stockfish::Color us = side_to_move(); + Stockfish::Color them = ~side_to_move(); + if (m.typeOf() == m.PROMOTION) + { + npm[(int) us] -= piece_value(m.promotionType()); + } + // en passant and PxP are handled in piece_value + if (b.isCapture(m)) + npm[(int) them] += piece_value(b.at(m.to())); + stack.pop(); + } + void do_null_move() { b.makeNullMove(); } + void undo_null_move() { b.unmakeNullMove(); } + chess::Board b; // exposure to generate legal moves + Stockfish::Eval::NNUE::AccumulatorStack stack; + + private: + Stockfish::Value npm[2] = {0, 0}; // non-pawn material for WHITE and BLACK + inline Stockfish::Value piece_value(chess::PieceType pt) { // fast and static check + switch (pt) + { + case (int) chess::PieceType::KNIGHT : + return Stockfish::KnightValue; + case (int) chess::PieceType::BISHOP : + return Stockfish::BishopValue; + case (int) chess::PieceType::ROOK : + return Stockfish::RookValue; + case (int) chess::PieceType::QUEEN : + return Stockfish::QueenValue; + default : + return 0; // includes king + } + } + inline Stockfish::Piece to_engine_piece(chess::Piece::underlying u) const { + switch (u) + { + case chess::Piece::underlying::WHITEPAWN : + return Stockfish::W_PAWN; + case chess::Piece::underlying::WHITEKNIGHT : + return Stockfish::W_KNIGHT; + case chess::Piece::underlying::WHITEBISHOP : + return Stockfish::W_BISHOP; + case chess::Piece::underlying::WHITEROOK : + return Stockfish::W_ROOK; + case chess::Piece::underlying::WHITEQUEEN : + return Stockfish::W_QUEEN; + case chess::Piece::underlying::WHITEKING : + return Stockfish::W_KING; + case chess::Piece::underlying::BLACKPAWN : + return Stockfish::B_PAWN; + case chess::Piece::underlying::BLACKKNIGHT : + return Stockfish::B_KNIGHT; + case chess::Piece::underlying::BLACKBISHOP : + return Stockfish::B_BISHOP; + case chess::Piece::underlying::BLACKROOK : + return Stockfish::B_ROOK; + case chess::Piece::underlying::BLACKQUEEN : + return Stockfish::B_QUEEN; + case chess::Piece::underlying::BLACKKING : + return Stockfish::B_KING; + default : + return Stockfish::NO_PIECE; + } + } +}; +} +#endif diff --git a/src/search.cpp b/src/search.cpp new file mode 100644 index 0000000..c959007 --- /dev/null +++ b/src/search.cpp @@ -0,0 +1,254 @@ +#include "search.h" +#include "evaluate.h" +#include +#include +#include +#include +#include + +namespace search { +using namespace Stockfish; // maybe.... +using Move = uint16_t; + +std::unique_ptr nn; +std::unique_ptr cache; +TranspositionTable tt; +std::chrono::time_point start; +std::atomic stop_requested; +int seldepth = 0; + +struct SearchStackEntry { + std::unique_ptr pv; + Value eval = Stockfish::VALUE_NONE; + + SearchStackEntry() : + pv(std::make_unique(Stockfish::MAX_PLY)) {} +}; + +inline Value value_draw(size_t nodes) { return VALUE_DRAW - 1 + Value(nodes & 0x2); } +inline Value value_to_tt(Value v, int ply) { + return is_win(v) ? v + ply : is_loss(v) ? v - ply : v; +} + +static Value value_from_tt(Value v, int ply, int r50c) { + if (is_win(v)) + { + if (v >= VALUE_MATE_IN_MAX_PLY && VALUE_MATE - v > 100 - r50c) + return VALUE_TB_WIN_IN_MAX_PLY - 1; + if (VALUE_TB - v > 100 - r50c) + return VALUE_TB_WIN_IN_MAX_PLY - 1; + return v - ply; + } + if (is_loss(v)) + { + if (v <= VALUE_MATED_IN_MAX_PLY && VALUE_MATE + v > 100 - r50c) + return VALUE_TB_LOSS_IN_MAX_PLY + 1; + if (VALUE_TB + v > 100 - r50c) + return VALUE_TB_LOSS_IN_MAX_PLY + 1; + return v + ply; + } + return v; +} + +static void update_pv(Move* pv, Move move, const Move* childPv) { + *pv++ = move; + while (childPv && *childPv) + *pv++ = *childPv++; + *pv = 0; +} +static Value qsearch(Position& pos, Value alpha, Value beta, int ply, SearchStackEntry* ss) { + seldepth = std::max(seldepth, ply+1); + + Value stand_pat = -VALUE_INFINITE; + if (!pos.b.inCheck()) + { + stand_pat = Stockfish::Eval::evaluate(*nn, pos, pos.stack, *cache, 0); + ss->eval = stand_pat; + if (stand_pat >= beta) + return stand_pat; + if (stand_pat > alpha) + alpha = stand_pat; + } + + chess::Movelist list, list2; + chess::movegen::legalmoves(list, pos.b); + if (list.size() == 0) + return pos.b.inCheck() ? mated_in(ply + 1) : value_draw(nodes.load()); + for (int i = 0; i < list.size(); ++i) + { + chess::Move mv = list[i]; + if (pos.b.isCapture(mv) || pos.b.givesCheck(mv) != chess::CheckType::NO_CHECK + || mv.typeOf() == mv.PROMOTION) + list2.add(mv); + } + movepick::qOrderMoves(pos.b, list2); + + for (int i = 0; i < list2.size(); ++i) + { + chess::Move mv = list2[i]; + pos.do_move(mv); + ++nodes; + Value score = -qsearch(pos, -beta, -alpha, ply + 1, ss + 1); + pos.undo_move(mv); + + if (score >= beta) + return score; + if (score > alpha) + alpha = score; + } + + return alpha; +} + +static Value +negamax(Position& pos, int depth, Value alpha, Value beta, int ply, SearchStackEntry* ss) { + seldepth = std::max(seldepth, ply+1); + if (stop_requested) return VALUE_NONE; + if (pos.b.isRepetition(1) || pos.b.halfMoveClock() >= 99) + return VALUE_DRAW; + + uint64_t key = pos.b.hash(); + ss->pv[0] = 0; + + TTEntry* entry = tt.lookup(key); + if (entry && entry->depth >= depth) + { + Value tt_score = value_from_tt(entry->score, ply, pos.b.halfMoveClock()); + if ((entry->flag == TTFlag::EXACT) + || (entry->flag == TTFlag::LOWERBOUND && tt_score >= beta) + || (entry->flag == TTFlag::UPPERBOUND && tt_score <= alpha)) + return tt_score; + } + + if (depth <= 0) + return qsearch(pos, alpha, beta, ply, ss); + + chess::Movelist list; + chess::movegen::legalmoves(list, pos.b); + if (list.empty()) + return pos.b.inCheck() ? mated_in(ply + 1) : value_draw(nodes.load()); + + chess::Move bestMove; + Value best = -VALUE_INFINITE, orig_alpha = alpha; + + movepick::orderMoves(pos.b, list, entry ? entry->move : chess::Move::NULL_MOVE, ply); + + for (int i = 0; i < list.size(); ++i) + { + chess::Move mv = list[i]; + if (ply == 0){ + auto end = std::chrono::steady_clock::now(); + auto s = std::chrono::duration_cast(end - start).count(); + if (s>=1) + std::cout << "info currmove " << chess::uci::moveToUci(mv) << " currmovenum " << i + 1 + << std::endl; + } + pos.do_move(mv); + ++nodes; + Value score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, ss + 1); + pos.undo_move(mv); + if (stop_requested) break; + if (score > best) + { + best = score; + bestMove = mv; + update_pv(ss->pv.get(), mv.move(), (ss + 1)->pv.get()); + } + + if (score > alpha) + alpha = score; + if (score >= beta) + { + if (!pos.b.isCapture(mv) && pos.b.givesCheck(mv) == chess::CheckType::NO_CHECK + && mv.typeOf() != mv.PROMOTION) + { + movepick::updateHistoryHeuristic(mv, depth); + movepick::updateKillerMoves(mv, ply); + } + break; + } + } + if (!stop_requested){ + TTFlag flag = (best >= beta) ? TTFlag::LOWERBOUND + : (best <= orig_alpha) ? TTFlag::UPPERBOUND + : TTFlag::EXACT; + + tt.store(key, bestMove, value_to_tt(best, ply), depth, flag); + } + ss->eval = best; + return best; +} +void run_search(const chess::Board& board, const TimeControl& tc) { + std::vector ss(MAX_PLY); + int rundepth = tc.depth ? tc.depth : 5; + Position pos(board); + std::vector pv(MAX_PLY); + seldepth = 0; + start = std::chrono::steady_clock::now(); + for (int d = 1; d <= rundepth && !stop_requested; ++d) + { + cache->clear(*nn); + for (auto& s : ss) + { + std::fill(s.pv.get(), s.pv.get() + MAX_PLY, 0); + s.eval = VALUE_NONE; + } + + pos.stack.reset(); + Value v = negamax(pos, d, -VALUE_INFINITE, VALUE_INFINITE, 0, ss.data()); + auto end = std::chrono::steady_clock::now(); + auto nanos = std::chrono::duration_cast(end - start).count(); + + std::cout << "info depth " << d << " seldepth " << seldepth << " score "; + if (is_decisive(v)) + { + int m = (v > 0 ? VALUE_MATE - v : -VALUE_MATE - v); + std::cout << "mate " << (v > 0 ? (m + 1) / 2 : -(m + 1) / 2); + } + else + { + std::cout << "cp " << v; + } + std::cout << " nodes " << nodes << " nps " << (nodes * 1000000000 / nanos); + std::cout << " time " << nanos / 1000000 << " hashfull " << tt.hashfull() << " pv "; + for (int j = 0; j < MAX_PLY && ss[0].pv[j]; ++j) + std::cout << chess::uci::moveToUci(chess::Move(ss[0].pv[j])) << " "; + std::cout << std::endl; + if (!stop_requested) { + for (int i = 0; i < MAX_PLY; ++i) { + pv[i] = ss[0].pv[i]; + } + } + + } + + if (pv[0]) + std::cout << "bestmove " << chess::uci::moveToUci(chess::Move(pv[0])) << std::endl; +} + +// ---------------------- Initialization --------------------------- +template +void init() { + if constexpr (init_nn) + { + Stockfish::Eval::NNUE::NetworkBig nBig( + Stockfish::Eval::NNUE::EvalFile{EvalFileDefaultNameBig, "", EvalFileDefaultNameBig}, + Stockfish::Eval::NNUE::EmbeddedNNUEType::BIG); + Stockfish::Eval::NNUE::NetworkSmall nSmall( + Stockfish::Eval::NNUE::EvalFile{EvalFileDefaultNameSmall, "", EvalFileDefaultNameSmall}, + Stockfish::Eval::NNUE::EmbeddedNNUEType::SMALL); + nn = std::make_unique(std::move(nBig), std::move(nSmall)); + } + else + { + nn->big.verify(EvalFileDefaultNameBig, onVerify); + nn->small.verify(EvalFileDefaultNameSmall, onVerify); + cache = std::make_unique(*nn); + } +} + +// Explicit instantiations for the linker +template void init(); +template void init(); + +} // namespace search diff --git a/src/search.h b/src/search.h new file mode 100644 index 0000000..6c09ac2 --- /dev/null +++ b/src/search.h @@ -0,0 +1,24 @@ +#ifndef CHESS_ENGINE_SEARCH_H +#define CHESS_ENGINE_SEARCH_H +#include "evaluate.h" +#include "movepick.hpp" +#include "position.h" +#include "timeman.hpp" +#include "tt.hpp" +#include "types.h" +#include "nnue/network.h" +#include +namespace search { +inline std::atomic nodes{0}; +struct SearchParams { + TimeControl tc; +}; +template +void init(); // Called twice (one for UCIOptions NNUE paths and one for anything else +void run_search(const chess::Board& board, const TimeControl& tc); +extern std::atomic stop_requested; +extern TranspositionTable tt; +extern std::unique_ptr nn; // defer construction +inline void onVerify(std::string_view sv) { std::cout << sv << std::endl; } +} // namespace search +#endif // CHESS_ENGINE_SEARCH_H diff --git a/src/timeman.cpp b/src/timeman.cpp new file mode 100644 index 0000000..e4c76b4 --- /dev/null +++ b/src/timeman.cpp @@ -0,0 +1,107 @@ +#include "timeman.hpp" +#include +#include +#include +#include +#include + +namespace timeman { +using namespace std::chrono; + +// Global state +TimeControl current_tc; // store the whole TimeControl struct globally +time_point start_time; +milliseconds inc = milliseconds(0); +milliseconds time_limit = milliseconds(0); +int moves_to_go = 40; +int depth = 0; +std::array times{}; +bool infinite = false; + +constexpr int MIN_SAFE_BUFFER_MS = 100; +constexpr double BUFFER_PERCENTAGE = 0.05; +constexpr int MIN_PER_MOVE = 400; +constexpr int MAX_PER_MOVE = 3000; +constexpr double PHASE_SCALE_OPENING = 0.7; +constexpr double PHASE_SCALE_MIDDLE = 1.0; +constexpr double PHASE_SCALE_ENDGAME = 1.25; +constexpr int OPENING_DEPTH = 5; +constexpr int MIDDLE_DEPTH = 10; + +static double get_phase_scale(int current_depth) { + if (current_depth <= OPENING_DEPTH) + return PHASE_SCALE_OPENING; + else if (current_depth <= MIDDLE_DEPTH) + return PHASE_SCALE_MIDDLE; + else + return PHASE_SCALE_ENDGAME; +} + +inline void reset_start_time() { start_time = high_resolution_clock::now(); } +void setLimits(const TimeControl& tc) { + current_tc = tc; + infinite = tc.infinite; + moves_to_go = std::max(1, tc.movestogo); + depth = tc.depth; + + if (infinite && tc.depth > 0) + { + time_limit = milliseconds::max(); + inc = milliseconds(0); + std::cout << "[TimeMan] Infinite or fixed depth mode.\n"; + } + else if (tc.movetime >= 0 && tc.movetime < INFINITE_TIME) + { + // UCI: movetime overrides all other time settings + time_limit = milliseconds(tc.movetime); + inc = milliseconds(0); + std::cout << "[TimeMan] Using fixed movetime: " << tc.movetime << " ms\n"; + } + else + { + // Fall back to clock-based time control + int time_left = tc.white_to_move ? tc.wtime : tc.btime; + int inc_ms = tc.white_to_move ? tc.winc : tc.binc; + + if (time_left != INFINITE_TIME) + { + int base_time = time_left / moves_to_go; + int safe_time = base_time / 16 + inc_ms; + safe_time = std::clamp(safe_time, MIN_PER_MOVE, MAX_PER_MOVE); + + double scale = get_phase_scale(depth); + safe_time = static_cast(safe_time * scale); + + time_limit = milliseconds(safe_time); + inc = milliseconds(inc_ms); + + std::cout << "[TimeMan] Using clock. Left: " << time_left << " ms, inc: " << inc_ms + << " ms, moves_to_go: " << moves_to_go << ", scaled: " << safe_time + << " ms\n"; + } + } + + reset_start_time(); +} + +bool check_time() { + if (infinite) + return false; + + int elapsed = duration_cast(high_resolution_clock::now() - start_time).count(); + + int dynamic_buffer = + std::max(MIN_SAFE_BUFFER_MS, static_cast(time_limit.count() * BUFFER_PERCENTAGE)); + + bool out_of_time = elapsed + dynamic_buffer >= time_limit.count(); + + if (out_of_time) + { + std::cout << "[TimeMan] Out of time! Elapsed: " << elapsed + << " ms, limit: " << time_limit.count() << " ms, buffer: " << dynamic_buffer + << " ms" << std::endl; + } + + return out_of_time; +} +} // namespace timeman diff --git a/src/timeman.hpp b/src/timeman.hpp new file mode 100644 index 0000000..8b89c52 --- /dev/null +++ b/src/timeman.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "types.h" +#include +constexpr int INFINITE_TIME = 86400000; +struct TimeControl { + int wtime = INFINITE_TIME; + int btime = INFINITE_TIME; + int winc = 0; + int binc = 0; + int movestogo = 40; + int movetime = INFINITE_TIME; + uint8_t depth = Stockfish::MAX_PLY; + bool infinite = false; + bool white_to_move = true; +}; + +namespace timeman { +extern std::chrono::time_point start_time; +extern std::chrono::milliseconds time_buffer; +extern std::chrono::milliseconds inc; +extern std::chrono::milliseconds time_limit; +extern int moves_to_go; +extern bool infinite; + +bool check_time(); + +// Set time control limits using the TimeControl struct +void setLimits(const TimeControl& tc); +} // namespace timeman diff --git a/src/tt.cpp b/src/tt.cpp new file mode 100644 index 0000000..6fd19b2 --- /dev/null +++ b/src/tt.cpp @@ -0,0 +1,69 @@ +#include "tt.hpp" +void TranspositionTable::store( + uint64_t hash, chess::Move best, Stockfish::Value score, int8_t depth, TTFlag flag) { + // 2 entries per bucket + if (buckets == 0) + return; + uint64_t index = (hash & (buckets - 1)) * 2; + TTEntry &e0 = table[index], &e1 = table[index + 1]; + // Store the entry + // Store the entry + TTEntry* replace = &e0; // Default to first entry + for (TTEntry* e : {&e0, &e1}) + { + if (e->hash == hash) + { + // Always replace exact hash match + e->set(depth, flag, score, best); + return; + } + if (e->depth < depth) + { + // Prefer entry with lower depth + replace = e; + break; + } + if (e->depth < replace->depth) + { + // Among candidates, choose the one with lowest depth + replace = e; + } + } + replace->hash = hash; + replace->set(depth, flag, score, best); +} + +TTEntry* TranspositionTable::lookup(uint64_t hash) { + // 2 entries per bucket + if (buckets == 0) + return nullptr; + uint64_t index = (hash & (buckets - 1)) * 2; + TTEntry &e0 = table[index], &e1 = table[index + 1]; + // Check the entries + for (TTEntry* e : {&e0, &e1}) + { + if (e->hash == hash) + return e; + } + return nullptr; +} +int TranspositionTable::hashfull() const { + size_t count = 0; + + // Unroll loop for speed (process 4 entries per iteration) + size_t i = 0; + size_t limit = size & ~3ULL; // align to multiple of 4 + for (; i < limit; i += 4) + { + count += (table[i].hash != 0); + count += (table[i + 1].hash != 0); + count += (table[i + 2].hash != 0); + count += (table[i + 3].hash != 0); + } + for (; i < size; ++i) + { + count += (table[i].hash != 0); + } + + return static_cast((count * 1000) / size); +} diff --git a/src/tt.hpp b/src/tt.hpp new file mode 100644 index 0000000..c2f1b36 --- /dev/null +++ b/src/tt.hpp @@ -0,0 +1,66 @@ +#pragma once +#include "chess.hpp" +#include "types.h" +#include +#include "memory.h" // Provides LargePagePtr and make_unique_large_page + +enum class TTFlag : uint8_t { + EXACT, + LOWERBOUND, + UPPERBOUND +}; + +struct TTEntry { + uint64_t hash; + uint16_t move; + Stockfish::Value score; + TTFlag flag; + int8_t depth; + void set(int8_t d, TTFlag f, Stockfish::Value s, chess::Move m) { + depth = d; + flag = f; + score = s; + move = m.move(); + } +}; + +class TranspositionTable { + NNUEParser::LargePagePtr table; // Aligned (large-page) buffer + int size = 0; + int buckets = 0; + + public: + TranspositionTable() = default; + + TranspositionTable(int sizeInMB) { resize(sizeInMB); } + + void resize(int sizeInMB) { + // Round down to power of 2 + while (sizeInMB & (sizeInMB - 1)) + sizeInMB >>= 1; + + int newSize = sizeInMB * 1048576 / sizeof(TTEntry); + if (newSize == size) + { + clear(); + return; + } + + table = NNUEParser::make_unique_large_page(newSize); + if (!table) + { + std::cerr << "info string TT allocation failed\n"; + std::exit(EXIT_FAILURE); + } + + size = newSize; + buckets = size / 2; + clear(); + } + + inline void clear() { std::fill_n(table.get(), size, TTEntry{}); } + + int hashfull() const; // Implement full-scan or sampling + TTEntry* lookup(uint64_t h); // Existing lookup logic + void store(uint64_t h, chess::Move m, Stockfish::Value s, int8_t d, TTFlag f); +}; diff --git a/src/types.h b/src/types.h new file mode 100644 index 0000000..a2ae2b0 --- /dev/null +++ b/src/types.h @@ -0,0 +1,444 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef TYPES_H_INCLUDED +#define TYPES_H_INCLUDED + +// When compiling with provided Makefile (e.g. for Linux and OSX), configuration +// is done automatically. To get started type 'make help'. +// +// When Makefile is not used (e.g. with Microsoft Visual Studio) some switches +// need to be set manually: +// +// -DNDEBUG | Disable debugging mode. Always use this for release. +// +// -DNO_PREFETCH | Disable use of prefetch asm-instruction. You may need this to +// | run on some very old machines. +// +// -DUSE_POPCNT | Add runtime support for use of popcnt asm-instruction. Works +// | only in 64-bit mode and requires hardware with popcnt support. +// +// -DUSE_PEXT | Add runtime support for use of pext asm-instruction. Works +// | only in 64-bit mode and requires hardware with pext support. + +#include +#include +#include +#include + +#if defined(_MSC_VER) + // Disable some silly and noisy warnings from MSVC compiler + #pragma warning(disable: 4127) // Conditional expression is constant + #pragma warning(disable: 4146) // Unary minus operator applied to unsigned type + #pragma warning(disable: 4800) // Forcing value to bool 'true' or 'false' +#endif + +// Predefined macros hell: +// +// __GNUC__ Compiler is GCC, Clang or ICX +// __clang__ Compiler is Clang or ICX +// __INTEL_LLVM_COMPILER Compiler is ICX +// _MSC_VER Compiler is MSVC +// _WIN32 Building on Windows (any) +// _WIN64 Building on Windows 64 bit + +// Enforce minimum GCC version +#if defined(__GNUC__) && !defined(__clang__) \ + && (__GNUC__ < 9 || (__GNUC__ == 9 && __GNUC_MINOR__ < 3)) + #error "Stockfish requires GCC 9.3 or later for correct compilation" +#endif + +// Enforce minimum Clang version +#if defined(__clang__) && (__clang_major__ < 10) + #error "Stockfish requires Clang 10.0 or later for correct compilation" +#endif + +#define ASSERT_ALIGNED(ptr, alignment) assert(reinterpret_cast(ptr) % alignment == 0) + +#if defined(_WIN64) && defined(_MSC_VER) // No Makefile used + #include // Microsoft header for _BitScanForward64() + #define IS_64BIT +#endif + +#if defined(USE_POPCNT) && defined(_MSC_VER) + #include // Microsoft header for _mm_popcnt_u64() +#endif + +#if !defined(NO_PREFETCH) && defined(_MSC_VER) + #include // Microsoft header for _mm_prefetch() +#endif + +#if defined(USE_PEXT) + #include // Header for _pext_u64() intrinsic + #define pext(b, m) _pext_u64(b, m) +#else + #define pext(b, m) 0 +#endif + +namespace Stockfish { + +#ifdef USE_POPCNT +constexpr bool HasPopCnt = true; +#else +constexpr bool HasPopCnt = false; +#endif + +#ifdef USE_PEXT +constexpr bool HasPext = true; +#else +constexpr bool HasPext = false; +#endif + +#ifdef IS_64BIT +constexpr bool Is64Bit = true; +#else +constexpr bool Is64Bit = false; +#endif + +using Key = uint64_t; +using Bitboard = uint64_t; + +constexpr int MAX_MOVES = 256; +constexpr int MAX_PLY = 246; + +enum Color : int8_t { + WHITE, + BLACK, + COLOR_NB = 2 +}; + +enum CastlingRights : int8_t { + NO_CASTLING, + WHITE_OO, + WHITE_OOO = WHITE_OO << 1, + BLACK_OO = WHITE_OO << 2, + BLACK_OOO = WHITE_OO << 3, + + KING_SIDE = WHITE_OO | BLACK_OO, + QUEEN_SIDE = WHITE_OOO | BLACK_OOO, + WHITE_CASTLING = WHITE_OO | WHITE_OOO, + BLACK_CASTLING = BLACK_OO | BLACK_OOO, + ANY_CASTLING = WHITE_CASTLING | BLACK_CASTLING, + + CASTLING_RIGHT_NB = 16 +}; + +enum Bound : int8_t { + BOUND_NONE, + BOUND_UPPER, + BOUND_LOWER, + BOUND_EXACT = BOUND_UPPER | BOUND_LOWER +}; + +// Value is used as an alias for int, this is done to differentiate between a search +// value and any other integer value. The values used in search are always supposed +// to be in the range (-VALUE_NONE, VALUE_NONE] and should not exceed this range. +using Value = int; + +constexpr Value VALUE_ZERO = 0; +constexpr Value VALUE_DRAW = 0; +constexpr Value VALUE_NONE = 32002; +constexpr Value VALUE_INFINITE = 32001; + +constexpr Value VALUE_MATE = 32000; +constexpr Value VALUE_MATE_IN_MAX_PLY = VALUE_MATE - MAX_PLY; +constexpr Value VALUE_MATED_IN_MAX_PLY = -VALUE_MATE_IN_MAX_PLY; + +constexpr Value VALUE_TB = VALUE_MATE_IN_MAX_PLY - 1; +constexpr Value VALUE_TB_WIN_IN_MAX_PLY = VALUE_TB - MAX_PLY; +constexpr Value VALUE_TB_LOSS_IN_MAX_PLY = -VALUE_TB_WIN_IN_MAX_PLY; + + +constexpr bool is_valid(Value value) { return value != VALUE_NONE; } + +constexpr bool is_win(Value value) { + assert(is_valid(value)); + return value >= VALUE_TB_WIN_IN_MAX_PLY; +} + +constexpr bool is_loss(Value value) { + assert(is_valid(value)); + return value <= VALUE_TB_LOSS_IN_MAX_PLY; +} + +constexpr bool is_decisive(Value value) { return is_win(value) || is_loss(value); } + +// In the code, we make the assumption that these values +// are such that non_pawn_material() can be used to uniquely +// identify the material on the board. +constexpr Value PawnValue = 208; +constexpr Value KnightValue = 781; +constexpr Value BishopValue = 825; +constexpr Value RookValue = 1276; +constexpr Value QueenValue = 2538; + + +// clang-format off +enum PieceType : std::int8_t { + NO_PIECE_TYPE, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, + ALL_PIECES = 0, + PIECE_TYPE_NB = 8 +}; + +enum Piece : std::int8_t { + NO_PIECE, + W_PAWN = PAWN, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING, + B_PAWN = PAWN + 8, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING, + PIECE_NB = 16 +}; +// clang-format on + +constexpr Value PieceValue[PIECE_NB] = { + VALUE_ZERO, PawnValue, KnightValue, BishopValue, RookValue, QueenValue, VALUE_ZERO, VALUE_ZERO, + VALUE_ZERO, PawnValue, KnightValue, BishopValue, RookValue, QueenValue, VALUE_ZERO, VALUE_ZERO}; + +using Depth = int; + +// The following DEPTH_ constants are used for transposition table entries +// and quiescence search move generation stages. In regular search, the +// depth stored in the transposition table is literal: the search depth +// (effort) used to make the corresponding transposition table value. In +// quiescence search, however, the transposition table entries only store +// the current quiescence move generation stage (which should thus compare +// lower than any regular search depth). +constexpr Depth DEPTH_QS = 0; +// For transposition table entries where no searching at all was done +// (whether regular or qsearch) we use DEPTH_UNSEARCHED, which should thus +// compare lower than any quiescence or regular depth. DEPTH_ENTRY_OFFSET +// is used only for the transposition table entry occupancy check (see tt.cpp), +// and should thus be lower than DEPTH_UNSEARCHED. +constexpr Depth DEPTH_UNSEARCHED = -2; +constexpr Depth DEPTH_ENTRY_OFFSET = -3; + +// clang-format off +enum Square : int8_t { + SQ_A1, SQ_B1, SQ_C1, SQ_D1, SQ_E1, SQ_F1, SQ_G1, SQ_H1, + SQ_A2, SQ_B2, SQ_C2, SQ_D2, SQ_E2, SQ_F2, SQ_G2, SQ_H2, + SQ_A3, SQ_B3, SQ_C3, SQ_D3, SQ_E3, SQ_F3, SQ_G3, SQ_H3, + SQ_A4, SQ_B4, SQ_C4, SQ_D4, SQ_E4, SQ_F4, SQ_G4, SQ_H4, + SQ_A5, SQ_B5, SQ_C5, SQ_D5, SQ_E5, SQ_F5, SQ_G5, SQ_H5, + SQ_A6, SQ_B6, SQ_C6, SQ_D6, SQ_E6, SQ_F6, SQ_G6, SQ_H6, + SQ_A7, SQ_B7, SQ_C7, SQ_D7, SQ_E7, SQ_F7, SQ_G7, SQ_H7, + SQ_A8, SQ_B8, SQ_C8, SQ_D8, SQ_E8, SQ_F8, SQ_G8, SQ_H8, + SQ_NONE, + + SQUARE_ZERO = 0, + SQUARE_NB = 64 +}; +// clang-format on + +enum Direction : int8_t { + NORTH = 8, + EAST = 1, + SOUTH = -NORTH, + WEST = -EAST, + + NORTH_EAST = NORTH + EAST, + SOUTH_EAST = SOUTH + EAST, + SOUTH_WEST = SOUTH + WEST, + NORTH_WEST = NORTH + WEST +}; + +enum File : int8_t { + FILE_A, + FILE_B, + FILE_C, + FILE_D, + FILE_E, + FILE_F, + FILE_G, + FILE_H, + FILE_NB +}; + +enum Rank : int8_t { + RANK_1, + RANK_2, + RANK_3, + RANK_4, + RANK_5, + RANK_6, + RANK_7, + RANK_8, + RANK_NB +}; + +// Keep track of what a move changes on the board (used by NNUE) +struct DirtyPiece { + Piece pc; // this is never allowed to be NO_PIECE + Square from, to; // to should be SQ_NONE for promotions + + // if {add,remove}_sq is SQ_NONE, {add,remove}_pc is allowed to be + // uninitialized + // castling uses add_sq and remove_sq to remove and add the rook + Square remove_sq, add_sq; + Piece remove_pc, add_pc; +}; + +#define ENABLE_INCR_OPERATORS_ON(T) \ + constexpr T& operator++(T& d) { return d = T(int(d) + 1); } \ + constexpr T& operator--(T& d) { return d = T(int(d) - 1); } + +ENABLE_INCR_OPERATORS_ON(PieceType) +ENABLE_INCR_OPERATORS_ON(Square) +ENABLE_INCR_OPERATORS_ON(File) +ENABLE_INCR_OPERATORS_ON(Rank) + +#undef ENABLE_INCR_OPERATORS_ON + +constexpr Direction operator+(Direction d1, Direction d2) { return Direction(int(d1) + int(d2)); } +constexpr Direction operator*(int i, Direction d) { return Direction(i * int(d)); } + +// Additional operators to add a Direction to a Square +constexpr Square operator+(Square s, Direction d) { return Square(int(s) + int(d)); } +constexpr Square operator-(Square s, Direction d) { return Square(int(s) - int(d)); } +constexpr Square& operator+=(Square& s, Direction d) { return s = s + d; } +constexpr Square& operator-=(Square& s, Direction d) { return s = s - d; } + +// Toggle color +constexpr Color operator~(Color c) { return Color(c ^ BLACK); } + +// Swap A1 <-> A8 +constexpr Square flip_rank(Square s) { return Square(s ^ SQ_A8); } + +// Swap A1 <-> H1 +constexpr Square flip_file(Square s) { return Square(s ^ SQ_H1); } + +// Swap color of piece B_KNIGHT <-> W_KNIGHT +constexpr Piece operator~(Piece pc) { return Piece(pc ^ 8); } + +constexpr CastlingRights operator&(Color c, CastlingRights cr) { + return CastlingRights((c == WHITE ? WHITE_CASTLING : BLACK_CASTLING) & cr); +} + +constexpr Value mate_in(int ply) { return VALUE_MATE - ply; } + +constexpr Value mated_in(int ply) { return -VALUE_MATE + ply; } + +constexpr Square make_square(File f, Rank r) { return Square((r << 3) + f); } + +constexpr Piece make_piece(Color c, PieceType pt) { return Piece((c << 3) + pt); } + +constexpr PieceType type_of(Piece pc) { return PieceType(pc & 7); } + +constexpr Color color_of(Piece pc) { + assert(pc != NO_PIECE); + return Color(pc >> 3); +} + +constexpr bool is_ok(Square s) { return s >= SQ_A1 && s <= SQ_H8; } + +constexpr File file_of(Square s) { return File(s & 7); } + +constexpr Rank rank_of(Square s) { return Rank(s >> 3); } + +constexpr Square relative_square(Color c, Square s) { return Square(s ^ (c * 56)); } + +constexpr Rank relative_rank(Color c, Rank r) { return Rank(r ^ (c * 7)); } + +constexpr Rank relative_rank(Color c, Square s) { return relative_rank(c, rank_of(s)); } + +constexpr Direction pawn_push(Color c) { return c == WHITE ? NORTH : SOUTH; } + + +// Based on a congruential pseudo-random number generator +constexpr Key make_key(uint64_t seed) { + return seed * 6364136223846793005ULL + 1442695040888963407ULL; +} + + +enum MoveType { + NORMAL, + PROMOTION = 1 << 14, + EN_PASSANT = 2 << 14, + CASTLING = 3 << 14 +}; + +// A move needs 16 bits to be stored +// +// bit 0- 5: destination square (from 0 to 63) +// bit 6-11: origin square (from 0 to 63) +// bit 12-13: promotion piece type - 2 (from KNIGHT-2 to QUEEN-2) +// bit 14-15: special move flag: promotion (1), en passant (2), castling (3) +// NOTE: en passant bit is set only when a pawn can be captured +// +// Special cases are Move::none() and Move::null(). We can sneak these in because +// in any normal move the destination square and origin square are always different, +// but Move::none() and Move::null() have the same origin and destination square. + +class Move { + public: + Move() = default; + constexpr explicit Move(std::uint16_t d) : + data(d) {} + + constexpr Move(Square from, Square to) : + data((from << 6) + to) {} + + template + static constexpr Move make(Square from, Square to, PieceType pt = KNIGHT) { + return Move(T + ((pt - KNIGHT) << 12) + (from << 6) + to); + } + + constexpr Square from_sq() const { + assert(is_ok()); + return Square((data >> 6) & 0x3F); + } + + constexpr Square to_sq() const { + assert(is_ok()); + return Square(data & 0x3F); + } + + constexpr int from_to() const { return data & 0xFFF; } + + constexpr MoveType type_of() const { return MoveType(data & (3 << 14)); } + + constexpr PieceType promotion_type() const { return PieceType(((data >> 12) & 3) + KNIGHT); } + + constexpr bool is_ok() const { return none().data != data && null().data != data; } + + static constexpr Move null() { return Move(65); } + static constexpr Move none() { return Move(0); } + + constexpr bool operator==(const Move& m) const { return data == m.data; } + constexpr bool operator!=(const Move& m) const { return data != m.data; } + + constexpr explicit operator bool() const { return data != 0; } + + constexpr std::uint16_t raw() const { return data; } + + struct MoveHash { + std::size_t operator()(const Move& m) const { return make_key(m.data); } + }; + + protected: + std::uint16_t data; +}; + +template +struct is_all_same { + static constexpr bool value = (std::is_same_v && ...); +}; + +template +constexpr auto is_all_same_v = is_all_same::value; + +} // namespace Stockfish + +#endif // #ifndef TYPES_H_INCLUDED diff --git a/src/uci.cpp b/src/uci.cpp new file mode 100644 index 0000000..e705d1b --- /dev/null +++ b/src/uci.cpp @@ -0,0 +1,333 @@ +#include "uci.hpp" +#include + +chess::Board board; +std::mutex board_mutex; +// Thread handle +static std::thread search_thread; +static void handleSetOption(const std::string& input) { + std::istringstream iss(input); + std::string token, name, value; + bool reading_name = false, reading_value = false; + + while (iss >> token) + { + if (token == "setoption") + continue; + if (token == "name") + { + reading_name = true; + reading_value = false; + name.clear(); + value.clear(); + continue; + } + if (token == "value") + { + reading_value = true; + reading_name = false; + continue; + } + + if (reading_name) + { + if (!name.empty()) + name += " "; + name += token; + } + else if (reading_value) + { + if (!value.empty()) + value += " "; + value += token; + } + } + + // Clean up name + auto pos = name.find_last_not_of(" \n\r\t"); + if (pos != std::string::npos) + { + name.erase(pos + 1); + } + else + { + name.clear(); // String contains only whitespace + } + + if (!UCIOptions::options.count(name)) + { + std::cerr << "info string Unknown option: " << name << "\n"; + return; + } + std::cerr << "info string option='" << name << "' value='" << value << "'\n"; + UCIOptions::setOption(name, value); +} +static void handle_uci() { + std::cout << "id name cppchess_engine\n"; + std::cout << "id author winapiadmin\n"; + UCIOptions::printOptions(); + std::cout << "uciok" << std::endl; +} + +inline void handle_isready() { std::cout << "readyok" << std::endl; } + +inline void handle_quit() { + search::stop_requested = true; + if (search_thread.joinable()) + search_thread.join(); +} + +inline void handle_stop() { search::stop_requested = true; } + +inline void handle_display() { std::cout << board << std::endl; } + +static void handle_position(std::istringstream& iss) { + std::string token; + iss >> token; + + if (token == "startpos") + { + board.setFen(chess::constants::STARTPOS); + iss >> token; + } + else if (token == "fen") + { + std::vector fen_parts; + while (iss >> token && token != "moves") + { + fen_parts.push_back(token); + } + + // Assign default values if necessary + while (fen_parts.size() < 6) + { + switch (fen_parts.size()) + { + case 1 : + fen_parts.push_back("w"); + break; // Active color + case 2 : + fen_parts.push_back("-"); + break; // Castling availability + case 3 : + fen_parts.push_back("-"); + break; // En passant target square + case 4 : + fen_parts.push_back("0"); + break; // Halfmove clock + case 5 : + fen_parts.push_back("1"); + break; // Fullmove number + } + } + + // Reconstruct the FEN string + std::string fen = fen_parts[0]; + for (size_t i = 1; i < 6; ++i) + { + fen += " " + fen_parts[i]; + } + + { + std::lock_guard lock(board_mutex); + board.setFen(fen); + } + } + + if (token == "moves") + { + while (iss >> token) + { + chess::Move mv = chess::uci::uciToMove(board, token); + { + std::lock_guard lock(board_mutex); + board.makeMove(mv); + } + } + } +} + +void handle_bench() { + uint64_t nodes = 0; + std::vector fen_list = { + "r7/pp3kb1/7p/2nr4/4p3/7P/PP1N1PP1/R1B1K2R b KQ - 1 19", + "r1bqk2r/pppp1ppp/2n5/4p3/4P3/2N5/PPPP1PPP/R1BQK2R w KQkq - 0 1", + "rnbqkb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1", + "8/pp3k2/7p/2nR4/4p3/7P/Pb3PP1/6K1 b - - 1 25", + "8/p4k2/1p1R3p/2n5/4p3/7P/Pb3PP1/6K1 b - - 1 26", + "8/p4k2/1p1R1b1p/2n5/4p3/7P/P4PP1/5K2 b - - 3 27", + "8/p3k3/1pR2b1p/2n5/4p3/7P/P4PP1/5K2 b - - 5 28", + "1R6/8/1p1knb1p/p7/4p3/7P/P3KPP1/8 b - - 3 32", + "3R4/8/1p1k3p/p7/3bp2P/6Pn/P3KP2/8 b - - 2 35", + "8/4R3/1p6/p5PP/3b4/2n2K2/P2kp3/8 w - - 1 46", + "rnbqkb1r/ppp1pppp/1n6/8/8/2N2N2/PPPP1PPP/R1BQKB1R w KQkq - 2 5", + "r2qr1k1/1ppb1pbp/np4p1/3Pp3/4N3/P2B1N1P/1PP2PP1/2RQR1K1 b - - 6 16", + "8/8/5k2/8/8/4Q1K1/PPP1PPPP/3R1BR1 w - - 67 88"}; + std::vector boards(fen_list.size()); + auto start_time = std::chrono::high_resolution_clock::now(); + for (size_t i = 0; i < fen_list.size(); ++i) + { + std::cout << "Position " << i + 1 << "/" << fen_list.size() << std::endl; + boards[i].setFen(fen_list[i]); + TimeControl tc{}; + tc.movestogo = 1; + tc.depth = 5; + tc.infinite = false; + // All other fields remain at default (0 or -1) + + search::run_search(boards[i], tc); + + nodes += search::nodes; + } + auto end_time = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = end_time - start_time; + + std::cout << "===========================\n"; + std::cout << "Total time (ms) : " << static_cast(elapsed.count() * 1000) << "\n"; + std::cout << "Nodes searched : " << nodes << "\n"; + std::cout << "Nodes/second : " << static_cast(nodes / elapsed.count()) + << std::endl; +} +static void handle_go(std::istringstream& iss) { + TimeControl tc; + bool white = (board.sideToMove() == chess::Color::WHITE); + std::string sub; + int depth = Stockfish::MAX_PLY; + while (iss >> sub) + { + if (sub == "depth") + iss >> depth; + else if (sub == "movetime") + iss >> tc.movetime; + else if (sub == "wtime") + iss >> tc.wtime; + else if (sub == "btime") + iss >> tc.btime; + else if (sub == "winc") + iss >> tc.winc; + else if (sub == "binc") + iss >> tc.binc; + else if (sub == "movestogo") + iss >> tc.movestogo; + else if (sub == "infinite") + { + tc.infinite = true; + tc.depth = Stockfish::MAX_PLY; + } + } + tc.white_to_move = white; + tc.depth = depth; + if (search_thread.joinable()) + { + search::stop_requested = true; + search_thread.join(); + } + + search::stop_requested = false; + + chess::Board board_copy; + { + std::lock_guard lock(board_mutex); + board_copy = board; + } + search_thread = std::thread(search::run_search, board_copy, tc); // keep joinable +} + +inline void handle_ucinewgame() { + std::lock_guard lock(board_mutex); + board.setFen(chess::constants::STARTPOS); + search::tt.clear(); +} +void uci_loop() { + std::string line; + while (std::getline(std::cin, line)) + { + std::istringstream iss(line); + std::string token; + iss >> token; + if (token == "ucinewgame") + handle_ucinewgame(); + else if (token == "uci") + handle_uci(); + else if (token == "isready") + handle_isready(); + else if (token == "quit") + { + handle_quit(); + return; // Exit the loop after quitting + } + else if (token == "stop") + handle_stop(); + else if (token == "setoption") + handleSetOption(line); + else if (token == "position") + handle_position(iss); + else if (token == "go") + handle_go(iss); + else if (token == "d") + handle_display(); + else if (token == "bench") + handle_bench(); + else + std::cerr << "[DEBUG] Unknown command: " << token << "\n"; + } +} + +struct WinRateParams { + double a; + double b; +}; +static int win_rate_model(Stockfish::Value v, const Stockfish::Position& pos); +static WinRateParams win_rate_params(const Stockfish::Position& pos) { + + int material = pos.count() + 3 * pos.count() + + 3 * pos.count() + 5 * pos.count() + + 9 * pos.count(); + + // The fitted model only uses data for material counts in [17, 78], and is anchored at count 58. + double m = std::clamp(material, 17, 78) / 58.0; + + // Return a = p_a(material) and b = p_b(material), see github.com/official-stockfish/WDL_model + constexpr double as[] = {-13.50030198, 40.92780883, -36.82753545, 386.83004070}; + constexpr double bs[] = {96.53354896, -165.79058388, 90.89679019, 49.29561889}; + + double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3]; + double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3]; + + return {a, b}; +} + +// The win rate model is 1 / (1 + exp((a - eval) / b)), where a = p_a(material) and b = p_b(material). +// It fits the LTC fishtest statistics rather accurately. +static int win_rate_model(Stockfish::Value v, const Stockfish::Position& pos) { + + auto [a, b] = win_rate_params(pos); + + // Return the win rate in per mille units, rounded to the nearest integer. + return int(0.5 + 1000 / (1 + std::exp((a - double(v)) / b))); +} + +// Turns a Value to an integer centipawn number, +// without treatment of mate and similar special scores. +int to_cp(Stockfish::Value v, const Stockfish::Position& pos) { + + // In general, the score can be defined via the WDL as + // (log(1/L - 1) - log(1/W - 1)) / (log(1/L - 1) + log(1/W - 1)). + // Based on our win_rate_model, this simply yields v / a. + + auto [a, b] = win_rate_params(pos); + + return std::round(100 * int(v) / a); +} + +std::string wdl(Stockfish::Value v, const Stockfish::Position& pos) { + std::stringstream ss; + + int wdl_w = win_rate_model(v, pos); + int wdl_l = win_rate_model(-v, pos); + int wdl_d = 1000 - wdl_w - wdl_l; + ss << wdl_w << " " << wdl_d << " " << wdl_l; + + return ss.str(); +} diff --git a/uci.hpp b/src/uci.hpp similarity index 55% rename from uci.hpp rename to src/uci.hpp index 8ea9bb4..595b4ab 100644 --- a/uci.hpp +++ b/src/uci.hpp @@ -1,4 +1,7 @@ #pragma once +#include "chess.hpp" +#include "search.h" +#include "ucioptions.hpp" #include #include #include @@ -6,6 +9,6 @@ #include #include #include -#include "chess.hpp" -#include "search.hpp" void uci_loop(); +void handle_bench(); // main() +int to_cp(Stockfish::Value v, const Stockfish::Position& pos); diff --git a/src/ucioptions.cpp b/src/ucioptions.cpp new file mode 100644 index 0000000..f1ae195 --- /dev/null +++ b/src/ucioptions.cpp @@ -0,0 +1,180 @@ +#include "ucioptions.hpp" +#include +#include +#include + +namespace UCIOptions { + +// Global options map +std::unordered_map options; + +// Constructors with callback support +Option::Option(Type t, int def, int min_, int max_, std::function cb) : + type(t), + min(min_), + max(max_), + value(def), + on_change(std::move(cb)) {} + +Option::Option(Type t, const std::string& def, std::function cb) : + type(t), + default_str(def), + value(def), + on_change(std::move(cb)) {} + +Option::Option(Type t, + const std::string& def, + const std::vector& _options, + std::function cb) : + type(t), + default_str(def), + vars(_options), + value(def), + on_change(std::move(cb)) {} + +// Adders with optional callbacks +void addSpin(const std::string& name, + int defaultValue, + int min, + int max, + std::function cb) { + options[name] = Option(Option::SPIN, defaultValue, min, max, std::move(cb)); + if (options[name].on_change) + options[name].on_change(options[name]); +} + +void addCheck(const std::string& name, bool defaultValue, std::function cb) { + options[name] = Option(Option::CHECK, defaultValue ? 1 : 0, 0, 1, std::move(cb)); + if (options[name].on_change) + options[name].on_change(options[name]); +} + +void addString(const std::string& name, + const std::string& defaultValue, + std::function cb) { + options[name] = Option(Option::STRING, defaultValue, std::move(cb)); + if (options[name].on_change) + options[name].on_change(options[name]); +} + +void addCombo(const std::string& name, + const std::string& defaultValue, + const std::vector& vars, + std::function cb) { + options[name] = Option(Option::COMBO, defaultValue, vars, std::move(cb)); + if (options[name].on_change) + options[name].on_change(options[name]); +} + +void addButton(const std::string& name, std::function cb) { + options[name] = Option(Option::BUTTON, "", {}, std::move(cb)); + if (options[name].on_change) + options[name].on_change(options[name]); +} + +void setOption(const std::string& name, const std::string& val) { + const auto it = options.find(name); + if (it == options.end()) + { + std::cerr << "info string Unknown option " << name << '\n'; + return; // Early-out – nothing to update + } + auto& opt = it->second; + + switch (opt.type) + { + case Option::CHECK : + if (val == "true" || val == "1") + opt.value = 1; + else if (val == "false" || val == "0") + opt.value = 0; + else + { + std::cerr << "info string Invalid value for option " << name << ": " << val + << " (expected true/false or 1/0)\n"; + return; + } + break; + case Option::SPIN : { + int v{}; + auto [p, ec] = std::from_chars(val.data(), val.data() + val.size(), v); + if (ec != std::errc() || p != val.data() + val.size()) + { + std::cerr << "info string Invalid value for option " << name << ": " << val + << " (not a valid integer)\n"; + return; + } + opt.value = std::clamp(v, opt.min, opt.max); + break; + } + case Option::STRING : + opt.value = val; + break; + case Option::COMBO : + if (std::find(opt.vars.begin(), opt.vars.end(), val) != opt.vars.end()) + { + opt.value = val; + } + else + { + std::cerr << "info string Illegal value \"" << val << "\" for option " << name << "\n"; + return; + } + break; + case Option::BUTTON : + // no value change + break; + } + + // Invoke the callback if present + if (opt.on_change) + opt.on_change(opt); +} + +int getInt(const std::string& name) { + const auto& opt = options.at(name); + if (auto pval = std::get_if(&opt.value)) + return *pval; + std::cerr << "info string Option " + name + " is not an integer"; + exit(1); +} + +std::string getString(const std::string& name) { + const auto& opt = options.at(name); + if (auto pval = std::get_if(&opt.value)) + return *pval; + if (auto pval = std::get_if(&opt.value)) + return std::to_string(*pval); + return ""; +} + +void printOptions() { + for (const auto& [name, opt] : options) + { + std::cout << "option name " << name << " type "; + switch (opt.type) + { + case Option::CHECK : + std::cout << "check default " << (std::get(opt.value) ? "true" : "false") << "\n"; + break; + case Option::SPIN : + std::cout << "spin default " << std::get(opt.value) << " min " << opt.min + << " max " << opt.max << "\n"; + break; + case Option::STRING : + std::cout << "string default " << std::get(opt.value) << "\n"; + break; + case Option::COMBO : + std::cout << "combo default " << std::get(opt.value); + for (const auto& var : opt.vars) + std::cout << " var " << var; + std::cout << "\n"; + break; + case Option::BUTTON : + std::cout << "button\n"; + break; + } + } +} + +} // namespace UCIOptions diff --git a/src/ucioptions.hpp b/src/ucioptions.hpp new file mode 100644 index 0000000..af3e1af --- /dev/null +++ b/src/ucioptions.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace UCIOptions { + +struct Option { + enum Type { + CHECK, + SPIN, + COMBO, + BUTTON, + STRING + } type; + + std::string default_str; + std::vector vars; // for COMBO + int min = 0, max = 0; + + std::variant value; + + // Optional callback when the option is set + std::function on_change; + + Option() = default; + Option(Type t, int def, int min_ = 0, int max_ = 0, std::function cb = {}); + Option(Type t, const std::string& def, std::function cb = {}); + Option(Type t, + const std::string& def, + const std::vector& _options, + std::function cb = {}); +}; + +// Declare the global options map +extern std::unordered_map options; + +// Adders with optional callback support +void addSpin(const std::string& name, + int defaultValue, + int min, + int max, + std::function cb = {}); +void addCheck(const std::string& name, + bool defaultValue, + std::function cb = {}); +void addString(const std::string& name, + const std::string& defaultValue, + std::function cb = {}); +void addCombo(const std::string& name, + const std::string& defaultValue, + const std::vector& vars, + std::function cb = {}); +void addButton(const std::string& name, std::function cb = {}); + +void setOption(const std::string& name, const std::string& val); + +int getInt(const std::string& name); +std::string getString(const std::string& name); + +void printOptions(); + +} // namespace UCIOptions diff --git a/timeman.cpp b/timeman.cpp deleted file mode 100644 index fbafa58..0000000 --- a/timeman.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "timeman.hpp" -#include -#include -#include -#include -#include - -namespace timeman -{ - using namespace std::chrono; - - // Global state - TimeControl current_tc; // store the whole TimeControl struct globally - time_point start_time; - milliseconds inc = milliseconds(0); - milliseconds time_limit = milliseconds(0); - int moves_to_go = 40; - int depth = 0; - std::array times{}; - bool infinite = false; - - constexpr int MIN_SAFE_BUFFER_MS = 100; - constexpr double BUFFER_PERCENTAGE = 0.05; - constexpr int MIN_PER_MOVE = 400; - constexpr int MAX_PER_MOVE = 3000; - constexpr double PHASE_SCALE_OPENING = 0.7; - constexpr double PHASE_SCALE_MIDDLE = 1.0; - constexpr double PHASE_SCALE_ENDGAME = 1.25; - constexpr int OPENING_DEPTH = 5; - constexpr int MIDDLE_DEPTH = 10; - - double get_phase_scale(int current_depth) - { - if (current_depth <= OPENING_DEPTH) - return PHASE_SCALE_OPENING; - else if (current_depth <= MIDDLE_DEPTH) - return PHASE_SCALE_MIDDLE; - else - return PHASE_SCALE_ENDGAME; - } - - void reset_start_time() - { - start_time = high_resolution_clock::now(); - } -void setLimits(const TimeControl &tc) -{ - current_tc = tc; - infinite = tc.infinite; - moves_to_go = std::max(1, tc.movestogo); - depth = tc.depth; - - if (infinite && tc.depth > 0) { - time_limit = milliseconds::max(); - inc = milliseconds(0); - std::cout << "[TimeMan] Infinite or fixed depth mode.\n"; - } - else if (tc.movetime >= 0 && tc.movetime < INFINITE_TIME) { - // UCI: movetime overrides all other time settings - time_limit = milliseconds(tc.movetime); - inc = milliseconds(0); - std::cout << "[TimeMan] Using fixed movetime: " << tc.movetime << " ms\n"; - } - else { - // Fall back to clock-based time control - int time_left = tc.white_to_move ? tc.wtime : tc.btime; - int inc_ms = tc.white_to_move ? tc.winc : tc.binc; - - if (time_left != INFINITE_TIME) { - int base_time = time_left / moves_to_go; - int safe_time = base_time / 16 + inc_ms; - safe_time = std::clamp(safe_time, MIN_PER_MOVE, MAX_PER_MOVE); - - double scale = get_phase_scale(depth); - safe_time = static_cast(safe_time * scale); - - time_limit = milliseconds(safe_time); - inc = milliseconds(inc_ms); - - std::cout << "[TimeMan] Using clock. Left: " << time_left - << " ms, inc: " << inc_ms << " ms, moves_to_go: " - << moves_to_go << ", scaled: " << safe_time << " ms\n"; - } - } - - reset_start_time(); -} - - - bool check_time() - { - if (infinite) - return false; - - int elapsed = duration_cast(high_resolution_clock::now() - start_time).count(); - - int dynamic_buffer = std::max(MIN_SAFE_BUFFER_MS, static_cast(time_limit.count() * BUFFER_PERCENTAGE)); - - bool out_of_time = elapsed + dynamic_buffer >= time_limit.count(); - - if (out_of_time) - { - std::cout << "[TimeMan] Out of time! Elapsed: " << elapsed - << " ms, limit: " << time_limit.count() - << " ms, buffer: " << dynamic_buffer << " ms" << std::endl; - } - - return out_of_time; - } -} diff --git a/timeman.hpp b/timeman.hpp deleted file mode 100644 index e24641e..0000000 --- a/timeman.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include -#include "eval.hpp" -constexpr int INFINITE_TIME = 86400000; -struct TimeControl -{ - int wtime = INFINITE_TIME; - int btime = INFINITE_TIME; - int winc = 0; - int binc = 0; - int movestogo = 40; - int movetime = INFINITE_TIME; - uint8_t depth = MAX_PLY; - bool infinite = false; - bool white_to_move = true; -}; - -namespace timeman -{ - extern std::chrono::time_point start_time; - extern std::chrono::milliseconds time_buffer; - extern std::chrono::milliseconds inc; - extern std::chrono::milliseconds time_limit; - extern int moves_to_go; - extern bool infinite; - - bool check_time(); - - // Set time control limits using the TimeControl struct - void setLimits(const TimeControl &tc); -} diff --git a/tt.cpp b/tt.cpp deleted file mode 100644 index ee69361..0000000 --- a/tt.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "tt.hpp" - -void TranspositionTable::newSearch() -{ - time = 0; -} - -void TranspositionTable::store(uint64_t hash, chess::Move best, int16_t score, int8_t depth, TTFlag flag) -{ - // 2 entries per bucket - if (buckets == 0) - return; - uint64_t index = hash % buckets; - if (index >= buckets - 1) - index = buckets - 2; // Ensure we don't overflow - TTEntry &e0 = table[index], &e1 = table[index + 1]; - // Store the entry - - ++time; // monotonically increasing stamp - for (TTEntry *e : {&e0, &e1}) - { - if (!e->valid() || e->hash == hash || e->depth() < depth) - { - e->hash = hash; - e->set(depth, flag, score, time, true, best); - return; - } - } - // If we get here, we need to evict an entry - // Find the oldest entry - TTEntry *oldest = (e0.timestamp() < e1.timestamp()) ? &e0 : &e1; - // Evict it - oldest->hash = hash; - oldest->set(depth, flag, score, time, true, best); -} - -TTEntry *TranspositionTable::lookup(uint64_t hash) -{ - // 2 entries per bucket - if (buckets == 0) - return nullptr; - uint64_t index = hash % buckets; - if (index >= buckets - 1) - index = buckets - 2; // Ensure we don't overflow - TTEntry &e0 = table[index], &e1 = table[index + 1]; - // Check the entries - for (TTEntry *e : {&e0, &e1}) - { - if (e->valid() && e->hash == hash) - return e; - } - return nullptr; -} diff --git a/tt.hpp b/tt.hpp deleted file mode 100644 index 0ec4a93..0000000 --- a/tt.hpp +++ /dev/null @@ -1,122 +0,0 @@ -#pragma once -#include "chess.hpp" -#include -#include -enum class TTFlag : uint8_t -{ - EXACT = 0, - LOWERBOUND = 1, - UPPERBOUND = 2, - // 3 is unused or reserved -}; - -struct TTEntry -{ - uint64_t hash; - chess::Move bestMove; - uint64_t packed; - - // Getters - uint8_t depth() const - { - return ((packed >> 58) & 0x3F) + 1; // 6 bits depth-1 - } - - TTFlag flag() const - { - return static_cast((packed >> 56) & 0x3); - } - - uint64_t timestamp() const - { - return (packed >> 17) & 0x7FFFFFFFFFULL; // 39 bits - } - - bool valid() const - { - return (packed & 1) != 0; - } - - int16_t score() const - { - return bestMove.score(); - } - - // Setter - void set(uint8_t d, TTFlag f, int16_t s, uint64_t ts, bool v, chess::Move move) - { - // Store depth - 1 in 6 bits, clamp at 63 - uint8_t depth_encoded = (d > 0) ? (d - 1) : 0; - if (depth_encoded > 63) - depth_encoded = 63; - - packed = (uint64_t(depth_encoded) << 58) | - (uint64_t((int)f & 0x3) << 56) | - ((ts & 0x7FFFFFFFFFULL) << 17) | - (v); - bestMove=move; - bestMove.setScore(s); - } -}; -class TranspositionTable -{ - TTEntry *table; - int buckets; // number of buckets (pairs) - uint32_t time; - -public: - int size; // total number of TTEntry elements (must be even) - TranspositionTable() : table(nullptr), buckets(0), time(0), size(0) {} - - TranspositionTable(int sizeInMB) : time(0) - { - size = sizeInMB * 1048576 / sizeof(TTEntry); - if (size % 2 != 0) - size--; // Ensure even size - buckets = size / 2; - table = new TTEntry[size]; - clear(); - } - - ~TranspositionTable() - { - delete[] table; - } - - void resize(int sizeInMB) - { - TTEntry *old_table = table; - int old_size = size; - - size = sizeInMB * 1048576 / sizeof(TTEntry); - if (size % 2 != 0) - size--; - buckets = size / 2; - - TTEntry *new_table = new (std::nothrow) TTEntry[size]; - if (!new_table) - { - // Restore old values on failure - table = old_table; - size = old_size; - buckets = old_size / 2; - throw std::bad_alloc(); - } - - std::memcpy(new_table, old_table, - sizeof(TTEntry) * std::min(size, old_size)); - delete[] old_table; - table = new_table; - buckets = size / 2; - } - - void newSearch(); - void store(uint64_t hash, chess::Move best, int16_t score, int8_t depth, TTFlag flag); - - inline void clear() - { - std::fill_n(table, size, TTEntry{}); - } - - TTEntry *lookup(uint64_t hash); -}; diff --git a/uci.cpp b/uci.cpp deleted file mode 100644 index d27dd8f..0000000 --- a/uci.cpp +++ /dev/null @@ -1,270 +0,0 @@ -#include "uci.hpp" - -chess::Board board; -// Thread handle -static std::thread search_thread; -void handleSetOption(const std::string &input) -{ - std::istringstream iss(input); - std::string token, name, value; - bool reading_name = false, reading_value = false; - - while (iss >> token) - { - if (token == "setoption") - continue; - if (token == "name") - { - reading_name = true; - reading_value = false; - name.clear(); - value.clear(); - continue; - } - if (token == "value") - { - reading_value = true; - reading_name = false; - continue; - } - - if (reading_name) - { - if (!name.empty()) - name += " "; - name += token; - } - else if (reading_value) - { - if (!value.empty()) - value += " "; - value += token; - } - } - - // Clean up name - auto pos = name.find_last_not_of(" \n\r\t"); - if (pos != std::string::npos) - { - name.erase(pos + 1); - } - else - { - name.clear(); // String contains only whitespace - } - - if (!UCIOptions::options.count(name)) - { - std::cerr << "info string Unknown option: " << name << "\n"; - return; - } - - UCIOptions::setOption(name, value); -} -void handle_uci() -{ - std::cout << "id name cppchess_engine\n"; - std::cout << "id author winapiadmin\n"; - UCIOptions::printOptions(); - std::cout << "uciok\n"; -} - -inline void handle_isready() { std::cout << "readyok\n"; } - -void handle_quit() -{ - search::stop_requested = true; - if (search_thread.joinable()) - search_thread.join(); - std::exit(0); -} - -inline void handle_stop() { search::stop_requested = true; } - -inline void handle_display() { std::cout << board << std::endl; } - -inline void handle_eval() { trace(board); } -void handle_position(std::istringstream &iss) -{ - std::string token; - iss >> token; - - if (token == "startpos") - { - board.setFen(chess::constants::STARTPOS); - iss >> token; - } - else if (token == "fen") - { - std::vector fen_parts; - while (iss >> token && token != "moves") - { - fen_parts.push_back(token); - } - - // Assign default values if necessary - while (fen_parts.size() < 6) - { - switch (fen_parts.size()) - { - case 1: - fen_parts.push_back("w"); - break; // Active color - case 2: - fen_parts.push_back("-"); - break; // Castling availability - case 3: - fen_parts.push_back("-"); - break; // En passant target square - case 4: - fen_parts.push_back("0"); - break; // Halfmove clock - case 5: - fen_parts.push_back("1"); - break; // Fullmove number - } - } - - // Reconstruct the FEN string - std::string fen = fen_parts[0]; - for (size_t i = 1; i < 6; ++i) - { - fen += " " + fen_parts[i]; - } - - board.setFen(fen); - } - - if (token == "moves") - { - while (iss >> token) - { - chess::Move mv = chess::uci::uciToMove(board, token); - board.makeMove(mv); - } - } - - search::tt.clear(); -} - -void handle_bench() -{ - uint64_t nodes = 0; - std::vector fen_list = {"r7/pp3kb1/7p/2nr4/4p3/7P/PP1N1PP1/R1B1K2R b KQ - 1 19", - "r1bqk2r/pppp1ppp/2n5/4p3/4P3/2N5/PPPP1PPP/R1BQK2R w KQkq - 0 1", - "rnbqkb1r/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1", - "8/pp3k2/7p/2nR4/4p3/7P/Pb3PP1/6K1 b - - 1 25", - "8/p4k2/1p1R3p/2n5/4p3/7P/Pb3PP1/6K1 b - - 1 26", - "8/p4k2/1p1R1b1p/2n5/4p3/7P/P4PP1/5K2 b - - 3 27", - "8/p3k3/1pR2b1p/2n5/4p3/7P/P4PP1/5K2 b - - 5 28", - "1R6/8/1p1knb1p/p7/4p3/7P/P3KPP1/8 b - - 3 32", - "3R4/8/1p1k3p/p7/3bp2P/6Pn/P3KP2/8 b - - 2 35", - "8/4R3/1p6/p5PP/3b4/2n2K2/P2kp3/8 w - - 1 46", - "rnbqkb1r/ppp1pppp/1n6/8/8/2N2N2/PPPP1PPP/R1BQKB1R w KQkq - 2 5", - "r2qr1k1/1ppb1pbp/np4p1/3Pp3/4N3/P2B1N1P/1PP2PP1/2RQR1K1 b - - 6 16", - "8/8/5k2/8/8/4Q1K1/PPP1PPPP/3R1BR1 w - - 67 88"}; - std::vector boards(fen_list.size()); - auto start_time = std::chrono::high_resolution_clock::now(); - for (size_t i = 0; i < fen_list.size(); ++i) - { - std::cout << "Position " << i + 1 << "/" << fen_list.size() << std::endl; - boards[i].setFen(fen_list[i]); - TimeControl tc{}; - tc.movestogo = 1; - tc.depth = 8; - tc.infinite = false; - // All other fields remain at default (0 or -1) - - search::run_search(boards[i], tc); - - nodes += search::nodes; - } - auto end_time = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time - start_time; - - std::cout << "===========================\n"; - std::cout << "Total time (ms) : " << static_cast(elapsed.count() * 1000) << "\n"; - std::cout << "Nodes searched : " << nodes << "\n"; - std::cout << "Nodes/second : " << static_cast(nodes / elapsed.count()) << std::endl; -} -void handle_go(std::istringstream &iss) -{ - TimeControl tc; - bool white = (board.sideToMove() == chess::Color::WHITE); - std::string sub; - - while (iss >> sub) - { - if (sub == "depth") - iss >> tc.depth; - else if (sub == "movetime") - iss >> tc.movetime; - else if (sub == "wtime") - iss >> tc.wtime; - else if (sub == "btime") - iss >> tc.btime; - else if (sub == "winc") - iss >> tc.winc; - else if (sub == "binc") - iss >> tc.binc; - else if (sub == "movestogo") - iss >> tc.movestogo; - else if (sub == "infinite") - { - tc.infinite = true; - tc.depth = MAX_PLY; - } - } - tc.white_to_move=white; - if (search_thread.joinable()) - { - search::stop_requested = true; - search_thread.join(); - } - - search::stop_requested = false; - - search_thread = std::thread(search::run_search, board, tc); // keep joinable -} - -void handle_ucinewgame() -{ - board.setFen(chess::constants::STARTPOS); - search::tt.clear(); - search::tt.newSearch(); -} -void uci_loop() -{ - std::string line; - while (std::getline(std::cin, line)) - { - std::istringstream iss(line); - std::string token; - iss >> token; - if (token == "ucinewgame") - handle_ucinewgame(); - else if (token == "uci") - handle_uci(); - else if (token == "isready") - handle_isready(); - else if (token == "quit") - handle_quit(); - else if (token == "stop") - handle_stop(); - else if (token == "setoption") - handleSetOption(line); - else if (token == "position") - handle_position(iss); - else if (token == "go") - handle_go(iss); - else if (token == "d") - handle_display(); - else if (token == "bench") - handle_bench(); - else if (token == "eval") - handle_eval(); - else - std::cerr << "[DEBUG] Unknown command: " << token << "\n"; - } -} diff --git a/ucioptions.cpp b/ucioptions.cpp deleted file mode 100644 index 4a4f22c..0000000 --- a/ucioptions.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "ucioptions.hpp" -#include -#include -#include - -namespace UCIOptions -{ - - // Global options map - std::unordered_map options; - - // Constructors with callback support - Option::Option(Type t, int def, int min_, int max_, std::function cb) - : type(t), min(min_), max(max_), value(def), on_change(std::move(cb)) {} - - Option::Option(Type t, const std::string &def, std::function cb) - : type(t), default_str(def), value(def), on_change(std::move(cb)) {} - - Option::Option(Type t, const std::string &def, const std::vector &options, std::function cb) - : type(t), default_str(def), vars(options), value(def), on_change(std::move(cb)) {} - - // Adders with optional callbacks - void addSpin(const std::string &name, int defaultValue, int min, int max, std::function cb) - { - options[name] = Option(Option::SPIN, defaultValue, min, max, std::move(cb)); - if (options[name].on_change) - options[name].on_change(options[name]); - } - - void addCheck(const std::string &name, bool defaultValue, std::function cb) - { - options[name] = Option(Option::CHECK, defaultValue ? 1 : 0, 0, 1, std::move(cb)); - if (options[name].on_change) - options[name].on_change(options[name]); - } - - void addString(const std::string &name, const std::string &defaultValue, std::function cb) - { - options[name] = Option(Option::STRING, defaultValue, std::move(cb)); - if (options[name].on_change) - options[name].on_change(options[name]); - } - - void addCombo(const std::string &name, const std::string &defaultValue, const std::vector &vars, std::function cb) - { - options[name] = Option(Option::COMBO, defaultValue, vars, std::move(cb)); - if (options[name].on_change) - options[name].on_change(options[name]); - } - - void addButton(const std::string &name, std::function cb) - { - options[name] = Option(Option::BUTTON, "", {}, std::move(cb)); - if (options[name].on_change) - options[name].on_change(options[name]); - } - - void setOption(const std::string &name, const std::string &val) - { - const auto it = options.find(name); - if (it == options.end()) { - std::cerr << "info string Unknown option " << name << '\n'; - return; // Early-out – nothing to update - } - auto &opt = it->second; - - switch (opt.type) - { - case Option::CHECK: - if (val == "true" || val == "1") - opt.value = 1; - else if (val == "false" || val == "0") - opt.value = 0; - else { - std::cerr << "info string Invalid value for option " << name - << ": " << val << " (expected true/false or 1/0)\n"; - return; - } - break; - case Option::SPIN: - { - try - { - int v{}; - auto [p, ec] = std::from_chars(val.data(), val.data() + val.size(), v); - if (ec != std::errc() || p != val.data() + val.size()) { - throw std::invalid_argument("not an int"); - } - opt.value = std::clamp(v, opt.min, opt.max); - } - catch (const std::exception &e) - { - std::cerr << "info string Invalid value for option " << name << ": " << val << "\n"; - return; - } - break; - } - case Option::STRING: - opt.value = val; - break; - case Option::COMBO: - if (std::find(opt.vars.begin(), opt.vars.end(), val) != opt.vars.end()) { - opt.value = val; - } else { - std::cerr << "info string Illegal value \"" << val - << "\" for option " << name << "\n"; - return; - } - break; - case Option::BUTTON: - // no value change - break; - } - - // Invoke the callback if present - if (opt.on_change) - opt.on_change(opt); - } - - int getInt(const std::string &name) - { - const auto &opt = options.at(name); - if (auto pval = std::get_if(&opt.value)) - return *pval; - throw std::runtime_error("Option " + name + " is not an integer"); - } - - std::string getString(const std::string &name) - { - const auto &opt = options.at(name); - if (auto pval = std::get_if(&opt.value)) - return *pval; - if (auto pval = std::get_if(&opt.value)) - return std::to_string(*pval); - return ""; - } - - void printOptions() - { - for (const auto &[name, opt] : options) - { - std::cout << "option name " << name << " type "; - switch (opt.type) - { - case Option::CHECK: - std::cout << "check default " << (std::get(opt.value) ? "true" : "false") << "\n"; - break; - case Option::SPIN: - std::cout << "spin default " << std::get(opt.value) - << " min " << opt.min << " max " << opt.max << "\n"; - break; - case Option::STRING: - std::cout << "string default " << std::get(opt.value) << "\n"; - break; - case Option::COMBO: - std::cout << "combo default " << std::get(opt.value); - for (const auto &var : opt.vars) - std::cout << " var " << var; - std::cout << "\n"; - break; - case Option::BUTTON: - std::cout << "button\n"; - break; - } - } - } - -} // namespace UCIOptions diff --git a/ucioptions.hpp b/ucioptions.hpp deleted file mode 100644 index 79130bd..0000000 --- a/ucioptions.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace UCIOptions { - -struct Option { - enum Type { CHECK, SPIN, COMBO, BUTTON, STRING } type; - - std::string default_str; - std::vector vars; // for COMBO - int min = 0, max = 0; - - std::variant value; - - // Optional callback when the option is set - std::function on_change; - - Option() = default; - Option(Type t, int def, int min_ = 0, int max_ = 0, std::function cb = {}); - Option(Type t, const std::string& def, std::function cb = {}); - Option(Type t, const std::string& def, const std::vector& options, std::function cb = {}); -}; - -// Declare the global options map -extern std::unordered_map options; - -// Adders with optional callback support -void addSpin(const std::string& name, int defaultValue, int min, int max, std::function cb = {}); -void addCheck(const std::string& name, bool defaultValue, std::function cb = {}); -void addString(const std::string& name, const std::string& defaultValue, std::function cb = {}); -void addCombo(const std::string& name, const std::string& defaultValue, const std::vector& vars, std::function cb = {}); -void addButton(const std::string& name, std::function cb = {}); - -void setOption(const std::string& name, const std::string& val); - -int getInt(const std::string& name); -std::string getString(const std::string& name); - -void printOptions(); - -} // namespace UCIOptions