diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..5de674c --- /dev/null +++ b/.clang-format @@ -0,0 +1,16 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +ContinuationIndentWidth: 4 +ColumnLimit: 128 +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +BinPackParameters: false +BinPackArguments: false +BraceWrapping: +# AfterInitializerList: true + AfterFunction: true + BeforeElse: false + IndentBraces: false +AlignArrayOfStructures: Right +Cpp11BracedListStyle: false +SpacesInContainerLiterals: false diff --git a/.github/workflows/games.yml b/.github/workflows/games.yml index 0f04925..8a47060 100644 --- a/.github/workflows/games.yml +++ b/.github/workflows/games.yml @@ -6,19 +6,27 @@ on: base_ref: description: "Base branch, tag, or commit" required: false - default: "HandcraftedEngine" + default: "test_chesslib" rounds: description: "Max rounds for SPRT" required: false default: "6000" tc: - description: "Time control (e.g. 60+0.5 or 10+0.1)" + description: "Time control (e.g. 60+0.6 or 10+0.1)" required: false - default: "60+0.5" + default: "60+0.6" + elo0: + description: "elo0" + required: false + default: "0.5" + elo1: + description: "elo1" + required: false + default: "2.5" output_exec: description: "Executable output file name" required: false - default: "chess_engine" + default: "engine" permissions: contents: write @@ -31,14 +39,14 @@ jobs: steps: - uses: actions/checkout@v4 with: - path: test_chesslib + path: new - name: Install deps run: sudo apt update && sudo apt install -y build-essential cmake - name: Build new engine run: | - cd test_chesslib + cd new mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=Release @@ -48,7 +56,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: new-engine - path: test_chesslib/build/engine + path: new/build/engine build-base: @@ -58,14 +66,14 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.base_ref }} - path: handcrafted + path: base - name: Install deps run: sudo apt update && sudo apt install -y build-essential cmake - name: Build base engine run: | - cd handcrafted + cd base mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE=Release @@ -75,7 +83,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: base-engine - path: handcrafted/build/${{ github.event.inputs.output_exec }} + path: base/build/${{ github.event.inputs.output_exec }} test: @@ -115,7 +123,7 @@ jobs: -rounds ${{ github.event.inputs.rounds }} \ -concurrency $(nproc) \ -pgnout notation=san nodes=true file=games.pgn \ - -sprt elo0=0 elo1=2 alpha=0.05 beta=0.05 | tee results.txt + -sprt elo0=${{ github.event.inputs.elo0 }} elo1=${{ github.event.inputs.elo1 }} alpha=0.05 beta=0.05 | tee results.txt ./ordo-linux64 -o ratings.txt -- games.pgn sed -n '/Results of new vs base/,/^--------------------------------------------------$/p' results.txt >> ratings.txt @@ -127,3 +135,4 @@ jobs: path: | games.pgn ratings.txt + results.txt diff --git a/.gitignore b/.gitignore index ec84a85..a31ee69 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ !.gitignore !CMakeLists.txt !.github/ -!.github/** \ No newline at end of file +!.github/** +!.clang-format \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 3405d8e..c2792d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,9 +11,35 @@ FetchContent_Declare( GIT_REPOSITORY https://github.com/winapiadmin/chesslib.git GIT_TAG main ) -FetchContent_MakeAvailable(chesslib) -add_executable(engine "main.cpp" "timeman.cpp" "timeman.h" "eval.h" "eval.cpp" "tune.h" "ucioption.h" "tune.cpp" "ucioption.cpp" "tt.h" "tt.cpp" "uci.cpp" "uci.h" "search.h" "search.cpp" "score.h" "score.cpp" "movepick.h" "movepick.cpp") -target_link_libraries(engine PRIVATE chesslib) +FetchContent_Declare( + tbprobe + GIT_REPOSITORY https://github.com/winapiadmin/tb_probing_tool.git + GIT_TAG main +) +set(BUILD_GAVIOTA OFF CACHE BOOL "Enable Gaviota TB probing" FORCE) +option(ENGINE_TUNING "enable Texel tuning (requires csv-parser)" OFF) +FetchContent_MakeAvailable(chesslib tbprobe) +set(SOURCES + "main.cpp" + "timeman.cpp" + "eval.cpp" + "tune.cpp" + "ucioption.cpp" + "tt.cpp" + "uci.cpp" + "search.cpp" + "score.cpp" + "movepick.cpp" + "tb.cpp" +) + +if(ENGINE_TUNING) + list(APPEND SOURCES "tune_cmd.cpp") +endif() + +add_executable(engine ${SOURCES}) + +target_link_libraries(engine PRIVATE chesslib syzygy_probe) target_include_directories(engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${chesslib_SOURCE_DIR}) execute_process( COMMAND git rev-parse --short HEAD @@ -39,5 +65,14 @@ else() endif() message(STATUS "Build Version: ${BUILD_VERSION}") -add_definitions(-DBUILD_VERSION="${BUILD_VERSION}") - +target_compile_definitions(engine PRIVATE BUILD_VERSION="${BUILD_VERSION}") +if (ENGINE_TUNING) + FetchContent_Declare( + csv + GIT_REPOSITORY https://github.com/vincentlaucsb/csv-parser.git + GIT_TAG master + ) + FetchContent_MakeAvailable(csv) + target_link_libraries(engine PRIVATE csv) + target_compile_definitions(engine PRIVATE USE_CSV_PARSER) +endif() diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f3bd4fa --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +# :( openbench and fishtest require make +TARGET = engine + +CXX ?= g++ +CXXFLAGS ?= -std=c++17 -Wall -Wextra +OPTFLAGS ?= -O3 +ifeq ($(LTO), yes) + OPTFLAGS += -flto +endif +ifeq ($(debug),no) + CXXFLAGS += -DNDEBUG +endif +ARCH ?= native + +ifeq ($(ARCH), native) + CXXFLAGS += -march=native -mtune=native +endif +ifeq ($(ARCH), avx2) + CXXFLAGS += -march=haswell -mavx2 -mbmi -mbmi2 -msse4.1 +endif +ifeq ($(ARCH), bmi2) + CXXFLAGS += -mbmi2 +endif +ifeq ($(ARCH), sse41) + CXXFLAGS += -msse4.1 +endif +ifeq ($(ARCH), x86-64) + CXXFLAGS += -march=x86-64 +endif +deps: + test -d deps/chesslib || git clone https://github.com/winapiadmin/chesslib deps/chesslib + test -d deps/tbprobe || git clone https://github.com/winapiadmin/tb_probing_tool deps/tbprobe +# Tuning is not required on Makefile, use CMake. +SRCS = $(filter-out tune_cmd.cpp, $(wildcard *.cpp)) $(wildcard deps/chesslib/*.cpp) $(wildcard deps/tbprobe/*.cpp) +OBJS = $(SRCS:.cpp=.o) + +.PHONY: all clean deps +all: deps $(TARGET) + +$(TARGET): $(OBJS) + $(CXX) $(CXXFLAGS) $(OPTFLAGS) -o $(TARGET) $(OBJS) + +%.o: %.cpp + $(CXX) $(CXXFLAGS) $(OPTFLAGS) -Ideps/chesslib -Ideps/tbprobe/syzygy -c $< -o $@ + +clean: + rm -f $(OBJS) $(TARGET) diff --git a/Weights.h b/Weights.h new file mode 100644 index 0000000..b20dd65 --- /dev/null +++ b/Weights.h @@ -0,0 +1,139 @@ +#ifndef WEIGHTS_H +#define WEIGHTS_H +#include "eval.h" +namespace engine::eval { +inline Value tempo = 20; +inline Value PawnValue = 100; +inline Value KnightValue = 320; +inline Value BishopValue = 330; +inline Value RookValue = 500; +inline Value QueenValue = 900; +inline Value fianchettoBonus = 20; +inline Value trappedBishopPenalty = 60; +inline Value centerWeight = 5; +inline Value mopUpKingDistWeight = 5; +inline Value mopUpEdgeDistWeight = 10; +inline Value spaceWeight = 2; +inline Value bishopPairMg = 25; +inline Value bishopPairEg = 50; +inline Value rookOpenFileMg = 25; +inline Value rookOpenFileEg = 20; +inline Value rookSemiOpenFileMg = 15; +inline Value rookSemiOpenFileEg = 12; +inline Value doubledPawnMg = 10; +inline Value doubledPawnEg = 20; +inline Value isolatedPawnMg = 15; +inline Value isolatedPawnEg = 25; +inline Value kingShelterBaseMg = 20; +inline Value kingShelterBaseEg = 5; +inline Value kingShelterDecayMg = 4; +inline Value kingShelterDecayEg = 1; +inline Value kqkDistWeight = 15; +inline Value kqkEdgeWeight = 15; +inline Value krkDistWeight = 5; +inline Value krkEdgeWeight = 10; +inline Value kpkWeight = 15; +inline Value mgMobilityCnt[7][8] = { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { -15, -8, -2, 4, 10, 16, 22, 28 }, + { -20, -12, -4, 5, 14, 23, 32, 40 }, + { -25, -15, -5, 5, 15, 25, 35, 45 }, + { -30, -18, -6, 6, 18, 30, 42, 55 }, + { -10, -5, 0, 5, 10, 15, 20, 25 } +}; +inline Value egMobilityCnt[7][8] = { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { -20, -12, -4, 5, 14, 23, 32, 40 }, + { -25, -15, -5, 5, 16, 27, 38, 50 }, + { -30, -18, -6, 6, 18, 30, 42, 55 }, + { -35, -22, -8, 8, 22, 35, 48, 60 }, + { -15, -8, -2, 4, 10, 16, 22, 28 } +}; +inline Value kingTropismMg[7] = { 0, 0, 3, 2, 2, 5, 0 }; +inline Value kingTropismEg[7] = { 0, 0, 2, 2, 3, 4, 0 }; +inline Value passedBonusMg[7] = { 0, 5, 15, 40, 80, 140, 200 }; +inline Value passedBonusEg[7] = { 0, 10, 30, 70, 140, 230, 350 }; +inline Value mg_knight_table[64] = { -50, -40, -30, -30, -30, -30, -40, -50, -40, -20, 0, 5, 5, 0, -20, -40, + -30, 5, 10, 15, 15, 10, 5, -30, -30, 0, 15, 20, 20, 15, 0, -30, + -30, 5, 15, 20, 20, 15, 5, -30, -30, 0, 10, 15, 15, 10, 0, -30, + -40, -20, 0, 0, 0, 0, -20, -40, -50, -40, -30, -30, -30, -30, -40, -50 }; +inline Value mg_bishop_table[64] = { -20, -10, -10, -10, -10, -10, -10, -20, -10, 0, 0, 0, 0, 0, 0, -10, + -10, 0, 10, 10, 10, 10, 0, -10, -10, 0, 5, 10, 10, 5, 0, -10, + -10, 5, 5, 10, 10, 5, 5, -10, -10, 0, 10, 10, 10, 10, 0, -10, + -10, 0, 0, 0, 0, 0, 0, -10, -20, -10, -10, -10, -10, -10, -10, -20 }; +inline Value mg_rook_table[64] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, + 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, + 5, 5, 5, 0, 10, 15, 15, 15, 15, 15, 15, 10, 0, 0, 0, 0, 0, 0, 0, 0 }; +inline Value mg_king_table[64] = { -30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, + -30, -40, -40, -50, -50, -40, -40, -30, -30, -40, -40, -50, -50, -40, -40, -30, + -20, -30, -30, -40, -40, -30, -30, -20, -10, -20, -20, -20, -20, -20, -20, -10, + 20, 20, 0, 0, 0, 0, 20, 20, 20, 30, 10, 0, 0, 10, 30, 20 }; +inline Value mg_queen_table[64] = { -20, -10, -10, -5, -5, -10, -10, -20, -10, 0, 5, 0, 0, 0, 0, -10, + -10, 5, 5, 5, 5, 5, 0, -10, 0, 0, 5, 5, 5, 5, 0, -5, + -5, 0, 5, 5, 5, 5, 0, -5, -10, 0, 5, 5, 5, 5, 0, -10, + -10, 0, 0, 0, 0, 0, 0, -10, -20, -10, -10, -5, -5, -10, -10, -20 }; +inline Value eg_knight_table[64] = { -50, -40, -20, -20, -20, -20, -40, -50, -40, -10, 5, 10, 10, 5, -10, -40, + -30, 10, 20, 25, 25, 20, 10, -30, -30, 15, 25, 30, 30, 25, 15, -30, + -30, 15, 25, 30, 30, 25, 15, -30, -30, 10, 20, 25, 25, 20, 10, -30, + -40, -10, 5, 10, 10, 5, -10, -40, -50, -40, -20, -20, -20, -20, -40, -50 }; +inline Value eg_bishop_table[64] = { -10, -5, -5, -5, -5, -5, -5, -10, -5, 5, 10, 10, 10, 10, 5, -5, -5, 10, 15, 20, 20, 15, + 10, -5, -5, 10, 20, 25, 25, 20, 10, -5, -5, 10, 20, 25, 25, 20, 10, -5, -5, 10, 15, 20, + 20, 15, 10, -5, -5, 5, 10, 10, 10, 10, 5, -5, -10, -5, -5, -5, -5, -5, -5, -10 }; +inline Value eg_rook_table[64] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 10, 15, 15, 10, 5, 0, 0, 5, 10, 15, 15, 10, + 5, 0, 0, 5, 10, 15, 15, 10, 5, 0, 10, 15, 20, 25, 25, 20, 15, 10, 10, 15, 20, 25, + 25, 20, 15, 10, 20, 25, 30, 35, 35, 30, 25, 20, 0, 0, 0, 5, 5, 0, 0, 0 }; +inline Value eg_king_table[64] = { -50, -40, -30, -20, -20, -30, -40, -50, -40, -20, 0, 10, 10, 0, -20, -40, + -30, 0, 20, 30, 30, 20, 0, -30, -20, 10, 30, 40, 40, 30, 10, -20, + -20, 10, 30, 40, 40, 30, 10, -20, -30, 0, 20, 30, 30, 20, 0, -30, + -40, -20, 0, 10, 10, 0, -20, -40, -50, -40, -30, -20, -20, -30, -40, -50 }; +inline Value eg_queen_table[64] = { -10, -5, -5, 0, 0, -5, -5, -10, -5, 0, 5, 5, 5, 5, 0, -5, -5, 5, 10, 15, 15, 10, + 5, -5, -5, 10, 15, 20, 20, 15, 10, -5, -5, 10, 15, 20, 20, 15, 10, -5, -5, 5, 10, 15, + 15, 10, 5, -5, -5, 0, 5, 5, 5, 5, 0, -5, -10, -5, -5, 0, 0, -5, -5, -10 }; +inline Value mg_pawn_table[56] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -2, 2, + 5, 5, 2, -2, -5, -10, -5, 10, 20, 20, 10, -5, -10, -20, -10, 25, 40, 40, 25, + -10, -20, -35, -20, 50, 80, 80, 50, -20, -35, -55, -40, 90, 130, 130, 90, -40, -55 }; +inline Value eg_pawn_table[56] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -2, 2, + 5, 5, 2, -2, -5, -10, -5, 10, 20, 20, 10, -5, -10, -20, -10, 30, 55, 55, 30, + -10, -20, -35, -20, 70, 110, 110, 70, -20, -35, -60, -45, 120, 180, 180, 120, -45, -60 }; +inline Value developedMg = 8; +inline Value developedEg = 4; +inline Value outpostBonusKnight[2] = { 15, 30 }; +inline Value outpostBonusBishop[2] = { 10, 25 }; +inline Value kingProtector[6][2] = { + { 0, 0 }, + { 8, 12 }, + { 5, 10 }, + { 10, 15 }, + { 3, 5 }, + { 0, 0 } +}; +inline Value threatByMinor[7][2] = { + { 0, 0 }, + { 0, 0 }, + { 15, 25 }, + { 15, 25 }, + { 25, 35 }, + { 35, 50 } +}; +inline Value threatByRook[7][2] = { + { 0, 0 }, + { 0, 0 }, + { 10, 15 }, + { 10, 15 }, + { 20, 25 }, + { 25, 35 } +}; +inline Value hangingScore = 70; +inline Value overloadScore = 25; +inline Value threatByRankScore = 10; +inline Value minorImWt = 30; +inline Value bishopImWt = 15; +inline Value rookImWt = 20; +inline Value queenImWt = 50; +inline Value rammedPawnPenalty = 10; +inline Value rookOnSeventhBonus = 30; +inline Value earlyQueenPenalty = 5; +} // namespace engine::eval +#endif diff --git a/eval.cpp b/eval.cpp index 14623ba..e192ed5 100644 --- a/eval.cpp +++ b/eval.cpp @@ -1,156 +1,730 @@ #include "eval.h" +#include "Weights.h" #include "tune.h" +#include #include using namespace chess; using namespace engine::eval; + namespace engine::eval { -Value PawnValue = 100, KnightValue = 325, BishopValue = 350, RookValue = 500, - QueenValue = 900; -Value 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, +static Bitboard passedMask[2][64]; +struct PassedMaskInit { + PassedMaskInit() { + for (int sq = 0; sq < 64; sq++) { + File f = file_of(Square(sq)); + Rank r = rank_of(Square(sq)); + for (int adjF = std::max(0, (int)f - 1); adjF <= std::min(7, (int)f + 1); adjF++) { + for (int r2 = (int)r + 1; r2 <= 7; r2++) + passedMask[WHITE][sq] |= attacks::MASK_FILE[adjF] & attacks::MASK_RANK[r2]; + for (int r2 = (int)r - 1; r2 >= 0; r2--) + passedMask[BLACK][sq] |= attacks::MASK_FILE[adjF] & attacks::MASK_RANK[r2]; + } + } + } }; +static PassedMaskInit passedMaskInit; -Value 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, -}; +Value *mgPst[] = { nullptr, mg_pawn_table, mg_knight_table, mg_bishop_table, mg_rook_table, mg_queen_table, mg_king_table }; -Value 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, -}; +Value *egPst[] = { nullptr, eg_pawn_table, eg_knight_table, eg_bishop_table, eg_rook_table, eg_queen_table, eg_king_table }; -Value 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, -}; +// tuning slop here +/* +TUNE(SetRange(5, 30), + tempo, + SetRange(80, 120), + PawnValue, + SetRange(280, 370), + KnightValue, + SetRange(300, 400), + BishopValue, + SetRange(450, 550), + RookValue, + SetRange(800, 1000), + QueenValue); +TUNE(SetRange(-50, 70), mgMobilityCnt, egMobilityCnt); +TUNE(SetRange(0, 30), fianchettoBonus, SetRange(0, 100), trappedBishopPenalty); +TUNE(SetRange(-20, 20), kingTropismMg, kingTropismEg); +TUNE(SetRange(0, 30), + centerWeight, + SetRange(0, 20), + mopUpKingDistWeight, + SetRange(0, 20), + mopUpEdgeDistWeight, + SetRange(1, 30), + spaceWeight); +TUNE(SetRange(0, 50), bishopPairMg, SetRange(0, 50), bishopPairEg); +TUNE(SetRange(1, 20), developedMg, SetRange(1, 20), developedEg); +TUNE(SetRange(0, 30), + rookOpenFileMg, + SetRange(0, 30), + rookOpenFileEg, + SetRange(0, 20), + rookSemiOpenFileMg, + SetRange(0, 20), + rookSemiOpenFileEg); +TUNE(SetRange(0, 30), + doubledPawnMg, + SetRange(0, 30), + doubledPawnEg, + SetRange(0, 40), + isolatedPawnMg, + SetRange(0, 40), + isolatedPawnEg); +TUNE(SetRange(0, 400), + passedBonusMg[1], + passedBonusMg[2], + passedBonusMg[3], + passedBonusMg[4], + passedBonusMg[5], + passedBonusMg[6], + passedBonusEg[1], + passedBonusEg[2], + passedBonusEg[3], + passedBonusEg[4], + passedBonusEg[5], + passedBonusEg[6]); +TUNE(SetRange(1, 30), + kingShelterBaseMg, + SetRange(1, 30), + kingShelterBaseEg, + SetRange(1, 10), + kingShelterDecayMg, + SetRange(1, 10), + kingShelterDecayEg); +TUNE(SetRange(-70, 200), + mg_knight_table, + mg_bishop_table, + mg_rook_table, + mg_king_table, + mg_queen_table, + eg_knight_table, + eg_bishop_table, + eg_rook_table, + eg_king_table, + eg_queen_table, + mg_pawn_table[8], + eg_pawn_table[8], + mg_pawn_table[9], + eg_pawn_table[9], + mg_pawn_table[10], + eg_pawn_table[10], + mg_pawn_table[11], + eg_pawn_table[11], + mg_pawn_table[12], + eg_pawn_table[12], + mg_pawn_table[13], + eg_pawn_table[13], + mg_pawn_table[14], + eg_pawn_table[14], + mg_pawn_table[15], + eg_pawn_table[15], + mg_pawn_table[16], + eg_pawn_table[16], + mg_pawn_table[17], + eg_pawn_table[17], + mg_pawn_table[18], + eg_pawn_table[18], + mg_pawn_table[19], + eg_pawn_table[19], + mg_pawn_table[20], + eg_pawn_table[20], + mg_pawn_table[21], + eg_pawn_table[21], + mg_pawn_table[22], + eg_pawn_table[22], + mg_pawn_table[23], + eg_pawn_table[23], + mg_pawn_table[24], + eg_pawn_table[24], + mg_pawn_table[25], + eg_pawn_table[25], + mg_pawn_table[26], + eg_pawn_table[26], + mg_pawn_table[27], + eg_pawn_table[27], + mg_pawn_table[28], + eg_pawn_table[28], + mg_pawn_table[29], + eg_pawn_table[29], + mg_pawn_table[30], + eg_pawn_table[30], + mg_pawn_table[31], + eg_pawn_table[31], + mg_pawn_table[32], + eg_pawn_table[32], + mg_pawn_table[33], + eg_pawn_table[33], + mg_pawn_table[34], + eg_pawn_table[34], + mg_pawn_table[35], + eg_pawn_table[35], + mg_pawn_table[36], + eg_pawn_table[36], + mg_pawn_table[37], + eg_pawn_table[37], + mg_pawn_table[38], + eg_pawn_table[38], + mg_pawn_table[39], + eg_pawn_table[39], + mg_pawn_table[40], + eg_pawn_table[40], + mg_pawn_table[41], + eg_pawn_table[41], + mg_pawn_table[42], + eg_pawn_table[42], + mg_pawn_table[43], + eg_pawn_table[43], + mg_pawn_table[44], + eg_pawn_table[44], + mg_pawn_table[45], + eg_pawn_table[45], + mg_pawn_table[46], + eg_pawn_table[46], + mg_pawn_table[47], + eg_pawn_table[47], + mg_pawn_table[48], + eg_pawn_table[48], + mg_pawn_table[49], + eg_pawn_table[49], + mg_pawn_table[50], + eg_pawn_table[50], + mg_pawn_table[51], + eg_pawn_table[51], + mg_pawn_table[52], + eg_pawn_table[52], + mg_pawn_table[53], + eg_pawn_table[53], + mg_pawn_table[54], + eg_pawn_table[54], + mg_pawn_table[55], + eg_pawn_table[55]); +TUNE(SetRange(0, 30), + kqkDistWeight, + SetRange(0, 20), + kqkEdgeWeight, + SetRange(0, 20), + krkDistWeight, + SetRange(0, 20), + krkEdgeWeight, + SetRange(0, 30), + kpkWeight); -Value 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, -}; +TUNE(SetRange(1, 100), outpostBonusKnight, outpostBonusBishop); +TUNE(SetRange(-20, 20), kingProtector); +TUNE(SetRange(-50, 100), + threatByMinor[1], + threatByMinor[2], + threatByMinor[3], + threatByMinor[4], + threatByMinor[5], + threatByMinor[6]); +TUNE(SetRange(-50, 100), threatByRook[1], threatByRook[2], threatByRook[3], threatByRook[4], threatByRook[5], threatByRook[6]); +TUNE(SetRange(1, 100), hangingScore, overloadScore, threatByRankScore); +TUNE(SetRange(10, 100), minorImWt, SetRange(5, 50), bishopImWt, SetRange(10, 100), rookImWt, SetRange(20, 150), queenImWt); +TUNE(SetRange(1, 50), rammedPawnPenalty); +TUNE(SetRange(1, 100), rookOnSeventhBonus); +TUNE(SetRange(1, 50), earlyQueenPenalty); +*/ +EvalComponents eval_components(const chess::Position &board) { + 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 mgScore = 0; + int egScore = 0; + int phase = 0; + // Precompute pawn attacks (needed for outpost, threats, etc.) + Bitboard pawnBB[2] = { board.pieces(PAWN, WHITE), board.pieces(PAWN, BLACK) }; + Bitboard pawnAtks[2] = { attacks::pawn(pawnBB[WHITE]), attacks::pawn(pawnBB[BLACK]) }; +#if 0 + // Development bonus: penalize undeveloped knights/bishops in middlegame + int devCount[2] = { 0, 0 }; + for (Color c : { WHITE, BLACK }) { + Bitboard homeRank = c == WHITE ? attacks::MASK_RANK[0] : attacks::MASK_RANK[7]; + Bitboard knights = board.pieces(KNIGHT, c); + Bitboard bishops = board.pieces(BISHOP, c); + Bitboard knightsHome = knights & homeRank; + Bitboard bishopsHome = bishops & homeRank; + devCount[c] = popcount(knightsHome) + popcount(bishopsHome); + } + mgScore += (devCount[BLACK] - devCount[WHITE]) * developedMg; + egScore += (devCount[BLACK] - devCount[WHITE]) * developedEg; -Value 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, -}; + // Early queen development penalty: queen moved but minors still on back rank + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Square qStart = c == WHITE ? SQ_D1 : SQ_D8; + if (!(board.pieces(QUEEN, c) & (1ULL << qStart))) { + Bitboard backRank = c == WHITE ? attacks::MASK_RANK[0] : attacks::MASK_RANK[7]; + int undeveloped = popcount((board.pieces(KNIGHT, c) | board.pieces(BISHOP, c)) & backRank); + if (undeveloped >= 2) { + mgScore -= s * earlyQueenPenalty; + //disabled intentionally in endgames + //egScore -= s * earlyQueenPenalty; + } + } + } +#endif + { + Bitboard pinMask = board.pin_mask(); + Bitboard occ = board.occ(), occ2 = occ; + while (occ) { + Square sq = Square(pop_lsb(occ)); + auto p = board.at(sq); + Color pc = color_of(p); + int _sign = pc == WHITE ? 1 : -1; + Square _sq = _sign == 1 ? sq : square_mirror(sq); + auto pt = piece_of(p); + if (pt == NO_PIECE_TYPE) + continue; + mgScore += _sign * mgPst[pt][_sq]; + egScore += _sign * egPst[pt][_sq]; + mgScore += _sign * piece_value(pt); + egScore += _sign * piece_value(pt); + if (pt == KNIGHT) + phase += KnightPhase; + else if (pt == BISHOP) + phase += BishopPhase; + else if (pt == ROOK) + phase += RookPhase; + else if (pt == QUEEN) + phase += QueenPhase; + Bitboard attacks = 0; + if (pt == KNIGHT) + attacks = chess::attacks::knight(sq); + else if (pt == BISHOP) + attacks = chess::attacks::bishop(sq, occ2); + else if (pt == ROOK) + attacks = chess::attacks::rook(sq, occ2); + else if (pt == QUEEN) + attacks = chess::attacks::queen(sq, occ2); + attacks &= ~board.us(pc); + int mobility = popcount(attacks); + int clampedMobility = std::min(mobility, 7); + if ((1ULL << sq) & pinMask) + clampedMobility = std::min(clampedMobility / 4, 7); + mgScore += _sign * mgMobilityCnt[pt][clampedMobility]; + egScore += _sign * egMobilityCnt[pt][clampedMobility]; + if (pt != PAWN && pt != KING) { + int kd = square_distance(sq, board.kingSq(~pc)); + mgScore += _sign * (7 - kd) * kingTropismMg[pt]; + egScore += _sign * (7 - kd) * kingTropismEg[pt]; + } +#if 0 + // King protector: bonus for pieces close to own king + if (pt != PAWN && pt != KING) { + int kdist = square_distance(sq, board.kingSq(pc)); + int ki = std::min(kdist, 5); + mgScore += _sign * kingProtector[pt][0] / (1 + ki); + egScore += _sign * kingProtector[pt][1] / (1 + ki); + } + // Outpost bonus for knights and bishops + if (pt == KNIGHT || pt == BISHOP) { + Rank r = rank_of(sq); + bool onOutpost = (pc == WHITE && r >= RANK_5) || (pc == BLACK && r <= RANK_4); + if (onOutpost && !(pawnAtks[~pc] & (1ULL << sq))) { + bool defended = (pawnAtks[pc] & (1ULL << sq)) != 0; + int idx = defended ? 1 : 0; + if (pt == KNIGHT) { + mgScore += _sign * outpostBonusKnight[idx]; + egScore += _sign * outpostBonusKnight[idx]; + } else { + mgScore += _sign * outpostBonusBishop[idx]; + egScore += _sign * outpostBonusBishop[idx]; + } + } + } +#endif + } + } + // Bishop pair bonus + for (Color c : { WHITE, BLACK }) { + if (board.count(BISHOP, c) >= 2) { + int s = (c == WHITE) ? 1 : -1; + mgScore += s * bishopPairMg; + egScore += s * bishopPairEg; + } + } -Value 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, -}; + // Fianchetto bonus + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Square fianchettoSq[2] = { relative_square(c, SQ_B2), relative_square(c, SQ_G2) }; + Square pawnSq[2] = { relative_square(c, SQ_B3), relative_square(c, SQ_G3) }; + for (int i = 0; i < 2; i++) { + if (board.at(fianchettoSq[i]) == BISHOP && board.at(fianchettoSq[i]) == c && + board.at(pawnSq[i]) == PAWN && board.at(pawnSq[i]) == c) { + mgScore += s * fianchettoBonus; + egScore += s * fianchettoBonus; + } + } + } +#if 0 + // Trapped bishop penalty + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Square trapSq[2] = { relative_square(c, SQ_A2), relative_square(c, SQ_H2) }; + Square kingAdj[2] = { relative_square(c, SQ_B1), relative_square(c, SQ_G1) }; + for (int i = 0; i < 2; i++) { + if (board.at(trapSq[i]) == BISHOP && board.at(trapSq[i]) == c && board.kingSq(c) == kingAdj[i]) { + mgScore -= s * trappedBishopPenalty; + egScore -= s * trappedBishopPenalty; + } + } + } +#endif + // Rook on open/semi-open file + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Bitboard rooks = board.pieces(ROOK, c); + while (rooks) { + Square sq = Square(pop_lsb(rooks)); + File f = file_of(sq); + Bitboard fileMask = attacks::MASK_FILE[f]; + bool hasOwnPawn = (board.pieces(PAWN, c) & fileMask) != 0; + bool hasEnemyPawn = (board.pieces(PAWN, ~c) & fileMask) != 0; + if (!hasOwnPawn && !hasEnemyPawn) { + mgScore += s * rookOpenFileMg; + egScore += s * rookOpenFileEg; + } else if (!hasOwnPawn) { + mgScore += s * rookSemiOpenFileMg; + egScore += s * rookSemiOpenFileEg; + } + } + } +#if 0 + // Rook on seventh rank bonus (endgame, x-raying >=2 undefended pawns) + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Bitboard rooks = board.pieces(ROOK, c); + while (rooks) { + Square sq = Square(pop_lsb(rooks)); + Rank r7 = c == WHITE ? RANK_7 : RANK_2; + if (rank_of(sq) != r7) + continue; + Bitboard ep = board.pieces(PAWN, ~c) & attacks::MASK_RANK[r7]; + int cnt = 0; + Bitboard tmp = ep; + while (tmp) { + Square psq = Square(pop_lsb(tmp)); + if (!board.is_attacked_by(~c, psq)) + cnt++; + } + if (cnt >= 2) { + mgScore += s * rookOnSeventhBonus / 2; + egScore += s * rookOnSeventhBonus; + } + } + } +#endif + // Pawn structure: doubled, isolated, passed in one pass per color + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Bitboard pawns = pawnBB[c]; + Bitboard enemyPawns = pawnBB[~c]; -Value 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, -}; + bool fileHasPawn[8] = { false }; + int fileCount[8] = { 0 }; + Bitboard tmp = pawns; + while (tmp) { + Square sq = Square(pop_lsb(tmp)); + File f = file_of(sq); + fileCount[f]++; + fileHasPawn[f] = true; + } -Value 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, -}; + for (int f = 0; f < 8; f++) + if (fileCount[f] >= 2) { + mgScore -= s * (fileCount[f] - 1) * doubledPawnMg; + egScore -= s * (fileCount[f] - 1) * doubledPawnEg; + } -Value 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, -}; + tmp = pawns; + while (tmp) { + Square sq = Square(pop_lsb(tmp)); + File f = file_of(sq); + Rank relRank = relative_rank(c, sq); -Value 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, -}; + bool isolated = true; + if (f > FILE_A && fileHasPawn[f - 1]) + isolated = false; + if (f < FILE_H && fileHasPawn[f + 1]) + isolated = false; + if (isolated) { + mgScore -= s * isolatedPawnMg; + egScore -= s * isolatedPawnEg; + } + + if (relRank >= RANK_2 && (passedMask[c][sq] & enemyPawns) == 0) { + mgScore += s * passedBonusMg[relRank]; + egScore += s * passedBonusEg[relRank]; + } + } + } +#if 0 + // Pawn rams: blocked pawn penalty + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Bitboard pawns = pawnBB[c]; + while (pawns) { + Square sq = Square(pop_lsb(pawns)); + Square forward = sq + pawn_push(c); + if (forward >= SQ_A1 && forward <= SQ_H8 && (pawnBB[~c] & (1ULL << forward))) { + mgScore -= s * rammedPawnPenalty; + egScore -= s * rammedPawnPenalty; + } + } + } +#endif + // Center control (using precomputed pawn attacks) + { + Bitboard centerMask = (1ULL << SQ_D4) | (1ULL << SQ_E4) | (1ULL << SQ_D5) | (1ULL << SQ_E5); + int wc = popcount(pawnAtks[WHITE] & centerMask); + int bc = popcount(pawnAtks[BLACK] & centerMask); + mgScore += (wc - bc) * centerWeight; + egScore += (wc - bc) * centerWeight; + } + + // Space evaluation + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Bitboard safe = ~pawnAtks[~c]; + Bitboard enemyCamp = c == WHITE ? (attacks::MASK_RANK[4] | attacks::MASK_RANK[5] | attacks::MASK_RANK[6]) + : (attacks::MASK_RANK[3] | attacks::MASK_RANK[2] | attacks::MASK_RANK[1]); + int space = popcount(pawnAtks[c] & safe & enemyCamp); + mgScore += s * space * spaceWeight; + egScore += s * space * spaceWeight; + } -Value 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}; + // King safety: pawn shelter + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Square kingSq = board.kingSq(c); + File kf = file_of(kingSq); + Rank kr = rank_of(kingSq); + Bitboard pawns = board.pieces(PAWN, c); + int shelterMg = 0, shelterEg = 0; + int startF = std::max(0, (int)kf - 1); + int endF = std::min(7, (int)kf + 1); + for (int adjF = startF; adjF <= endF; adjF++) { + Bitboard fileMask = attacks::MASK_FILE[adjF]; + if (c == WHITE) { + for (int r = (int)kr + 1; r <= std::min(7, (int)kr + 3); r++) { + if (pawns & (fileMask & attacks::MASK_RANK[r])) { + shelterMg += kingShelterBaseMg - (r - (int)kr - 1) * kingShelterDecayMg; + shelterEg += kingShelterBaseEg - (r - (int)kr - 1) * kingShelterDecayEg; + } + } + } else { + for (int r = (int)kr - 1; r >= std::max(0, (int)kr - 3); r--) { + if (pawns & (fileMask & attacks::MASK_RANK[r])) { + shelterMg += kingShelterBaseMg - ((int)kr - r - 1) * kingShelterDecayMg; + shelterEg += kingShelterBaseEg - ((int)kr - r - 1) * kingShelterDecayEg; + } + } + } + } + mgScore += s * shelterMg; + egScore += s * shelterEg; + } -Value *mg_pesto_table[] = {nullptr, mg_pawn_table, mg_knight_table, - mg_bishop_table, mg_rook_table, mg_queen_table, - mg_king_table}; + // Endgame bonuses + mop-up in one pass per color + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + int oppCount = popcount(board.occ(~c)); + Square myKing = board.kingSq(c); + Square oppKing = board.kingSq(~c); -Value *eg_pesto_table[] = {nullptr, eg_pawn_table, eg_knight_table, - eg_bishop_table, eg_rook_table, eg_queen_table, - eg_king_table}; -// tuning slop here -Value eval(const chess::Board &board) { - int pieceCount[10] = { - board.count(), board.count(), - board.count(), board.count(), - board.count(), board.count(), - board.count(), board.count(), - board.count(), board.count(), - }; - Value material = (pieceCount[0] * PawnValue + pieceCount[1] * KnightValue + - pieceCount[2] * BishopValue + pieceCount[3] * RookValue + - pieceCount[4] * QueenValue) - - (pieceCount[5] * PawnValue + pieceCount[6] * KnightValue + - pieceCount[7] * BishopValue + pieceCount[8] * RookValue + - pieceCount[9] * QueenValue); - 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 = (pieceCount[1] + pieceCount[6]) * KnightPhase + - (pieceCount[2] + pieceCount[7]) * BishopPhase + - (pieceCount[3] + pieceCount[8]) * RookPhase + - (pieceCount[4] + pieceCount[9]) * QueenPhase; - phase = (phase * 256 + TotalPhase / 2) / TotalPhase; - const int sign = board.side_to_move() == chess::Color::WHITE ? 1 : -1; - int mgScore = material; - int egScore = material; - { - Bitboard occ = board.occ(); - while (occ) { - Square i = (Square)pop_lsb(occ); - auto p = board.at(i); - int _sign = 1; - if (color_of(p) == BLACK) { - _sign = -1; - i = square_mirror(i); - } - mgScore += _sign * mg_pesto_table[piece_of(p)][i]; - egScore += _sign * eg_pesto_table[piece_of(p)][i]; - } - } - Value finalScore = - ((mgScore * phase) + (egScore * (256 - phase))) / 256 * sign; - return finalScore; + // KQK: queen vs lone king + if (board.count(QUEEN, c) >= 1 && oppCount == 1) { + int kingDist = std::max(std::abs((int)file_of(myKing) - (int)file_of(oppKing)), + std::abs((int)rank_of(myKing) - (int)rank_of(oppKing))); + File ef = file_of(oppKing); + Rank er = rank_of(oppKing); + int edgeDist = std::min(std::min((int)ef, 7 - (int)ef), std::min((int)er, 7 - (int)er)); + mgScore += s * ((14 - kingDist) * kqkDistWeight + (7 - edgeDist) * kqkEdgeWeight); + egScore += s * ((14 - kingDist) * kqkDistWeight + (7 - edgeDist) * kqkEdgeWeight); + } + + // KRK: rook vs lone king + if (board.count(ROOK, c) >= 1 && oppCount == 1 && board.count(QUEEN, c) == 0 && board.count(BISHOP, c) == 0 && + board.count(KNIGHT, c) == 0 && board.count(PAWN, c) == 0) { + int kingDist = std::max(std::abs((int)file_of(myKing) - (int)file_of(oppKing)), + std::abs((int)rank_of(myKing) - (int)rank_of(oppKing))); + File ef = file_of(oppKing); + Rank er = rank_of(oppKing); + int edgeDist = std::min(std::min((int)ef, 7 - (int)ef), std::min((int)er, 7 - (int)er)); + mgScore += s * ((14 - kingDist) * krkDistWeight + (7 - edgeDist) * krkEdgeWeight); + egScore += s * ((14 - kingDist) * krkDistWeight + (7 - edgeDist) * krkEdgeWeight); + } + + // KPK: king + pawn(s) vs lone king + if (board.count(PAWN, c) >= 1 && oppCount == 1 && board.count(QUEEN, c) == 0 && board.count(ROOK, c) == 0 && + board.count(BISHOP, c) == 0 && board.count(KNIGHT, c) == 0) { + Bitboard ps = pawnBB[c]; + while (ps) { + Square psq = Square(pop_lsb(ps)); + Rank pr = relative_rank(c, psq); + if (pr < RANK_4) + continue; + + File pf = file_of(psq); + Square promSq = make_sq(pf, c == WHITE ? RANK_8 : RANK_1); + int pawnDist = (c == WHITE) ? (7 - (int)rank_of(psq)) : (int)rank_of(psq); + int pkDist = + std::max(std::abs((int)file_of(oppKing) - (int)pf), std::abs((int)rank_of(oppKing) - (int)rank_of(promSq))); + int mkDist = std::max(std::abs((int)file_of(myKing) - (int)file_of(psq)), + std::abs((int)rank_of(myKing) - (int)rank_of(psq))); + + bool winning = pkDist > pawnDist || mkDist <= pawnDist + 1 || + (pr >= RANK_6 && file_of(myKing) == pf && + ((c == WHITE && rank_of(myKing) >= RANK_6) || (c == BLACK && rank_of(myKing) <= RANK_3))); + if ((pf == FILE_A || pf == FILE_H) && oppKing == promSq) + winning = false; + + if (winning) { + mgScore += s * kpkWeight * pawnDist; + egScore += s * kpkWeight * pawnDist; + } + } + } + + // Mop-up: general endgame drive + Bitboard ourMat = board.occ(c) & ~board.pieces(PAWN) & ~board.pieces(KING); + Bitboard theirMat = board.occ(~c) & ~board.pieces(PAWN) & ~board.pieces(KING); + if (ourMat && !theirMat && oppCount <= 2) { + int kingDist = std::max(std::abs((int)file_of(myKing) - (int)file_of(oppKing)), + std::abs((int)rank_of(myKing) - (int)rank_of(oppKing))); + File ef = file_of(oppKing); + Rank er = rank_of(oppKing); + int edgeDist = std::min(std::min((int)ef, 7 - (int)ef), std::min((int)er, 7 - (int)er)); + mgScore += s * ((14 - kingDist) * mopUpKingDistWeight + (7 - edgeDist) * mopUpEdgeDistWeight); + egScore += s * ((14 - kingDist) * mopUpKingDistWeight + (7 - edgeDist) * mopUpEdgeDistWeight); + } + } + +// --- Threat evaluation --- +#if 0 + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Color opp = ~c; + Bitboard ourPieces = board.occ(c) & ~board.pieces(PAWN) & ~board.pieces(KING); + while (ourPieces) { + Square sq = Square(pop_lsb(ourPieces)); + PieceType pt = piece_of(board.at(sq)); + bool attacked = board.is_attacked_by(opp, sq); + if (!attacked) + continue; + bool defended = board.is_attacked_by(c, sq); + Bitboard occ = board.occ(); + int attackers = popcount(board.attackers_mask(opp, sq, occ)); + int defenders = defended ? popcount(board.attackers_mask(c, sq, occ)) : 0; + bool hanging = !defended; + bool overloaded = defenders == 1 && attackers >= 2; + bool attackedByMinor = + (board.attackers_mask(opp, sq, occ) & (board.pieces(KNIGHT, opp) | board.pieces(BISHOP, opp))) != 0; + bool attackedByRook = (board.attackers_mask(opp, sq, occ) & board.pieces(ROOK, opp)) != 0; + + int mgThreat = 0, egThreat = 0; + if (hanging) { + mgThreat += (pt == QUEEN ? hangingScore / 2 : hangingScore); + egThreat += (pt == QUEEN ? hangingScore / 4 : hangingScore / 2); + } + if (overloaded) { + mgThreat += overloadScore; + egThreat += overloadScore / 2; + } + if (attackedByMinor && pt < KING) { + mgThreat += threatByMinor[pt][0]; + egThreat += threatByMinor[pt][1]; + } + if (attackedByRook && pt < KING) { + mgThreat += threatByRook[pt][0]; + egThreat += threatByRook[pt][1]; + } + // Rank-based threat bonus + Rank relRank = relative_rank(c, sq); + mgThreat += threatByRankScore * static_cast(relRank); + egThreat += threatByRankScore * static_cast(relRank); + + mgScore -= s * mgThreat; + egScore -= s * egThreat; + } + } + + // --- Trapped bishop at a7/h7 --- + for (Color c : { WHITE, BLACK }) { + int s = (c == WHITE) ? 1 : -1; + Color opp = ~c; + Square aFileSq = relative_square(c, SQ_A7); + Square hFileSq = relative_square(c, SQ_H7); + Square bPawnSq = relative_square(c, SQ_B6); + Square gPawnSq = relative_square(c, SQ_G6); + // Bishop on a7/h7 trapped by b6/g6 pawn + if (board.at(aFileSq) == BISHOP && board.at(aFileSq) == c) { + if (board.at(bPawnSq) == PAWN && board.at(bPawnSq) == opp && !board.is_attacked_by(c, aFileSq)) { + mgScore -= s * trappedBishopPenalty; + egScore -= s * trappedBishopPenalty; + } + } + if (board.at(hFileSq) == BISHOP && board.at(hFileSq) == c) { + if (board.at(gPawnSq) == PAWN && board.at(gPawnSq) == opp && !board.is_attacked_by(c, hFileSq)) { + mgScore -= s * trappedBishopPenalty; + egScore -= s * trappedBishopPenalty; + } + } + } + // --- Material imbalance --- + { + int pieceCount[2][7] = { { 0 } }; + for (Color c : { WHITE, BLACK }) + for (PieceType pt : { PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING }) + pieceCount[c][pt] = board.count(pt, c); + int ourMinors = pieceCount[WHITE][KNIGHT] + pieceCount[WHITE][BISHOP]; + int theirMinors = pieceCount[BLACK][KNIGHT] + pieceCount[BLACK][BISHOP]; + int imbalance = (ourMinors - theirMinors) * minorImWt // minor piece imbalance + + (pieceCount[WHITE][BISHOP] - pieceCount[BLACK][BISHOP]) * bishopImWt // bishop vs knight + + (pieceCount[WHITE][ROOK] - pieceCount[BLACK][ROOK]) * rookImWt // rook imbalance + + (pieceCount[WHITE][QUEEN] - pieceCount[BLACK][QUEEN]) * queenImWt; // queen imbalance + mgScore += imbalance; + egScore += imbalance; + } +#endif + // Draw detection: score 0 for positions where neither side can force a win + int totalPieces = popcount(board.occ()); + int pawnCount = board.count(); + + // KBKB same-colored bishops (no pawns) - drawn + if (totalPieces == 4 && pawnCount == 0 && board.count() == 2 && board.count() == 0 && + board.count() == 0 && board.count() == 0 && board.count(BISHOP, WHITE) == 1 && + board.count(BISHOP, BLACK) == 1) { + Bitboard wbBB = board.pieces(BISHOP, WHITE); + Bitboard bbBB = board.pieces(BISHOP, BLACK); + Square wb = Square(pop_lsb(wbBB)); + Square bb = Square(pop_lsb(bbBB)); + if (square_color(wb) == square_color(bb)) + return { 0, 0, 0 }; + } + + // KNNK (no pawns) - drawn + if (totalPieces == 4 && pawnCount == 0 && board.count() == 2 && board.count() == 0 && + board.count() == 0 && board.count() == 0) + return { 0, 0, 0 }; + + phase = std::min((phase * 256 + TotalPhase / 2) / TotalPhase, 256); + return { mgScore, egScore, phase }; +} +Value eval(const chess::Position &board) { + const int sign = board.side_to_move() == WHITE ? 1 : -1; + auto [mg, eg, phase] = eval_components(board); + if (mg == 0 && eg == 0) + return 0; + return (((mg * phase) + (eg * (256 - phase))) * sign) / 256 + engine::eval::tempo; } Value piece_value(PieceType pt) { - Value pieces[] = {0, PawnValue, KnightValue, - BishopValue, RookValue, QueenValue}; - return pieces[pt]; + Value pieces[] = { 0, PawnValue, KnightValue, BishopValue, RookValue, QueenValue, 0 }; + return pieces[pt]; } } // namespace engine::eval diff --git a/eval.h b/eval.h index 74c1e20..aecdc5a 100644 --- a/eval.h +++ b/eval.h @@ -1,4 +1,5 @@ #pragma once +#include #include using Value = int; namespace engine { @@ -20,17 +21,37 @@ constexpr bool is_valid(Value value) { return value != VALUE_NONE; } constexpr bool is_win(Value value) { return value >= VALUE_TB_WIN_IN_MAX_PLY; } -constexpr bool is_loss(Value value) { - return value <= VALUE_TB_LOSS_IN_MAX_PLY; +constexpr bool is_loss(Value value) { return value <= VALUE_TB_LOSS_IN_MAX_PLY; } + +constexpr bool is_decisive(Value value) { return is_win(value) || is_loss(value); } +constexpr bool is_mate(Value value) { + assert(is_valid(value)); + return value >= VALUE_MATE_IN_MAX_PLY; } -constexpr bool is_decisive(Value value) { - return is_win(value) || is_loss(value); +constexpr bool is_mated(Value value) { + assert(is_valid(value)); + return value <= VALUE_MATED_IN_MAX_PLY; } -constexpr Value MATE(int i) { return VALUE_MATE - i; } -constexpr Value MATE_DISTANCE(int i) { return VALUE_MATE - (i < 0 ? -i : i); } + +constexpr bool is_mate_or_mated(Value value) { return is_mate(value) || is_mated(value); } + +constexpr Value mate_in(int ply) { return VALUE_MATE - ply; } + +constexpr Value mated_in(int ply) { return -VALUE_MATE + ply; } namespace eval { -Value eval(const chess::Board &board); + +struct EvalComponents { + int mg; + int eg; + int phase; +}; + +extern Value *mgPst[]; +extern Value *egPst[]; + +Value eval(const chess::Position &board); +EvalComponents eval_components(const chess::Position &board); Value piece_value(chess::PieceType pt); } // namespace eval } // namespace engine diff --git a/main.cpp b/main.cpp index 69cc515..9ae3626 100644 --- a/main.cpp +++ b/main.cpp @@ -1,21 +1,37 @@ #include "search.h" +#include "tb.h" #include "tune.h" #include "uci.h" #include "ucioption.h" #include +#include using namespace engine; +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + int main() { - std::cout << std::unitbuf; - std::cout << "cppchess_engine version " << BUILD_VERSION << '\n'; - options.add("Move Overhead", Option(10, 0, 1000)); - options.add("Hash", Option(16, 1, 1 << 25, - [](const Option &o) { return std::nullopt; })); + std::cout << std::unitbuf; + std::cout << "cppchess_engine version " << BUILD_VERSION << '\n'; + options.add("Move Overhead", Option(10, 0, 1000)); + options.add("Hash", Option(16, 1, 1 << 25, [](const Option &o) { + try { + search::tt.resize(int(o)); + } catch (std::bad_alloc &) { + std::cerr << "info string Hash resize failed: bad_alloc\n"; + } + return std::nullopt; + })); - options.add("Clear Hash", Option(+[](const Option &) { - search::tt.clear(); + options.add("Clear Hash", Option(+[](const Option &) { + search::tt.clear(); - return std::nullopt; - })); - Tune::init(options); - loop(); + return std::nullopt; + })); + options.add("SyzygyPath", Option("", [](const Option &o) { + tb::init(std::string(o)); + return std::nullopt; + })); + Tune::init(options); + std::cout.flush(); + loop(); } diff --git a/movepick.cpp b/movepick.cpp index a461c74..4134810 100644 --- a/movepick.cpp +++ b/movepick.cpp @@ -1,43 +1,138 @@ #include "movepick.h" #include "eval.h" +#include "search.h" #include -#include using namespace chess; using engine::eval::piece_value; namespace engine::movepick { -Value historyHeuristic[SQUARE_NB][SQUARE_NB]{}; -Move killerMoves[256][2]; -void orderMoves(chess::Board &board, chess::Movelist &moves, chess::Move ttMove, - int ply) { - std::vector> scoredMoves; - scoredMoves.reserve(moves.size()); - - for (const auto &move : moves) { - Value score = 0; - - if (move == ttMove) - score = 10000; - else if (board.isCapture(move)) - score = ((move.type_of() & EN_PASSANT) == 0 - ? piece_value(board.at(move.to())) - : piece_value(PAWN)) * - 10 - - piece_value(board.at(move.from())); - else if (move == killerMoves[ply][0]) - score = 8500; - else if (move == killerMoves[ply][1]) - score = 8000; - else - score = historyHeuristic[move.from()][move.to()]; - - scoredMoves.emplace_back(move, score); - } - - std::stable_sort( - scoredMoves.begin(), scoredMoves.end(), - [](const auto &a, const auto &b) { return a.second > b.second; }); - - for (size_t i = 0; i < scoredMoves.size(); ++i) - moves[i] = scoredMoves[i].first; + +static Bitboard att(PieceType pt, Square sq, Bitboard occ) { + switch (pt) { + case BISHOP: + return attacks::bishop(sq, occ); + case ROOK: + return attacks::rook(sq, occ); + case QUEEN: + return attacks::queen(sq, occ); + default: + return 0; + } +} +inline Square least_valuable_attacker(const Position &board, Bitboard attackers, Color side) { + Bitboard bb; + + bb = attackers & board.pieces(PAWN, side); + if (bb) + return Square(pop_lsb(bb)); + + bb = attackers & board.pieces(KNIGHT, side); + if (bb) + return Square(pop_lsb(bb)); + + bb = attackers & board.pieces(BISHOP, side); + if (bb) + return Square(pop_lsb(bb)); + + bb = attackers & board.pieces(ROOK, side); + if (bb) + return Square(pop_lsb(bb)); + + bb = attackers & board.pieces(QUEEN, side); + if (bb) + return Square(pop_lsb(bb)); + + bb = attackers & board.pieces(KING, side); + if (bb) + return Square(pop_lsb(bb)); + + return SQ_NONE; +} +Value see(Position &board, Move move) { + Square from = move.from(); + Square to = move.to(); + + PieceType captured = move.type_of() == EN_PASSANT ? PAWN : board.at(to); + + if (captured == NO_PIECE_TYPE) + return 0; + + Bitboard occ = board.occ(); + occ ^= 1ULL << from; + if (move.type_of() == EN_PASSANT) + occ ^= 1ULL << Square(int(to) + (board.side_to_move() == WHITE ? -8 : 8)); + + Bitboard attackers = board.attackers(WHITE, to, occ) | board.attackers(BLACK, to, occ); + + Value gain[32]; + PieceType attacker = board.at(move.from()); + + gain[0] = piece_value(captured); + + Color stm = ~board.side_to_move(); + int d = 0; + + while (++d < 32) { + + // Charge the piece that just captured. + gain[d] = piece_value(attacker) - gain[d - 1]; + + if (gain[d] < 0) + break; + + Bitboard stmAttackers = attackers & board.occ(stm); + if (!stmAttackers) + break; + + Square sq = least_valuable_attacker(board, stmAttackers, stm); + attacker = board.at(sq); + + occ ^= 1ULL << sq; + + // Recompute x-rays after EVERY removal. + attackers = board.attackers(WHITE, to, occ) | board.attackers(BLACK, to, occ); + + stm = ~stm; + } + + while (--d) + gain[d - 1] = -std::max(-gain[d - 1], gain[d]); + + return gain[0]; +} + +void orderMoves(Position &board, Movelist &moves, Move ttMove, int ply, const engine::search::Session &session, Move prevMove) { + Value scores[300]; + size_t n = moves.size(); + for (size_t i = 0; i < n; ++i) { + Move move = moves[i]; + if (move == ttMove) + scores[i] = 10000; + else if (board.isCapture(move)) { + Value s = see(board, move); + Value capturedVal = move.type_of() == EN_PASSANT ? piece_value(PAWN) : piece_value(board.at(move.to())); + Value attackerVal = piece_value(board.at(move.from())); + scores[i] = (s >= -50 ? 9000 : 4000) + std::max(s, Value(-50)) + (capturedVal * 10 - attackerVal) / 100; + } else if (move == session.killerMoves[ply][0]) + scores[i] = 8500; + else if (move == session.killerMoves[ply][1]) + scores[i] = 8000; + else if (prevMove.is_ok() && move == session.counterMoves[prevMove.from_to()]) + scores[i] = 7500; + else if (board.givesCheck(move) != CheckType::NO_CHECK) + scores[i] = 7000; + else + scores[i] = session.historyHeuristic[move.from()][move.to()]; + } + size_t limit = std::min(n, size_t(12)); + for (size_t i = 0; i + 1 < limit; ++i) { + size_t best = i; + for (size_t j = i + 1; j < n; ++j) + if (scores[j] > scores[best]) + best = j; + if (best != i) { + std::swap(moves[i], moves[best]); + std::swap(scores[i], scores[best]); + } + } } } // namespace engine::movepick diff --git a/movepick.h b/movepick.h index a789802..91396be 100644 --- a/movepick.h +++ b/movepick.h @@ -1,7 +1,10 @@ #pragma once +#include "eval.h" #include +namespace engine::search { +struct Session; +} // namespace engine::search namespace engine::movepick { -void orderMoves(chess::Board &, chess::Movelist &, chess::Move, int); -extern int historyHeuristic[64][64]; -extern chess::Move killerMoves[256][2]; +void orderMoves(chess::Position &, chess::Movelist &, chess::Move, int, const engine::search::Session &, chess::Move); +Value see(chess::Position &, chess::Move); } // namespace engine::movepick diff --git a/score.cpp b/score.cpp index 2d07d9e..ca3a1c2 100644 --- a/score.cpp +++ b/score.cpp @@ -3,16 +3,16 @@ #include namespace engine { Score::Score(Value v) { - assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); + assert(-VALUE_INFINITE < v && v < VALUE_INFINITE); - if (!is_decisive(v)) { - score = InternalUnits{v}; - } else if (std::abs(v) <= VALUE_TB) { - auto distance = VALUE_TB - std::abs(v); - score = (v > 0) ? Tablebase{distance, true} : Tablebase{-distance, false}; - } else { - auto distance = VALUE_MATE - std::abs(v); - score = (v > 0) ? Mate{distance} : Mate{-distance}; - } + if (!is_decisive(v)) { + score = InternalUnits{ v }; + } else if (std::abs(v) <= VALUE_TB) { + auto distance = VALUE_TB - std::abs(v); + score = (v > 0) ? Tablebase{ distance, true } : Tablebase{ -distance, false }; + } else { + auto distance = VALUE_MATE - std::abs(v); + score = (v > 0) ? Mate{ distance } : Mate{ -distance }; + } } } // namespace engine diff --git a/score.h b/score.h index 2b7b0e1..34d01bd 100644 --- a/score.h +++ b/score.h @@ -25,35 +25,31 @@ namespace engine { class Score { -public: - struct Mate { - int plies; - }; + public: + struct Mate { + int plies; + }; - struct Tablebase { - int plies; - bool win; - }; + struct Tablebase { + int plies; + bool win; + }; - struct InternalUnits { - int value; - }; + struct InternalUnits { + int value; + }; - Score() = default; - Score(Value v); + Score() = default; + Score(Value v); - template bool is() const { - return std::holds_alternative(score); - } + template bool is() const { return std::holds_alternative(score); } - template T get() const { return std::get(score); } + template T get() const { return std::get(score); } - template decltype(auto) visit(F &&f) const { - return std::visit(std::forward(f), score); - } + template decltype(auto) visit(F &&f) const { return std::visit(std::forward(f), score); } -private: - std::variant score; + private: + std::variant score; }; } // namespace engine diff --git a/search.cpp b/search.cpp index dd61af5..d580188 100644 --- a/search.cpp +++ b/search.cpp @@ -1,6 +1,7 @@ #include "search.h" #include "eval.h" #include "movepick.h" +#include "tb.h" #include "timeman.h" #include "uci.h" #include @@ -8,364 +9,702 @@ #include #include #include +#include +#include using namespace chess; -namespace engine { -TranspositionTable search::tt(16); -std::atomic stopSearch{false}; -void search::stop() { stopSearch.store(true, std::memory_order_relaxed); } -bool search::isStopped() { return stopSearch; } -struct Session { - timeman::TimeManagement tm; - timeman::LimitsType tc; - int seldepth = 0; - uint64_t nodes = 0; - chess::Move pv[MAX_PLY][MAX_PLY]; -}; -namespace { // SF +namespace engine::search { +TranspositionTable tt(16); +std::atomic stopSearch{ false }; +void stop() { stopSearch.store(true, std::memory_order_relaxed); } +bool isStopped() { return stopSearch.load(std::memory_order_relaxed); } +namespace { void update_pv(Move *pv, Move move, const Move *childPv) { - - for (*pv++ = move; childPv && *childPv != Move::none();) - *pv++ = *childPv++; - *pv = Move::none(); + for (*pv++ = move; childPv && *childPv != Move::none();) + *pv++ = *childPv++; + *pv = Move::none(); } // Adjusts a mate or TB score from "plies to mate from the root" to // "plies to mate from the current position". Standard scores are unchanged. // The function is called before storing a value in the transposition table. -Value value_to_tt(Value v, int ply) { - return is_win(v) ? v + ply : is_loss(v) ? v - ply : v; -} +Value value_to_tt(Value v, int ply) { return is_win(v) ? v + ply : is_loss(v) ? v - ply : v; } -// Inverse of value_to_tt(): it adjusts a mate or TB score from the -// transposition table (which refers to the plies to mate/be mated from current -// position) to "plies to mate/be mated (TB win/loss) from the root". However, -// to avoid potentially false mate or TB scores related to the 50 moves rule and -// the graph history interaction, we return the highest non-TB score instead. +// Inverse of value_to_tt(): it adjusts a mate or TB score from the transposition +// table (which refers to the plies to mate/be mated from current position) to +// "plies to mate/be mated (TB win/loss) from the root". However, to avoid +// potentially false mate or TB scores related to the 50 moves rule and the +// graph history interaction, we return the highest non-TB score instead. Value value_from_tt(Value v, int ply, int r50c) { - if (!is_valid(v)) - return VALUE_NONE; + if (!is_valid(v)) + return VALUE_NONE; - // handle TB win or better - if (is_win(v)) { - // Downgrade a potentially false mate score - if (v >= VALUE_MATE_IN_MAX_PLY && VALUE_MATE - v > 100 - r50c) - return VALUE_TB_WIN_IN_MAX_PLY - 1; + // handle TB win or better + if (is_win(v)) { + // Downgrade a potentially false mate score + if (is_mate(v) && VALUE_MATE - v > 100 - r50c) + return VALUE_TB_WIN_IN_MAX_PLY - 1; - // Downgrade a potentially false TB score. - if (VALUE_TB - v > 100 - r50c) - return VALUE_TB_WIN_IN_MAX_PLY - 1; + // Downgrade a potentially false TB score. + if (VALUE_TB - v > 100 - r50c) + return VALUE_TB_WIN_IN_MAX_PLY - 1; - return v - ply; - } + return v - ply; + } - // handle TB loss or worse - if (is_loss(v)) { - // Downgrade a potentially false mate score. - if (v <= VALUE_MATED_IN_MAX_PLY && VALUE_MATE + v > 100 - r50c) - return VALUE_TB_LOSS_IN_MAX_PLY + 1; + // handle TB loss or worse + if (is_loss(v)) { + // Downgrade a potentially false mate score. + if (is_mated(v) && VALUE_MATE + v > 100 - r50c) + return VALUE_TB_LOSS_IN_MAX_PLY + 1; - // Downgrade a potentially false TB score. - if (VALUE_TB + v > 100 - r50c) - return VALUE_TB_LOSS_IN_MAX_PLY + 1; + // Downgrade a potentially false TB score. + if (VALUE_TB + v > 100 - r50c) + return VALUE_TB_LOSS_IN_MAX_PLY + 1; - return v + ply; - } + return v + ply; + } - return v; + return v; } } // namespace -Value qsearch(Board &board, Value alpha, Value beta, Session &session, - int ply = 0) { - session.nodes++; - session.seldepth = std::max(session.seldepth, ply); - int standPat = eval::eval(board); - if (session.tm.elapsed() >= session.tm.optimum() || - stopSearch.load(std::memory_order_relaxed)) - return standPat; - Value maxScore = standPat; - if (maxScore >= beta) - return maxScore; - if (maxScore > alpha) - alpha = maxScore; - Movelist moves; - board.legals(moves); - for (Move move : moves) { - board.doMove(move); - Value score = qsearch(board, -beta, -alpha, session, ply + 1); - board.undoMove(); - if (score == VALUE_NONE) - return VALUE_NONE; - score = -score; - if (score >= beta) - return score; - if (score > maxScore) - maxScore = score; - if (score > alpha) - alpha = score; - } - return maxScore; -} -Value doSearch(Board &board, int depth, Value alpha, Value beta, - Session &session, int ply = 0) { - // TLE or exceeded depth limit - if (ply >= MAX_PLY - 1) - return eval::eval(board); - if (depth <= 0) { - session.nodes++; - return qsearch(board, alpha, beta, session, ply); - } - if (session.tm.elapsed() >= session.tm.optimum() || - stopSearch.load(std::memory_order_relaxed)) - return qsearch(board, alpha, beta, session, ply); - Value alphaOrig = alpha; - // Reset PV - std::fill(std::begin(session.pv[ply]), std::end(session.pv[ply]), - Move::none()); - std::fill(std::begin(session.pv[ply + 1]), std::end(session.pv[ply + 1]), - Move::none()); - if (board.is_draw(3) || board.is_insufficient_material()) { +Value qsearch(Position &board, Value alpha, Value beta, search::Session &session, int ply) { session.nodes++; - session.pv[ply][0] = Move::none(); - return 0; - } - session.seldepth = std::max(session.seldepth, ply); - uint64_t hash = board.hash(); - Move preferred = Move::none(); - if (TTEntry *entry = search::tt.lookup(hash)) { - if (entry->getDepth() >= depth) { - Value ttScore = - value_from_tt(entry->getScore(), ply, board.rule50_count()); - TTFlag flag = entry->getFlag(); - - if (flag == TTFlag::EXACT) { - session.pv[ply][0] = Move(entry->getMove()); - session.pv[ply][1] = Move::none(); - return ttScore; - } - - if (flag == TTFlag::LOWERBOUND && ttScore >= beta) { - session.pv[ply][0] = Move(entry->getMove()); - session.pv[ply][1] = Move::none(); - return ttScore; - } - - if (flag == TTFlag::UPPERBOUND && ttScore <= alpha) { - session.pv[ply][0] = Move(entry->getMove()); - session.pv[ply][1] = Move::none(); - return ttScore; - } + session.qnodes++; + session.seldepth = std::max(session.seldepth, ply); + if (((session.nodes & 2047) == 0 && session.tm.elapsed() >= session.tm.optimum()) || + (session.tc.nodes > 0 && session.nodes >= session.tc.nodes) || stopSearch.load(std::memory_order_relaxed)) + return VALUE_NONE; + + bool inCheck = board.is_check(); + if (board.is_draw(3) || board.is_insufficient_material()) + return VALUE_DRAW; + + Move ttMove = Move::none(); + TTEntry *entry = search::tt.lookup(board.hash()); + Value alphaOrig = alpha; + if (entry) { + session.ttHits++; + Value ttScore = value_from_tt(entry->getScore(), ply, board.rule50_count()); + + if (entry->getFlag() == EXACT) { + session.ttCutoffs++; + return ttScore; + } + if (entry->getFlag() == LOWERBOUND && ttScore >= beta) { + session.ttCutoffs++; + return ttScore; + } + if (entry->getFlag() == UPPERBOUND && ttScore <= alpha) { + session.ttCutoffs++; + return ttScore; + } + if (entry->getFlag() == LOWERBOUND) + alpha = std::max(alpha, ttScore); + else if (entry->getFlag() == UPPERBOUND) + beta = std::min(beta, ttScore); + if (alpha >= beta) { + session.ttCutoffs++; + return ttScore; + } + ttMove = Move(entry->getMove()); } - preferred = Move(entry->getMove()); - } - Value maxScore = -VALUE_INFINITE; - Movelist moves; - board.legals(moves); - if (!moves.size()) { - session.pv[ply][0] = Move::none(); - return board.checkers() ? -MATE(ply) : 0; - } - movepick::orderMoves(board, moves, preferred, ply); - if (bool useNMP = depth >= 3 && !board.checkers() && ply > 0) { - int R = 2 + depth / 6; - uint64_t hash_ = board.hash(); - board.doNullMove(); - Value score = - doSearch(board, depth - 1 - R, -beta, -beta + 1, session, ply + 1); - - score = -score; - board.undoMove(); - if (score >= beta) - return score; - } - for (size_t i = 0; i < moves.size(); ++i) { - Move move = moves[i]; - - bool isCapture = board.isCapture(move); - bool givesCheck = board.givesCheck(move) != CheckType::NO_CHECK; - - // --- LMR reduction --- - int reduction = 0; - if (i >= 3 && depth >= 3 && !isCapture && !givesCheck) { - reduction = 1 + (int)(i / 6) + (depth / 8); - - // history heuristic: good moves get reduced less - if (movepick::historyHeuristic[(int)move.from()][(int)move.to()] > 0) - reduction--; - - reduction = std::max(0, reduction); - reduction = std::min(reduction, depth - 2); + + Movelist moves; + + if (inCheck) + board.legals(moves); + else + board.legals(moves); + Value best = -VALUE_INFINITE; + Value standPat = VALUE_NONE; + + if (!inCheck) { + standPat = eval::eval(board); + + if (standPat >= beta) + return standPat; + + if (standPat > alpha) + alpha = standPat; + + best = standPat; } - board.doMove(move); + if (!moves.size()) + return inCheck ? mated_in(ply) : best; + if (ply >= MAX_PLY - 1) + return best; + + movepick::orderMoves(board, moves, ttMove, ply, session, Move::none()); + int movesSearched = 0; + for (Move move : moves) { + bool isCapture = board.isCapture(move); + bool givesCheck = board.givesCheck(move) != CheckType::NO_CHECK; + + if (!is_loss(best)) { + if (!isCapture && !givesCheck) + continue; + if (isCapture && !givesCheck && move.type_of() != PROMOTION) { + Value capturedValue = + move.type_of() == EN_PASSANT ? eval::piece_value(PAWN) : eval::piece_value(board.at(move.to())); + if (standPat + capturedValue + 200 < alpha) + continue; + if (movepick::see(board, move) < 0) + continue; + } + } - Value score; + board.doMove(move); - if (i == 0) { - uint64_t hash_ = board.hash(); - // --- First move: full window (PVS root move) --- - score = doSearch(board, depth - 1, -beta, -alpha, session, ply + 1); + Value score = -qsearch(board, -beta, -alpha, session, ply + 1); - if (score == VALUE_NONE) { - board.undoMove(); - return VALUE_NONE; - } - score = -score; - } else { - uint64_t hash_ = board.hash(); - // --- Null-window search (PVS + LMR) --- - score = doSearch(board, depth - 1 - reduction, -alpha - 1, -alpha, - session, ply + 1); - if (score == VALUE_NONE) { board.undoMove(); + + if (score == VALUE_NONE) + return VALUE_NONE; + + if (score > best) { + ttMove = move; + best = score; + } + if (score > alpha) + alpha = score; + if (alpha >= beta) + break; + + movesSearched++; + } + TTFlag flag = best >= beta ? LOWERBOUND : best <= alphaOrig ? UPPERBOUND : EXACT; + tt.store(board.hash(), ttMove, value_to_tt(best, ply), 0, flag); + return best; +} +Value doSearch( + Position &board, int depth, Value alpha, Value beta, search::Session &session, int ply = 0, Move prevMove = Move::none()) { + session.nodes++; + session.seldepth = std::max(session.seldepth, ply); + if (ply >= MAX_PLY - 1) + return eval::eval(board); + if (depth <= 0) + return qsearch(board, alpha, beta, session, ply); + if (((session.nodes & 2047) == 0 && session.tm.elapsed() >= session.tm.optimum()) || + (session.tc.nodes > 0 && session.nodes >= session.tc.nodes) || stopSearch.load(std::memory_order_relaxed)) return VALUE_NONE; - } - score = -score; - // --- Re-search if it improves alpha --- - if (score > alpha) { - score = doSearch(board, depth - 1, -beta, -alpha, session, ply + 1); - if (score == VALUE_NONE) { - board.undoMove(); - return VALUE_NONE; + + bool inCheck = board.is_check(); + + alpha = std::max(mated_in(ply), alpha); + beta = std::min(mate_in(ply + 1), beta); + if (alpha >= beta) + return alpha; + + session.pv[ply][0] = Move::none(); + if (board.is_draw(3) || board.is_insufficient_material()) + return VALUE_DRAW; + + Value alphaOrig = alpha; + uint64_t hash = board.hash(); + Move ttMove = Move::none(); + Value staticEval = eval::eval(board); + + TTEntry *entry = search::tt.lookup(hash); + if (entry) { + if (entry->getDepth() >= depth) { + session.ttHits++; + Value ttScore = value_from_tt(entry->getScore(), ply, board.rule50_count()); + TTFlag flag = entry->getFlag(); + + if (flag == EXACT && ply > 0) { + session.ttCutoffs++; + session.pv[ply][0] = Move(entry->getMove()); + session.pv[ply][1] = Move::none(); + return ttScore; + } + if (flag == LOWERBOUND && ttScore >= beta && ply > 0) { + session.ttCutoffs++; + session.pv[ply][0] = Move(entry->getMove()); + session.pv[ply][1] = Move::none(); + return ttScore; + } + if (flag == UPPERBOUND && ttScore <= alpha && ply > 0) { + session.ttCutoffs++; + session.pv[ply][0] = Move(entry->getMove()); + session.pv[ply][1] = Move::none(); + return ttScore; + } } + ttMove = Move(entry->getMove()); + } + + // Reverse futility pruning: if eval is well above beta, prune + if (!inCheck && ply > 0 && depth <= 3 && staticEval - 150 * depth >= beta && !is_win(beta) && + std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY) + return staticEval; + + // Razoring: if eval is far below alpha, try qsearch to verify + if (depth <= 2 && !inCheck && ply > 0 && !is_win(alpha)) { + Value razorMargin = Value(256 + 100 * depth); + if (staticEval + razorMargin < alpha) { + Value v = qsearch(board, alpha - 1, alpha, session, ply); + if (v == VALUE_NONE) + return VALUE_NONE; + if (v < alpha && std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY) + return v; + } + } + + // Null move pruning (skip when a mate threat is possible) + if (depth >= 3 && !inCheck && ply > 0 && staticEval >= beta && !is_win(beta)) { + int R = 2 + depth / 6; // + std::min(2, depth / 10); + board.doNullMove(); + Value score = doSearch(board, depth - 1 - R, -beta, -beta + 1, session, ply + 1, Move::none()); + board.undoMove(); + if (score == VALUE_NONE) + return VALUE_NONE; score = -score; - } + if (score >= beta) { + session.nullCutoffs++; + return score; + } } - board.undoMove(); - if (score > maxScore) { - maxScore = score; - update_pv(session.pv[ply], move, session.pv[ply + 1]); + // ProbCut: if eval is well above beta, verify with reduced-depth search + if (depth >= 5 && !inCheck && !is_win(beta) && std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY) { + Value probCutMargin = Value(200 + 100 * (depth - 5)); + if (staticEval >= beta + probCutMargin) { + Value v = doSearch(board, depth - 2, beta - 1, beta, session, ply, prevMove); + if (v == VALUE_NONE) + return VALUE_NONE; + if (v >= beta) + return v; + } } - if (score > alpha) { - alpha = score; + // Existing static null-move / futility pruning + if (!inCheck && staticEval < alpha - 512 - 293 * depth * depth) { + Value value = qsearch(board, alpha - 1, alpha, session, ply); + if (value == VALUE_NONE) + return VALUE_NONE; + if (value < alpha && std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY) + return value; + } - if (!isCapture) - movepick::historyHeuristic[(int)move.from()][(int)move.to()] += - depth * depth; + // Tablebase probing (unchanged) + if (ply != 0) { + if (popcount(board.occ()) <= 7 && board.castlingRights() == NO_CASTLING) { + int wdl = engine::tb::probe_wdl(board); + if (wdl != engine::tb::TB_ERROR) { + session.tbHits++; + + int drawScore = 1; + + Value tbValue = VALUE_TB - ply; + + Value value = wdl < -drawScore ? -tbValue : wdl > drawScore ? tbValue : VALUE_DRAW + 2 * wdl * drawScore; + TTFlag b = wdl < -drawScore ? UPPERBOUND : wdl > drawScore ? LOWERBOUND : EXACT; + if (b == EXACT || (b == LOWERBOUND ? value >= beta : value <= alpha)) { + tt.store(hash, Move::none(), value_to_tt(value, ply), std::min(MAX_PLY - 1, depth + 6), b); + return value; + } + + if (b == LOWERBOUND) + alpha = std::max(alpha, value); + if (alpha >= beta) + return alpha; + } + } } - if (alpha >= beta) { - // killer moves - if (!isCapture) { - if (movepick::killerMoves[ply][0] != move) { - movepick::killerMoves[ply][1] = movepick::killerMoves[ply][0]; - movepick::killerMoves[ply][0] = move; + // Internal Iterative Deepening (IID): get a TT move when we don't have one + if (depth >= 8 && ttMove == Move::none() && !inCheck && alpha != beta - 1) { + int d = std::max(2, depth - 2 - depth / 4); + Value v = doSearch(board, d, alpha, beta, session, ply, prevMove); + if (v == VALUE_NONE) + return VALUE_NONE; + if (TTEntry *e = search::tt.lookup(hash)) + ttMove = Move(e->getMove()); + } + Movelist moves; + board.legals(moves); + if (!moves.size()) { + session.pv[ply][0] = Move::none(); + return board.checkers() ? mated_in(ply) : 0; + } + movepick::orderMoves(board, moves, ttMove, ply, session, prevMove); + + // Singular Extension: extend TT move when it dominates all others + int singularExt = 0; + if (depth >= 12 && ttMove.is_ok() && !inCheck && ply > 0 && entry && entry->getDepth() >= depth - 4 && + entry->getFlag() != UPPERBOUND && alpha != beta - 1 && !is_win(beta)) { + Value sBeta = std::max(staticEval - 2 * depth, Value(-VALUE_MATE)); + int r = std::max(2, depth / 4); + if (moves.size() <= 5) { + bool singular = true; + for (size_t si = 0; si < moves.size() && singular; ++si) { + if (ply == 0 && !session.tc.searchmoves.empty() && + std::find(session.tc.searchmoves.begin(), + session.tc.searchmoves.end(), + chess::uci::moveToUci(moves[si], board.chess960())) == session.tc.searchmoves.end()) + continue; + if (moves[si] == ttMove) + continue; + board.doMove(moves[si]); + Value v = doSearch(board, r, -sBeta, -sBeta + 1, session, ply + 1, moves[si]); + board.undoMove(); + if (v == VALUE_NONE) { + singular = false; + break; + } + v = -v; + if (v >= sBeta) + singular = false; + } + if (singular) + singularExt = 1; } - } - break; } - if (session.tm.elapsed() >= session.tm.optimum() || - stopSearch.load(std::memory_order_relaxed)) - return maxScore; - } + Value maxScore = -VALUE_INFINITE; + int movesSearched = 0; + + for (size_t i = 0; i < moves.size(); ++i) { + Move move = moves[i]; + if (ply == 0 && !session.tc.searchmoves.empty() && + std::find(session.tc.searchmoves.begin(), + session.tc.searchmoves.end(), + chess::uci::moveToUci(move, board.chess960())) == session.tc.searchmoves.end()) + continue; + + bool isCapture = board.isCapture(move); + bool givesCheck = board.givesCheck(move) != CheckType::NO_CHECK; + + // Futility pruning at shallow depths + if (!inCheck && !isCapture && !givesCheck && depth <= 2 && ply > 0 && movesSearched > 0) { + Value margin = Value(128 + 128 * depth); + if (staticEval + margin <= alpha && std::abs(alpha) < VALUE_TB_WIN_IN_MAX_PLY) + continue; + } - if (maxScore != -VALUE_INFINITE) { - TTFlag flag; + // Late move pruning at very shallow depths + if (!inCheck && !isCapture && !givesCheck && depth <= 2 && movesSearched > 3 + 2 * depth && + std::abs(alpha) < VALUE_TB_WIN_IN_MAX_PLY) + continue; - if (maxScore <= alphaOrig) - flag = TTFlag::UPPERBOUND; - else if (maxScore >= beta) - flag = TTFlag::LOWERBOUND; - else - flag = TTFlag::EXACT; + // SEE pruning for losing captures at shallow depths + if (!inCheck && isCapture && !givesCheck && depth <= 2 && movesSearched > 0 && move.type_of() != PROMOTION && + std::abs(alpha) < VALUE_TB_WIN_IN_MAX_PLY) { + if (movepick::see(board, move) < 0) + continue; + } + + // LMR reduction + int reduction = 0; + if (movesSearched >= 2 && depth >= 3) { + if (!isCapture && !givesCheck) { + reduction = 1 + movesSearched / 5 + depth / 7; + int history = session.historyHeuristic[(int)move.from()][(int)move.to()]; + if (history > 0) + reduction--; + else if (history < 0) + reduction++; + if (move == session.killerMoves[ply][0] || move == session.killerMoves[ply][1]) + reduction--; + if (prevMove.is_ok() && move == session.counterMoves[prevMove.from_to()]) + reduction--; + if (staticEval + 50 < alphaOrig) + reduction++; + else if (staticEval - 50 >= alphaOrig) + reduction--; + } else if (movesSearched >= 6) { + reduction = 1 + movesSearched / 8; + } + if (reduction > 0) + reduction = std::clamp(reduction, 1, depth - 2); + } + + int ext = 0; + if (singularExt && movesSearched == 0) + ext = 1; + if (ext == 0 && moves.size() == 1) + ext = 1; + if (ext == 0 && isCapture && movesSearched == 0) + ext = 1; + board.doMove(move); + + Value score; + + if (movesSearched == 0 || reduction == 0) { + score = doSearch(board, depth - 1 + ext, -beta, -alpha, session, ply + 1, move); + if (score == VALUE_NONE) { + board.undoMove(); + return VALUE_NONE; + } + score = -score; + } else { + int d = depth - 1 - reduction + ext; + score = doSearch(board, d, -alpha - 1, -alpha, session, ply + 1, move); + if (score == VALUE_NONE) { + board.undoMove(); + return VALUE_NONE; + } + score = -score; + if (score > alpha && reduction) { + session.lmrResearches++; + score = doSearch(board, depth - 1 + ext, -beta, -alpha, session, ply + 1, move); + if (score == VALUE_NONE) { + board.undoMove(); + return VALUE_NONE; + } + score = -score; + } + } + + board.undoMove(); + movesSearched++; - search::tt.store(hash, session.pv[ply][0], value_to_tt(maxScore, ply), - depth, flag); - } - return maxScore; + if (score > maxScore) { + maxScore = score; + update_pv(session.pv[ply], move, session.pv[ply + 1]); + } + + if (score > alpha) { + alpha = score; + + if (!isCapture) { + int bonus = depth * depth; + if (is_win(score)) + bonus += 4 * depth * depth; + session.historyHeuristic[(int)move.from()][(int)move.to()] = + std::clamp(session.historyHeuristic[(int)move.from()][(int)move.to()] + bonus, -16384, 16384); + } + } /*else if (!isCapture && depth > 0) { + int malus = -depth * depth; + session.historyHeuristic[(int)move.from()][(int)move.to()] = + std::clamp(session.historyHeuristic[(int)move.from()][(int)move.to()] + malus, -16384, 16384); + }*/ + + if (alpha >= beta) { + if (!isCapture) { + if (session.killerMoves[ply][0] != move) { + session.killerMoves[ply][1] = session.killerMoves[ply][0]; + session.killerMoves[ply][0] = move; + } + if (prevMove.is_ok()) + session.counterMoves[prevMove.from_to()] = move; + } + break; + } + + if (((session.nodes & 2047) == 0 && session.tm.elapsed() >= session.tm.optimum()) || + stopSearch.load(std::memory_order_relaxed)) + return VALUE_NONE; + } + if (maxScore != -VALUE_INFINITE) { + TTFlag flag = maxScore >= beta ? LOWERBOUND : maxScore <= alphaOrig ? UPPERBOUND : EXACT; + + tt.store(hash, session.pv[ply][0], value_to_tt(maxScore, ply), depth, flag); + } + return maxScore; } -void search::search(const chess::Board &board, - const timeman::LimitsType timecontrol) { - stopSearch = false; - static double originalTimeAdjust = -1; - Session session; - session.tc = timecontrol; - session.tm.init(session.tc, board.side_to_move(), 0, originalTimeAdjust); - InfoFull lastInfo{}; - chess::Move lastPV[MAX_PLY]{}; - for (int i = 1; i < timecontrol.depth; i++) { - for (int _ = 0; _ < 64; _++) - for (int j = 0; j < 64; j++) { - movepick::historyHeuristic[_][j] /= 2; - // since MAX_PLY=64 - session.pv[_][j] = Move::none(); - } - auto board_ = board; - Value score_ = - doSearch(board_, i, -VALUE_INFINITE, VALUE_INFINITE, session); - if (session.tm.elapsed() >= session.tm.optimum() || - stopSearch.load(std::memory_order_relaxed) || score_ == VALUE_NONE) - break; - InfoFull info{}; - info.depth = i; - info.selDepth = session.seldepth; - info.hashfull = tt.hashfull(); - info.nodes = session.nodes; - info.nps = session.nodes * 1000 / - std::max(session.tm.elapsed(), (timeman::TimePoint)1); - info.timeMs = session.tm.elapsed(); - info.multiPV = 1; - info.score = score_; - TTEntry *entry = tt.lookup(board.hash()); - if (entry) - switch (entry->getFlag()) { - case LOWERBOUND: - info.bound = "lowerbound"; - break; - case UPPERBOUND: - info.bound = "upperbound"; - break; - default: - break; - } - std::string pv = ""; - for (Move *m = session.pv[0]; *m != Move::none(); m++) - pv += chess::uci::moveToUci(*m, board.chess960()) + " "; - info.pv = pv; - report(info); - lastInfo = info; - std::copy(session.pv[0], &session.pv[0][MAX_PLY], lastPV); - } - if (lastPV[0].is_ok()) - report(chess::uci::moveToUci(lastPV[0])); - else { - // try to TT probe it - TTEntry *entry = tt.lookup(board.hash()); - if (entry && entry->getMove() != Move::none().raw()) - report(chess::uci::moveToUci(Move(entry->getMove()), board.chess960())); - else { - Movelist moves; - board.legals(moves); +std::string extract_pv(const chess::Position &root, int maxPly) { + std::string pv; + chess::Position pos = root; + for (int ply = 0; ply < maxPly; ply++) { + if (pos.is_draw(3)) + break; + TTEntry *e = search::tt.lookup(pos.hash()); + if (!e) + break; + chess::Move m(e->getMove()); + if (!m.is_ok()) + break; + chess::Movelist ml; + pos.legals(ml); + bool legal = false; + for (size_t i = 0; i < ml.size(); i++) + if (ml[i] == m) { + legal = true; + break; + } + if (!legal) + break; + pv += chess::uci::moveToUci(m, root.chess960()) + " "; + if (ply + 1 >= maxPly) + break; + pos.doMove(m); + } + return pv; +} + +void search(const chess::Position &board, const timeman::LimitsType timecontrol) { + stopSearch = false; + tt.newSearch(); + static double originalTimeAdjust = -1; + Session session; + session.tc = timecontrol; + session.tm.init(session.tc, board.side_to_move(), board.ply(), originalTimeAdjust); + session.lastLogTime = session.tm.elapsed(); + session.ogcolor = board.side_to_move(); + chess::Move lastPV[MAX_PLY]{}; + Value prevScore = VALUE_NONE; + + if (!session.tc.searchmoves.empty()) { + Movelist legal; + board.legals(legal); + bool any_legal = false; + for (size_t i = 0; i < legal.size() && !any_legal; ++i) + if (std::find(session.tc.searchmoves.begin(), + session.tc.searchmoves.end(), + chess::uci::moveToUci(legal[i], board.chess960())) != session.tc.searchmoves.end()) + any_legal = true; + if (!any_legal) + session.tc.searchmoves.clear(); + } - if (moves.size()) { - Board board_ = board; - Move best = moves[0]; + { + Movelist moves; + board.legals(moves); + Position board_ = board; + Move best = Move::none(); Value bestScore = -VALUE_INFINITE; + double unusedTimeAdjust = -1; + Session tmpSession; + tmpSession.tc = session.tc; + tmpSession.tm.init(session.tc, board.side_to_move(), board.ply(), unusedTimeAdjust); for (Move move : moves) { - board_.doMove(move); - Value score = -eval::eval(board_); - if (score > bestScore) { - bestScore = score; - best = move; - } - board_.undoMove(); + if (!session.tc.searchmoves.empty() && + std::find(session.tc.searchmoves.begin(), + session.tc.searchmoves.end(), + chess::uci::moveToUci(move, board.chess960())) == session.tc.searchmoves.end()) + continue; + board_.doMove(move); + Value score = -qsearch(board_, -VALUE_INFINITE, VALUE_INFINITE, tmpSession, 0); + if (score > bestScore) { + bestScore = score; + best = move; + } + board_.undoMove(); } + lastPV[0] = best; + } + for (int i = 1; i <= timecontrol.depth; i++) { + session.lastLogTime = session.tm.elapsed(); + session.depth = i; + for (int _ = 0; _ < 64; _++) + for (int j = 0; j < 64; j++) + session.historyHeuristic[_][j] = session.historyHeuristic[_][j] * 1 / 2; + auto board_ = board; + Value score_; + + if (i >= 3 && prevScore != VALUE_NONE && !is_win(prevScore) && !is_loss(prevScore)) { + Value delta = Value(20 + i * 5); + Value alpha0 = std::max(prevScore - delta, -VALUE_INFINITE); + Value beta0 = std::min(prevScore + delta, VALUE_INFINITE); + + score_ = doSearch(board_, i, alpha0, beta0, session); + + if (score_ != VALUE_NONE && score_ <= alpha0) { + score_ = doSearch(board_, i, -VALUE_INFINITE, beta0, session); + if (score_ != VALUE_NONE && score_ <= alpha0) + score_ = doSearch(board_, i, -VALUE_INFINITE, VALUE_INFINITE, session); + } else if (score_ != VALUE_NONE && score_ >= beta0) { + score_ = doSearch(board_, i, alpha0, VALUE_INFINITE, session); + if (score_ != VALUE_NONE && score_ >= beta0) + score_ = doSearch(board_, i, -VALUE_INFINITE, VALUE_INFINITE, session); + } + } else { + score_ = doSearch(board_, i, -VALUE_INFINITE, VALUE_INFINITE, session); + } + prevScore = score_; + if (session.tm.elapsed() >= session.tm.optimum() || session.tm.elapsed() >= session.tm.maximum() || + stopSearch.load(std::memory_order_relaxed) || score_ == VALUE_NONE) + break; InfoFull info{}; - info.depth = 1; - info.nodes = 1; - info.score = 0; + info.depth = i; + info.selDepth = session.seldepth; + info.hashfull = tt.hashfull(); + info.nodes = session.nodes; + info.nps = session.nodes * 1000 / std::max(session.tm.elapsed(), (timeman::TimePoint)1); + info.timeMs = session.tm.elapsed(); + info.tbHits = session.tbHits; info.multiPV = 1; - info.pv = std::string(chess::uci::moveToUci(best, board.chess960())); + info.score = score_; + TTEntry *entry = tt.lookup(board.hash()); + if (entry) + switch (entry->getFlag()) { + case LOWERBOUND: + info.bound = "lowerbound"; + break; + case UPPERBOUND: + info.bound = "upperbound"; + break; + default: + break; + } + info.pv = extract_pv(board, 2 * i + 4); + // Save first move from PV for bestmove output + std::string pvStr = info.pv; + size_t sp = pvStr.find(' '); + std::string firstMove = (sp == std::string::npos) ? pvStr : pvStr.substr(0, sp); + if (!firstMove.empty()) + lastPV[0] = chess::Move(chess::uci::uciToMove(board, firstMove).raw()); + std::stringstream ss; + ss << "qnodes " << session.qnodes << " lmrResearches " << session.lmrResearches << " ttHits " << session.ttHits + << " ttCutoffs " << session.ttCutoffs << " nullCutoffs " << session.nullCutoffs; + info.extrainfo = ss.str(); report(info); - - report(chess::uci::moveToUci(best, board.chess960())); - } else { - report("0000"); - } } - } + if (lastPV[0].is_ok()) + report(chess::uci::moveToUci(lastPV[0], board.chess960())); + else { + std::cerr << "info string Warning: Did not search\n"; + TTEntry *entry = tt.lookup(board.hash()); + if (entry && entry->getMove() != Move::none().raw()) + report(chess::uci::moveToUci(Move(entry->getMove()), board.chess960())); + else { + Movelist moves; + board.legals(moves); + + if (moves.size()) { + Position board_ = board; + Move best = Move::none(); + Value bestScore = -VALUE_INFINITE; + double unusedTimeAdjust = -1; + Session tmpSession; + tmpSession.tc = session.tc; + tmpSession.tm.init(session.tc, board.side_to_move(), board.ply(), unusedTimeAdjust); + for (Move move : moves) { + if (!session.tc.searchmoves.empty() && + std::find(session.tc.searchmoves.begin(), + session.tc.searchmoves.end(), + chess::uci::moveToUci(move, board.chess960())) == session.tc.searchmoves.end()) + continue; + board_.doMove(move); + Value score = -qsearch(board_, -VALUE_INFINITE, VALUE_INFINITE, tmpSession, 0); + if (score > bestScore) { + bestScore = score; + best = move; + } + board_.undoMove(); + } + + if (best.is_ok()) { + InfoFull info{}; + info.depth = 1; + info.nodes = 1; + info.score = 0; + info.multiPV = 1; + info.pv = std::string(chess::uci::moveToUci(best, board.chess960())); + report(info); + + report(chess::uci::moveToUci(best, board.chess960())); + } else { + report("0000"); + } + } else { + report("0000"); + } + } + } } -} // namespace engine +} // namespace engine::search diff --git a/search.h b/search.h index d0331d9..16bb71a 100644 --- a/search.h +++ b/search.h @@ -1,14 +1,25 @@ #pragma once +#include "eval.h" +#include "timeman.h" #include "tt.h" #include -namespace engine { -namespace timeman { -struct LimitsType; -} -} // namespace engine namespace engine::search { +struct Session { + timeman::TimeManagement tm; + timeman::LimitsType tc; + int seldepth = 0; + uint64_t nodes = 0, qnodes = 0, lmrResearches = 0; + uint64_t tbHits = 0, ttHits = 0, ttCutoffs = 0, nullCutoffs = 0; + chess::Move pv[MAX_PLY][MAX_PLY]{}; + Value historyHeuristic[chess::SQUARE_NB][chess::SQUARE_NB]{}; + chess::Move killerMoves[MAX_PLY][2]{}; + chess::Move counterMoves[4096]{}; + timeman::TimePoint lastLogTime; + int depth = 0; + chess::Color ogcolor; +}; void stop(); -void search(const chess::Board &, const timeman::LimitsType); +void search(const chess::Position &, const timeman::LimitsType); bool isStopped(); extern engine::TranspositionTable tt; } // namespace engine::search diff --git a/tb.cpp b/tb.cpp new file mode 100644 index 0000000..0904630 --- /dev/null +++ b/tb.cpp @@ -0,0 +1,59 @@ +#include "tb.h" +#include +#include +#include +#include + +namespace engine::tb { + +tbprobe::syzygy::Tablebase tablebase; + +namespace { +std::size_t unique_table_count(const std::unordered_map &tables) { + std::unordered_set uniqueTables; + + for (const auto &[_, table] : tables) + if (table != nullptr) + uniqueTables.insert(table); + + return uniqueTables.size(); +} +} // namespace + +void init(const std::string &path) { + tablebase.close(); + + if (path.empty()) { + std::cout << "info string Found 0 WDL and 0 DTZ tablebase files\n"; + return; + } + + try { + tbprobe::syzygy::initialize(); + tablebase.add_directory(path, true, true); + std::cout << "info string Found " << wdl_count() << " WDL and " << dtz_count() << " DTZ tablebase files\n"; + } catch (const std::exception &e) { + std::cout << "info string Syzygy init failed: " << e.what() << '\n'; + } +} + +std::size_t wdl_count() { return unique_table_count(tablebase.wdl); } + +std::size_t dtz_count() { return unique_table_count(tablebase.dtz); } + +int probe_wdl(chess::Position &board) { + try { + return *tablebase.get_wdl(board, TB_ERROR); + } catch (const std::exception &) { + return TB_ERROR; + } +} + +int probe_dtz(chess::Position &board) { + try { + return *tablebase.get_dtz(board, TB_ERROR); + } catch (const std::exception &) { + return TB_ERROR; + } +} +} // namespace engine::tb diff --git a/tb.h b/tb.h new file mode 100644 index 0000000..de215d0 --- /dev/null +++ b/tb.h @@ -0,0 +1,15 @@ +#pragma once +#include "eval.h" +#include +#include +#include + +namespace engine::tb { + +void init(const std::string &path); +constexpr int TB_ERROR = 1000; +std::size_t wdl_count(); +std::size_t dtz_count(); +int probe_wdl(chess::Position &board); +int probe_dtz(chess::Position &board); +} // namespace engine::tb diff --git a/timeman.cpp b/timeman.cpp index 96d6b29..3bd6469 100644 --- a/timeman.cpp +++ b/timeman.cpp @@ -18,75 +18,67 @@ void TimeManagement::clear() {} // the bounds of time allowed for the current game ply. We currently support: // 1) x basetime (+ z increment) // 2) x moves in y seconds (+ z increment) -void TimeManagement::init(LimitsType &limits, chess::Color us, int ply, - double &originalTimeAdjust) { +void TimeManagement::init(LimitsType &limits, chess::Color us, int ply, double &originalTimeAdjust) { - // If we have no time, we don't need to fully initialize TM. - // startTime is used by movetime and useNodesTime is used in elapsed calls. - startTime = limits.startTime; - if (limits.movetime != 0 && limits.time[us] == 0) { - optimumTime = maximumTime = TimePoint(limits.movetime); - return; - } - if (limits.time[us] == 0 && limits.movetime == 0) - return; - // optScale is a percentage of available time to use for the current move. - // maxScale is a multiplier applied to optimumTime. - double optScale, maxScale; + // If we have no time, we don't need to fully initialize TM. + // startTime is used by movetime and useNodesTime is used in elapsed calls. + startTime = limits.startTime; + if (limits.movetime != 0 && limits.time[us] == 0) { + optimumTime = maximumTime = TimePoint(limits.movetime); + return; + } + if (limits.time[us] == 0 && limits.movetime == 0) { + optimumTime = maximumTime = INFINITE_TIME; + return; + } + // optScale is a percentage of available time to use for the current move. + // maxScale is a multiplier applied to optimumTime. + double optScale, maxScale; - // These numbers are used where multiplications, divisions or comparisons - // with constants are involved. - const TimePoint time = limits.time[us]; - const int moveOverhead = options["Move Overhead"]; - // Maximum move horizon - int centiMTG = - limits.movestogo ? std::min(limits.movestogo * 100, 5000) : 5051; + // These numbers are used where multiplications, divisions or comparisons + // with constants are involved. + const TimePoint time = limits.time[us]; + const int moveOverhead = options["Move Overhead"]; + // Maximum move horizon + int centiMTG = limits.movestogo ? std::min(limits.movestogo * 100, 5000) : 5051; - // If less than one second, gradually reduce mtg - if (time < 1000) - centiMTG = int(time * 5.051); + // If less than one second, gradually reduce mtg + if (time < 1000) + centiMTG = int(time * 5.051); - // Make sure timeLeft is > 0 since we may use it as a divisor - TimePoint timeLeft = - std::max(TimePoint(1), time + (limits.inc[us] * (centiMTG - 100) - - moveOverhead * (200 + centiMTG)) / - 100); + // Make sure timeLeft is > 0 since we may use it as a divisor + TimePoint timeLeft = + std::max(TimePoint(1), time + (limits.inc[us] * (centiMTG - 100) - moveOverhead * (200 + centiMTG)) / 100); - // x basetime (+ z increment) - // If there is a healthy increment, timeLeft can exceed the actual available - // game time for the current move, so also cap to a percentage of available - // game time. - if (limits.movestogo == 0) { - // Extra time according to timeLeft - if (originalTimeAdjust < 0) - originalTimeAdjust = 0.3128 * std::log10(timeLeft) - 0.4354; + // x basetime (+ z increment) + // If there is a healthy increment, timeLeft can exceed the actual available + // game time for the current move, so also cap to a percentage of available + // game time. + if (limits.movestogo == 0) { + // Extra time according to timeLeft + if (originalTimeAdjust < 0) + originalTimeAdjust = 0.3128 * std::log10(timeLeft) - 0.4354; - // Calculate time constants based on current time left. - double logTimeInSec = std::log10(time / 1000.0); - double optConstant = - std::min(0.0032116 + 0.000321123 * logTimeInSec, 0.00508017); - double maxConstant = std::max(3.3977 + 3.03950 * logTimeInSec, 2.94761); + // Calculate time constants based on current time left. + double logTimeInSec = std::log10(time / 1000.0); + double optConstant = std::min(0.0032116 + 0.000321123 * logTimeInSec, 0.00508017); + double maxConstant = std::max(3.3977 + 3.03950 * logTimeInSec, 2.94761); - optScale = - std::min(0.0121431 + std::pow(ply + 2.94693, 0.461073) * optConstant, - 0.213035 * time / timeLeft) * - originalTimeAdjust; + optScale = std::min(0.0121431 + std::pow(ply + 2.94693, 0.461073) * optConstant, 0.213035 * time / timeLeft) * + originalTimeAdjust; - maxScale = std::min(6.67704, maxConstant + ply / 11.9847); - } + maxScale = std::min(6.67704, maxConstant + ply / 11.9847); + } - // x moves in y seconds (+ z increment) - else { - optScale = std::min((0.88 + ply / 116.4) / (centiMTG / 100.0), - 0.88 * time / timeLeft); - maxScale = 1.3 + 0.11 * (centiMTG / 100.0); - } + // x moves in y seconds (+ z increment) + else { + optScale = std::min((0.88 + ply / 116.4) / (centiMTG / 100.0), 0.88 * time / timeLeft); + maxScale = 1.3 + 0.11 * (centiMTG / 100.0); + } - // Limit the maximum possible time for this move - optimumTime = TimePoint(optScale * timeLeft); - maximumTime = TimePoint(std::min(0.825179 * time - moveOverhead, - maxScale * optimumTime)) - - 10; + // Limit the maximum possible time for this move + optimumTime = TimePoint(optScale * timeLeft); + maximumTime = TimePoint(std::min(0.825179 * time - moveOverhead, maxScale * optimumTime)) - 10; } } // namespace engine::timeman diff --git a/timeman.h b/timeman.h index ca90fe2..6a09fdb 100644 --- a/timeman.h +++ b/timeman.h @@ -8,47 +8,41 @@ constexpr TimePoint INFINITE_TIME = 864000000; // LimitsType struct stores information sent by the caller about the analysis // required. inline TimePoint now() { - return std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count(); + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } struct LimitsType { - // Init explicitly due to broken value-initialization of non POD in MSVC - LimitsType() { - time[chess::WHITE] = time[chess::BLACK] = inc[chess::WHITE] = - inc[chess::BLACK] = movetime = TimePoint(0); - movestogo = mate = perft = infinite = 0; - depth = 64; - nodes = 0; - ponderMode = false; - } + // Init explicitly due to broken value-initialization of non POD in MSVC + LimitsType() { + time[chess::WHITE] = time[chess::BLACK] = inc[chess::WHITE] = inc[chess::BLACK] = movetime = TimePoint(0); + movestogo = mate = perft = infinite = 0; + depth = 64; + nodes = 0; + ponderMode = false; + } - bool use_time_management() const { - return time[chess::WHITE] || time[chess::BLACK]; - } + bool use_time_management() const { return time[chess::WHITE] || time[chess::BLACK]; } - std::vector searchmoves; - TimePoint time[chess::COLOR_NB], inc[chess::COLOR_NB], movetime, startTime; - int movestogo, depth, mate, perft, infinite; - uint64_t nodes; - bool ponderMode; + std::vector searchmoves; + TimePoint time[chess::COLOR_NB], inc[chess::COLOR_NB], movetime, startTime; + int movestogo, depth, mate, perft, infinite; + uint64_t nodes; + bool ponderMode; }; class TimeManagement { -public: - void init(LimitsType &limits, chess::Color us, int ply, - double &originalTimeAdjust); + public: + void init(LimitsType &limits, chess::Color us, int ply, double &originalTimeAdjust); - TimePoint optimum() const; - TimePoint maximum() const; - TimePoint elapsed() const { return elapsed_time(); } - TimePoint elapsed_time() const { return now() - startTime; }; + TimePoint optimum() const; + TimePoint maximum() const; + TimePoint elapsed() const { return elapsed_time(); } + TimePoint elapsed_time() const { return now() - startTime; }; - void clear(); + void clear(); -private: - TimePoint startTime; - TimePoint optimumTime; - TimePoint maximumTime; + private: + TimePoint startTime; + TimePoint optimumTime; + TimePoint maximumTime; }; } // namespace engine::timeman \ No newline at end of file diff --git a/tt.cpp b/tt.cpp index f084236..f95ec6b 100644 --- a/tt.cpp +++ b/tt.cpp @@ -6,70 +6,69 @@ #endif using namespace engine; static inline uint64_t index_for_hash(uint64_t hash, uint64_t buckets) { - if (buckets == 0) - return 0; + if (buckets == 0) + return 0; #if defined(_MSC_VER) - // MSVC: use _umul128 to get high 64 bits of 128-bit product - unsigned long long high = 0; - (void)_umul128((unsigned long long)hash, (unsigned long long)buckets, &high); - return (uint64_t)high; + // MSVC: use _umul128 to get high 64 bits of 128-bit product + unsigned long long high = 0; + (void)_umul128((unsigned long long)hash, (unsigned long long)buckets, &high); + return (uint64_t)high; #elif defined(__SIZEOF_INT128__) - // GCC/Clang: use __uint128_t - __uint128_t prod = (__uint128_t)hash * (__uint128_t)buckets; - return (uint64_t)(prod >> 64); + // GCC/Clang: use __uint128_t + __uint128_t prod = (__uint128_t)hash * (__uint128_t)buckets; + return (uint64_t)(prod >> 64); #else - // Portable fallback (rare): fall back to modulo if no 128-bit or _umul128 - // available. This is only used on very uncommon toolchains; primary - // implementations above avoid division. - return hash % buckets; + uint64_t aL = uint32_t(hash), aH = hash >> 32; + uint64_t bL = uint32_t(buckets), bH = buckets >> 32; + uint64_t c1 = (aL * bL) >> 32; + uint64_t c2 = aH * bL + c1; + uint64_t c3 = aL * bH + uint32_t(c2); + return aH * bH + (c2 >> 32) + (c3 >> 32); #endif } void TranspositionTable::newSearch() { time++; } -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; +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 = index_for_hash(hash, buckets); - if (index >= buckets - 1) - index = buckets - 2; // Ensure we don't overflow + uint64_t index = index_for_hash(hash, buckets); - TTEntry &e0 = table[index], &e1 = table[index + 1]; - // Store the entry - for (TTEntry *e : {&e0, &e1}) { - if (e->key == hash || e->getDepth() < depth) { - e->key = hash; - e->setPackedFields(score, depth, flag, best.raw(), time); + TTEntry &e0 = table[2 * index], &e1 = table[2 * index + 1]; + // Store the entry + for (TTEntry *e : { &e0, &e1 }) { + if (e->key == hash || e->getDepth() < depth) { + e->key = hash; + e->setPackedFields(score, depth, flag, best.raw(), time); - return; + 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->key = hash; - oldest->setPackedFields(score, depth, flag, best.raw(), time); + // 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->key = hash; + oldest->setPackedFields(score, depth, flag, best.raw(), time); } TTEntry *TranspositionTable::lookup(uint64_t hash) { - // 2 entries per bucket - if (buckets == 0) - return nullptr; + if (buckets == 0 || hash == 0) + return nullptr; + + uint64_t bucket = index_for_hash(hash, buckets); + + TTEntry &e0 = table[2 * bucket]; + TTEntry &e1 = table[2 * bucket + 1]; - uint64_t index = index_for_hash(hash, buckets); - if (index >= buckets - 1) - index = buckets - 2; // Ensure we don't overflow + if (e0.key == hash) + return &e0; - TTEntry &e0 = table[index], &e1 = table[index + 1]; - // Check the entries - for (TTEntry *e : {&e0, &e1}) { - if (e->key == hash && e->getGeneration() == this->time) - return e; - } - return nullptr; + if (e1.key == hash) + return &e1; + + return nullptr; } \ No newline at end of file diff --git a/tt.h b/tt.h index 6c69dc3..823452b 100644 --- a/tt.h +++ b/tt.h @@ -6,174 +6,138 @@ namespace engine { enum TTFlag : uint8_t { EXACT = 0, LOWERBOUND = 1, UPPERBOUND = 2 }; struct TTEntry { - uint64_t key; - uint64_t pack; // 16-bit score, 8-bit depth, 3-bit flags, 16-bit move, 21 bits - // for generation - - // bit layout constants - static constexpr unsigned SCORE_SHIFT = 0; - static constexpr unsigned SCORE_BITS = 16; - static constexpr uint64_t SCORE_MASK = ((uint64_t(1) << SCORE_BITS) - 1) - << SCORE_SHIFT; - - static constexpr unsigned DEPTH_SHIFT = 16; - static constexpr unsigned DEPTH_BITS = 8; - static constexpr uint64_t DEPTH_MASK = ((uint64_t(1) << DEPTH_BITS) - 1) - << DEPTH_SHIFT; - - static constexpr unsigned FLAG_SHIFT = 24; - static constexpr unsigned FLAG_BITS = 3; - static constexpr uint64_t FLAG_MASK = ((uint64_t(1) << FLAG_BITS) - 1) - << FLAG_SHIFT; - - static constexpr unsigned MOVE_SHIFT = 27; - static constexpr unsigned MOVE_BITS = 16; - static constexpr uint64_t MOVE_MASK = ((uint64_t(1) << MOVE_BITS) - 1) - << MOVE_SHIFT; - - static constexpr unsigned GEN_SHIFT = 43; - static constexpr unsigned GEN_BITS = 21; - static constexpr uint64_t GEN_MASK = ((uint64_t(1) << GEN_BITS) - 1) - << GEN_SHIFT; - - // getters - inline int16_t getScore() const noexcept { - return static_cast((pack & SCORE_MASK) >> SCORE_SHIFT); - } - - inline uint8_t getDepth() const noexcept { - return static_cast((pack & DEPTH_MASK) >> DEPTH_SHIFT); - } - - inline TTFlag getFlag() const noexcept { - return static_cast((pack & FLAG_MASK) >> FLAG_SHIFT); - } - - inline uint16_t getMove() const noexcept { - return static_cast((pack & MOVE_MASK) >> MOVE_SHIFT); - } - - inline uint32_t getGeneration() const noexcept { - return static_cast((pack & GEN_MASK) >> GEN_SHIFT); - } - - // setters - inline void setScore(int16_t score) noexcept { - // preserve two's complement by casting through uint16_t - const uint64_t v = - (static_cast(static_cast(score)) << SCORE_SHIFT) & - SCORE_MASK; - pack = (pack & ~SCORE_MASK) | v; - } - - inline void setDepth(uint8_t depth) noexcept { - const uint64_t v = - (static_cast(depth) << DEPTH_SHIFT) & DEPTH_MASK; - pack = (pack & ~DEPTH_MASK) | v; - } - - inline void setFlag(TTFlag flag) noexcept { - const uint64_t v = - (static_cast(static_cast(flag)) << FLAG_SHIFT) & - FLAG_MASK; - pack = (pack & ~FLAG_MASK) | v; - } - - inline void setMove(uint16_t move) noexcept { - const uint64_t v = (static_cast(move) << MOVE_SHIFT) & MOVE_MASK; - pack = (pack & ~MOVE_MASK) | v; - } - - inline void setGeneration(uint32_t gen) noexcept { - const uint64_t v = (static_cast(gen) << GEN_SHIFT) & GEN_MASK; - pack = (pack & ~GEN_MASK) | v; - } - - // convenience: set all packed fields at once - inline void setPackedFields(int16_t score, uint8_t depth, TTFlag flag, - uint16_t move, uint32_t gen) noexcept { - const uint64_t s = - (static_cast(static_cast(score)) << SCORE_SHIFT) & - SCORE_MASK; - const uint64_t d = - (static_cast(depth) << DEPTH_SHIFT) & DEPTH_MASK; - const uint64_t f = - (static_cast(static_cast(flag)) << FLAG_SHIFT) & - FLAG_MASK; - const uint64_t m = (static_cast(move) << MOVE_SHIFT) & MOVE_MASK; - const uint64_t g = (static_cast(gen) << GEN_SHIFT) & GEN_MASK; - pack = s | d | f | m | g; - } - - inline uint32_t timestamp() const noexcept { return getGeneration(); } + uint64_t key; + uint64_t pack; // 16-bit score, 8-bit depth, 3-bit flags, 16-bit move, 21 bits + // for generation + + // bit layout constants + static constexpr unsigned SCORE_SHIFT = 0; + static constexpr unsigned SCORE_BITS = 16; + static constexpr uint64_t SCORE_MASK = ((uint64_t(1) << SCORE_BITS) - 1) << SCORE_SHIFT; + + static constexpr unsigned DEPTH_SHIFT = 16; + static constexpr unsigned DEPTH_BITS = 8; + static constexpr uint64_t DEPTH_MASK = ((uint64_t(1) << DEPTH_BITS) - 1) << DEPTH_SHIFT; + + static constexpr unsigned FLAG_SHIFT = 24; + static constexpr unsigned FLAG_BITS = 3; + static constexpr uint64_t FLAG_MASK = ((uint64_t(1) << FLAG_BITS) - 1) << FLAG_SHIFT; + + static constexpr unsigned MOVE_SHIFT = 27; + static constexpr unsigned MOVE_BITS = 16; + static constexpr uint64_t MOVE_MASK = ((uint64_t(1) << MOVE_BITS) - 1) << MOVE_SHIFT; + + static constexpr unsigned GEN_SHIFT = 43; + static constexpr unsigned GEN_BITS = 21; + static constexpr uint64_t GEN_MASK = ((uint64_t(1) << GEN_BITS) - 1) << GEN_SHIFT; + + // getters + inline int16_t getScore() const noexcept { return static_cast((pack & SCORE_MASK) >> SCORE_SHIFT); } + + inline uint8_t getDepth() const noexcept { return static_cast((pack & DEPTH_MASK) >> DEPTH_SHIFT); } + + inline TTFlag getFlag() const noexcept { return static_cast((pack & FLAG_MASK) >> FLAG_SHIFT); } + + inline uint16_t getMove() const noexcept { return static_cast((pack & MOVE_MASK) >> MOVE_SHIFT); } + + inline uint32_t getGeneration() const noexcept { return static_cast((pack & GEN_MASK) >> GEN_SHIFT); } + + // setters + inline void setScore(int16_t score) noexcept { + // preserve two's complement by casting through uint16_t + const uint64_t v = (static_cast(static_cast(score)) << SCORE_SHIFT) & SCORE_MASK; + pack = (pack & ~SCORE_MASK) | v; + } + + inline void setDepth(uint8_t depth) noexcept { + const uint64_t v = (static_cast(depth) << DEPTH_SHIFT) & DEPTH_MASK; + pack = (pack & ~DEPTH_MASK) | v; + } + + inline void setFlag(TTFlag flag) noexcept { + const uint64_t v = (static_cast(static_cast(flag)) << FLAG_SHIFT) & FLAG_MASK; + pack = (pack & ~FLAG_MASK) | v; + } + + inline void setMove(uint16_t move) noexcept { + const uint64_t v = (static_cast(move) << MOVE_SHIFT) & MOVE_MASK; + pack = (pack & ~MOVE_MASK) | v; + } + + inline void setGeneration(uint32_t gen) noexcept { + const uint64_t v = (static_cast(gen) << GEN_SHIFT) & GEN_MASK; + pack = (pack & ~GEN_MASK) | v; + } + + // convenience: set all packed fields at once + inline void setPackedFields(int16_t score, uint8_t depth, TTFlag flag, uint16_t move, uint32_t gen) noexcept { + const uint64_t s = (static_cast(static_cast(score)) << SCORE_SHIFT) & SCORE_MASK; + const uint64_t d = (static_cast(depth) << DEPTH_SHIFT) & DEPTH_MASK; + const uint64_t f = (static_cast(static_cast(flag)) << FLAG_SHIFT) & FLAG_MASK; + const uint64_t m = (static_cast(move) << MOVE_SHIFT) & MOVE_MASK; + const uint64_t g = (static_cast(gen) << GEN_SHIFT) & GEN_MASK; + pack = s | d | f | m | g; + } + + inline uint32_t timestamp() const noexcept { return getGeneration(); } }; 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(); + TTEntry *table; + size_t buckets; // number of buckets (pairs) + uint32_t time; + + public: + size_t size; // total number of TTEntry elements (must be even) + TranspositionTable() : table(nullptr), buckets(0), time(0), size(0) {} + + TranspositionTable(size_t sizeInMB) : time(0) { + size = sizeInMB * 1048576LL / sizeof(TTEntry); + if (size % 2 != 0) + size--; // Ensure even size + buckets = size / 2; + table = new TTEntry[size]; + clear(); } - std::memcpy(new_table, old_table, - sizeof(TTEntry) * std::min(size, old_size)); - delete[] old_table; - table = new_table; - buckets = size / 2; - } + ~TranspositionTable() { delete[] table; } + + void resize(int sizeInMB) { + size_t new_size = sizeInMB * 1048576LL / sizeof(TTEntry); + if (new_size % 2 != 0) + new_size--; - void newSearch(); - void store(uint64_t hash, chess::Move best, int16_t score, int8_t depth, - TTFlag flag); + TTEntry *new_table = new (std::nothrow) TTEntry[new_size](); + if (!new_table) { + throw std::bad_alloc(); + } - inline void clear() { std::fill_n(table, size, TTEntry{}); } + delete[] table; + table = new_table; + size = new_size; + buckets = size / 2; + } - TTEntry *lookup(uint64_t hash); + void newSearch(); + void store(uint64_t hash, chess::Move best, int16_t score, int8_t depth, TTFlag flag); - // Returns hash usage as percentage [0..100] based on occupied buckets. - inline int hashfull() const noexcept { - if (!table || buckets <= 0) - return 0; + inline void clear() { std::fill_n(table, size, TTEntry{}); } - int used = 0; - for (int i = 0; i < buckets; ++i) { - const TTEntry &a = table[2 * i]; - const TTEntry &b = table[2 * i + 1]; - if (a.key != 0 || b.key != 0) - ++used; - } + TTEntry *lookup(uint64_t hash); - return (used * 1000) / buckets; - } + // Returns hash usage as percentage [0..100] based on occupied buckets. + inline int hashfull() const noexcept { + if (!table || buckets <= 0) + return 0; + + int used = 0; + for (int i = 0; i < buckets; ++i) { + const TTEntry &a = table[2 * i]; + const TTEntry &b = table[2 * i + 1]; + if (a.key != 0 || b.key != 0) + ++used; + } + + return (used * 1000LL) / buckets; + } }; } // namespace engine diff --git a/tune.cpp b/tune.cpp index d29fed6..c118521 100644 --- a/tune.cpp +++ b/tune.cpp @@ -19,11 +19,15 @@ #include "tune.h" #include +#include +#include +#include #include #include #include #include #include +#include #include "ucioption.h" @@ -39,71 +43,190 @@ std::map TuneResults; std::optional on_tune(const Option &o) { - if (!Tune::update_on_last || LastOption == &o) - Tune::read_options(); + if (!Tune::update_on_last || LastOption == &o) + Tune::read_options(); - return std::nullopt; + return std::nullopt; } } // namespace -void Tune::make_option(OptionsMap *opts, const string &n, int v, - const SetRange &r) { +void Tune::make_option(OptionsMap *opts, const string &n, int v, const SetRange &r) { - // Do not generate option when there is nothing to tune (ie. min = max) - if (r(v).first == r(v).second) - return; + // Do not generate option when there is nothing to tune (ie. min = max) + if (r(v).first == r(v).second) + return; - if (TuneResults.count(n)) - v = TuneResults[n]; + if (TuneResults.count(n)) + v = TuneResults[n]; - opts->add(n, Option(v, r(v).first, r(v).second, on_tune)); - LastOption = &((*opts)[n]); + opts->add(n, Option(v, r(v).first, r(v).second, on_tune)); + LastOption = &((*opts)[n]); + auto [a, b] = r(v); + if (!(a <= v && v <= b)) { + std::cerr << "wrong bounds, name: " << n << '\n'; + std::exit(1); + } - // Print formatted parameters, ready to be copy-pasted in Fishtest - std::cout << n << "," // + // Print formatted parameters, ready to be copy-pasted in Fishtest + std::cout << n << "," // #ifdef OPENBENCH_SUPPORT - // or OpenBench - << "int" << "," + // or OpenBench + << "int" << "," #endif - << v << "," // - << r(v).first << "," // - << r(v).second << "," // - << (r(v).second - r(v).first) / 20.0 << "," // - << "0.0020" << std::endl; + << v << "," // + << a << "," // + << b << "," // + << (b - a) / 20.0 << "," // + << "0.0020" << '\n'; } string Tune::next(string &names, bool pop) { - string name; + string name; - do { - string token = names.substr(0, names.find(',')); + do { + string token = names.substr(0, names.find(',')); - if (pop) - names.erase(0, token.size() + 1); + if (pop) + names.erase(0, token.size() + 1); - std::stringstream ws(token); - name += (ws >> token, token); // Remove trailing whitespace + std::stringstream ws(token); + name += (ws >> token, token); // Remove trailing whitespace - } while (std::count(name.begin(), name.end(), '(') - - std::count(name.begin(), name.end(), ')')); + } while (std::count(name.begin(), name.end(), '(') - std::count(name.begin(), name.end(), ')')); - return name; + return name; } -template <> void Tune::Entry::init_option() { - make_option(options, name, value, range); -} +template <> void Tune::Entry::init_option() { make_option(options, name, value, range); } template <> void Tune::Entry::read_option() { - if (options->count(name)) - value = int((*options)[name]); + if (options->count(name)) + value = int((*options)[name]); } // Instead of a variable here we have a PostUpdate function: just call it template <> void Tune::Entry::init_option() {} template <> void Tune::Entry::read_option() { value(); } +template <> void Tune::Entry::print_option(std::ostream &) const {} + +template <> void Tune::Entry::print_option(std::ostream &) const {} + +namespace { + +struct EntryInfo { + std::string base; + int value; + std::vector idx; +}; + +EntryInfo parse_entry(const std::string &name, int value) { + EntryInfo info; + size_t bracket = name.find('['); + if (bracket == std::string::npos) { + info.base = name; + info.value = value; + return info; + } + info.base = name.substr(0, bracket); + while (bracket != std::string::npos) { + size_t end = name.find(']', bracket); + if (end == std::string::npos) + break; + info.idx.push_back(std::stoi(name.substr(bracket + 1, end - bracket - 1))); + bracket = name.find('[', end + 1); + } + info.value = value; + return info; +} + +void deduce_shape(const std::vector &entries, std::vector &shape) { + if (entries.empty()) + return; + int ndim = (int)entries[0].idx.size(); + shape.assign(ndim, 0); + for (auto &e : entries) + for (int d = 0; d < ndim; d++) + if (e.idx[d] >= shape[d]) + shape[d] = e.idx[d] + 1; +} + +void print_array(std::ostream &os, const std::vector &flat, const int *shape, int ndim, int dim, int &pos) { + if (dim == ndim - 1) { + os << "{ "; + for (int i = 0; i < shape[dim]; i++) { + if (i) + os << ", "; + os << flat[pos++]; + } + os << " }"; + } else { + os << "{ "; + for (int i = 0; i < shape[dim]; i++) { + if (i) + os << ", "; + print_array(os, flat, shape, ndim, dim + 1, pos); + } + os << " }"; + } +} + +} // namespace + +void Tune::export_weights(std::ostream &os) { + std::vector scalars; + std::map> groups; + + for (auto &e : instance().list) { + auto entry = dynamic_cast *>(e.get()); + if (!entry) + continue; + auto info = parse_entry(entry->name, entry->value); + if (info.idx.empty()) + scalars.push_back(info); + else + groups[info.base].push_back(info); + } + + os << "#ifndef WEIGHTS_H\n"; + os << "#define WEIGHTS_H\n"; + os << "#include \"eval.h\"\n"; + os << "namespace engine::eval {\n"; + + for (auto &e : scalars) + os << "inline Value " << e.base << " = " << e.value << ";\n"; + + for (auto &[base, entries] : groups) { + std::vector shape; + deduce_shape(entries, shape); + int ndim = (int)shape.size(); + // Build flat buffer, zero-initialized, fill from entries + int total = 1; + for (int d = 0; d < ndim; d++) + total *= shape[d]; + std::vector flat(total, 0); + for (auto &e : entries) { + int linear = 0, stride = total; + for (int d = 0; d < ndim; d++) { + stride /= shape[d]; + linear += e.idx[d] * stride; + } + flat[linear] = e.value; + } + os << "inline Value " << base; + for (int d = 0; d < ndim; d++) + os << "[" << shape[d] << "]"; + os << " = "; + int pos = 0; + print_array(os, flat, shape.data(), ndim, 0, pos); + os << ";\n"; + } + + os << "} // namespace engine::eval\n"; + os << "#endif\n"; +} + } // namespace engine // Init options with tuning session results instead of default values. Useful to diff --git a/tune.h b/tune.h index d6a0022..328d80d 100644 --- a/tune.h +++ b/tune.h @@ -20,6 +20,7 @@ #define TUNE_H_INCLUDED #include +#include #include #include #include // IWYU pragma: keep @@ -34,17 +35,15 @@ using Range = std::pair; // Option's min-max values using RangeFun = Range(int); // Default Range function, to calculate Option's min-max values -inline Range default_range(int v) { - return v > 0 ? Range(0, 2 * v) : Range(2 * v, 0); -} +inline Range default_range(int v) { return v > 0 ? Range(0, 2 * v) : Range(2 * v, 0); } struct SetRange { - explicit SetRange(RangeFun f) : fun(f) {} - SetRange(int min, int max) : fun(nullptr), range(min, max) {} - Range operator()(int v) const { return fun ? fun(v) : range; } + explicit SetRange(RangeFun f) : fun(f) {} + SetRange(int min, int max) : fun(nullptr), range(min, max) {} + Range operator()(int v) const { return fun ? fun(v) : range; } - RangeFun *fun; - Range range; + RangeFun *fun; + Range range; }; #define SetDefaultRange SetRange(default_range) @@ -78,105 +77,104 @@ struct SetRange { class Tune { - using PostUpdate = void(); // Post-update function - - Tune() { read_results(); } - Tune(const Tune &) = delete; - void operator=(const Tune &) = delete; - void read_results(); - - static Tune &instance() { - static Tune t; - return t; - } // Singleton - - // Use polymorphism to accommodate Entry of different types in the same vector - struct EntryBase { - virtual ~EntryBase() = default; - virtual void init_option() = 0; - virtual void read_option() = 0; - }; - - template struct Entry : public EntryBase { - - static_assert(!std::is_const_v, "Parameter cannot be const!"); - - static_assert(std::is_same_v || std::is_same_v, - "Parameter type not supported!"); - - Entry(const std::string &n, T &v, const SetRange &r) - : name(n), value(v), range(r) {} - void operator=(const Entry &) = delete; // Because 'value' is a reference - void init_option() override; - void read_option() override; - - std::string name; - T &value; - SetRange range; - }; - - // Our facility to fill the container, each Entry corresponds to a parameter - // to tune. We use variadic templates to deal with an unspecified number of - // entries, each one of a possible different type. - static std::string next(std::string &names, bool pop = true); - - int add(const SetRange &, std::string &&) { return 0; } - - template - int add(const SetRange &range, std::string &&names, T &value, - Args &&...args) { - list.push_back( - std::unique_ptr(new Entry(next(names), value, range))); - return add(range, std::move(names), args...); - } - - // Template specialization for arrays: recursively handle multi-dimensional - // arrays - template - int add(const SetRange &range, std::string &&names, T (&value)[N], - Args &&...args) { - for (size_t i = 0; i < N; i++) - add(range, next(names, i == N - 1) + "[" + std::to_string(i) + "]", - value[i]); - return add(range, std::move(names), args...); - } - - // Template specialization for SetRange - template - int add(const SetRange &, std::string &&names, SetRange &value, - Args &&...args) { - return add(value, (next(names), std::move(names)), args...); - } - - static void make_option(OptionsMap *options, const std::string &n, int v, - const SetRange &r); - - std::vector> list; - -public: - template - static int add(const std::string &names, Args &&...args) { - return instance().add(SetDefaultRange, names.substr(1, names.size() - 2), - args...); // Remove trailing parenthesis - } - static void init(OptionsMap &o) { - options = &o; - for (auto &e : instance().list) - e->init_option(); - read_options(); - } // Deferred, due to UCIEngine::Options access - static void read_options() { - for (auto &e : instance().list) - e->read_option(); - } - - static bool update_on_last; - static OptionsMap *options; + using PostUpdate = void(); // Post-update function + + Tune() { read_results(); } + Tune(const Tune &) = delete; + void operator=(const Tune &) = delete; + void read_results(); + + static Tune &instance() { + static Tune t; + return t; + } // Singleton + + public: + // Use polymorphism to accommodate Entry of different types in the same vector + struct EntryBase { + virtual ~EntryBase() = default; + virtual void init_option() = 0; + virtual void read_option() = 0; + virtual void print_option(std::ostream &) const = 0; + }; + + template struct Entry : public EntryBase { + + static_assert(!std::is_const_v, "Parameter cannot be const!"); + + static_assert(std::is_same_v || std::is_same_v, "Parameter type not supported!"); + + Entry(const std::string &n, T &v, const SetRange &r) : name(n), value(v), range(r) {} + void operator=(const Entry &) = delete; // Because 'value' is a reference + void init_option() override; + void read_option() override; + void print_option(std::ostream &) const override; + + std::string name; + T &value; + SetRange range; + }; + + static std::vector> &get_list() { return instance().list; } + + private: + // Our facility to fill the container, each Entry corresponds to a parameter + // to tune. We use variadic templates to deal with an unspecified number of + // entries, each one of a possible different type. + static std::string next(std::string &names, bool pop = true); + + int add(const SetRange &, std::string &&) { return 0; } + + template int add(const SetRange &range, std::string &&names, T &value, Args &&...args) { + list.push_back(std::unique_ptr(new Entry(next(names), value, range))); + return add(range, std::move(names), args...); + } + + // Template specialization for arrays: recursively handle multi-dimensional + // arrays + template + int add(const SetRange &range, std::string &&names, T (&value)[N], Args &&...args) { + for (size_t i = 0; i < N; i++) + add(range, next(names, i == N - 1) + "[" + std::to_string(i) + "]", value[i]); + return add(range, std::move(names), args...); + } + + // Template specialization for SetRange + template int add(const SetRange &, std::string &&names, SetRange &value, Args &&...args) { + return add(value, (next(names), std::move(names)), args...); + } + + static void make_option(OptionsMap *options, const std::string &n, int v, const SetRange &r); + + std::vector> list; + + public: + template static int add(const std::string &names, Args &&...args) { + return instance().add(SetDefaultRange, names.substr(1, names.size() - 2), + args...); // Remove trailing parenthesis + } + static void init(OptionsMap &o) { + options = &o; + for (auto &e : instance().list) + e->init_option(); + read_options(); + } // Deferred, due to UCIEngine::Options access + template static void visit_entries(F &&f) { + for (auto &e : instance().list) + f(e.get()); + } + static void export_weights(std::ostream &); + static void read_options() { + for (auto &e : instance().list) + e->read_option(); + } + + static bool update_on_last; + static OptionsMap *options; }; template constexpr void tune_check_args(Args &&...) { - static_assert((!std::is_fundamental_v && ...), - "TUNE macro arguments wrong"); + static_assert((!std::is_fundamental_v && ...), "TUNE macro arguments wrong"); } // Some macro magic :-) we define a dummy int variable that the compiler @@ -184,11 +182,18 @@ template constexpr void tune_check_args(Args &&...) { #define STRINGIFY(x) #x #define UNIQUE2(x, y) x##y #define UNIQUE(x, y) UNIQUE2(x, y) // Two indirection levels to expand __LINE__ -#define TUNE(...) \ - int UNIQUE(p, __LINE__) = []() -> int { \ - tune_check_args(__VA_ARGS__); \ - return engine::Tune::add(STRINGIFY((__VA_ARGS__)), __VA_ARGS__); \ - }(); +#ifdef _MSC_VER +// Completely bypass the lambda bug for Microsoft Visual Studio +#define TUNE(...) \ + int UNIQUE(p, __LINE__) = (tune_check_args(__VA_ARGS__), engine::Tune::add(STRINGIFY((__VA_ARGS__)), __VA_ARGS__)); +#else +// Keep the original standard lambda format for GCC / Clang (Linux/Mac) +#define TUNE(...) \ + int UNIQUE(p, __LINE__) = []() -> int { \ + tune_check_args(__VA_ARGS__); \ + return engine::Tune::add(STRINGIFY((__VA_ARGS__)), __VA_ARGS__); \ + }(); +#endif #define UPDATE_ON_LAST() bool UNIQUE(p, __LINE__) = Tune::update_on_last = true diff --git a/tune_cmd.cpp b/tune_cmd.cpp new file mode 100644 index 0000000..fe5926c --- /dev/null +++ b/tune_cmd.cpp @@ -0,0 +1,960 @@ +#include "tune_cmd.h" +#include "Weights.h" +#include "eval.h" +#include "tune.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace engine { + +namespace { + +double sigmoid(double cp, double K = 0.004) { return 1.0 / (1.0 + std::exp(-K * cp)); } + +struct TuneParam { + std::string name; + int *value_ptr; + int lower; + int upper; +}; + +std::vector collect_params() { + std::vector params; + for (auto &e : Tune::get_list()) { + if (auto entry = dynamic_cast *>(e.get())) { + auto bounds = entry->range(entry->value); + params.push_back({ entry->name, &entry->value, bounds.first, bounds.second }); + } + } + return params; +} + +struct TuneData { + std::vector fen_buf; + std::vector offsets; + std::vector results; + + size_t size() const { return results.size(); } + + void add(const std::string &fen, double result) { + offsets.push_back(fen_buf.size()); + fen_buf.insert(fen_buf.end(), fen.begin(), fen.end()); + fen_buf.push_back('\0'); + results.push_back(result); + } + + double result(size_t i) const { return results[i]; } +}; + +double eval_loss(const TuneData &data, const std::vector &indices, int num_threads = 0) { + if (indices.empty()) + return 0.0; + if (num_threads <= 0) + num_threads = (int)std::thread::hardware_concurrency(); + if (num_threads < 1) + num_threads = 1; + + size_t n = indices.size(); + std::vector partial(num_threads, 0.0); + std::vector threads; + + auto worker = [&](int tid, size_t start, size_t end) { + chess::Position pos; + std::string fen; + double sum = 0.0; + for (size_t j = start; j < end; j++) { + size_t idx = indices[j]; + fen.assign(data.fen_buf.data() + data.offsets[idx]); + pos.set_fen(fen); + Value score = eval::eval(pos); + double diff = sigmoid(score) - data.result(idx); + sum += diff * diff; + } + partial[tid] = sum; + }; + + size_t chunk = (n + num_threads - 1) / num_threads; + for (int t = 0; t < num_threads; t++) { + size_t start = t * chunk; + size_t end = std::min(start + chunk, n); + if (start >= n) + break; + threads.emplace_back(worker, t, start, end); + } + + for (auto &t : threads) + t.join(); + + double loss = std::accumulate(partial.begin(), partial.end(), 0.0); + return loss / n; +} + +std::unordered_map build_addr_map(const std::vector ¶ms) { + std::unordered_map map; + map.reserve(params.size()); + for (int i = 0; i < (int)params.size(); i++) + map[params[i].value_ptr] = i; + return map; +} + +// Compute passed-pawn mask for a single square and color +chess::Bitboard passed_pawn_mask(chess::Color c, chess::Square sq) { + chess::Bitboard mask = 0; + chess::File f = chess::file_of(sq); + int startF = std::max(0, (int)f - 1); + int endF = std::min(7, (int)f + 1); + for (int adjF = startF; adjF <= endF; adjF++) { + if (c == chess::WHITE) { + for (int r = (int)chess::rank_of(sq) + 1; r <= 7; r++) + mask |= chess::attacks::MASK_FILE[adjF] & chess::attacks::MASK_RANK[r]; + } else { + for (int r = (int)chess::rank_of(sq) - 1; r >= 0; r--) + mask |= chess::attacks::MASK_FILE[adjF] & chess::attacks::MASK_RANK[r]; + } + } + return mask; +} + +static constexpr int KnightPhase = 1; +static constexpr int BishopPhase = 1; +static constexpr int RookPhase = 2; +static constexpr int QueenPhase = 4; +static constexpr int TotalPhase = KnightPhase * 4 + BishopPhase * 4 + RookPhase * 4 + QueenPhase * 2; + +int compute_game_phase(const chess::Position &board) { + int phase = 0; + chess::Bitboard occ = board.occ(); + while (occ) { + chess::Square sq = chess::Square(chess::pop_lsb(occ)); + auto pt = chess::piece_of(board.at(sq)); + if (pt == chess::KNIGHT) + phase += KnightPhase; + else if (pt == chess::BISHOP) + phase += BishopPhase; + else if (pt == chess::ROOK) + phase += RookPhase; + else if (pt == chess::QUEEN) + phase += QueenPhase; + } + return (phase * 256 + TotalPhase / 2) / TotalPhase; +} + +void accumulate_gradient(const chess::Position &board, + double common_factor, + const std::unordered_map &addr_to_idx, + double phase_mg, + double phase_eg, + double stm_sign, + std::vector &gradient) { + using namespace chess; + using namespace engine::eval; + + Bitboard pawnBB[2] = { board.pieces(PAWN, WHITE), board.pieces(PAWN, BLACK) }; + Bitboard pawnAtks[2] = { attacks::pawn(pawnBB[WHITE]), attacks::pawn(pawnBB[BLACK]) }; + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Square qStart = c == WHITE ? SQ_D1 : SQ_D8; + if (!(board.pieces(QUEEN, c) & (1ULL << qStart))) { + Bitboard backRank = c == WHITE ? attacks::MASK_RANK[0] : attacks::MASK_RANK[7]; + int undeveloped = popcount((board.pieces(KNIGHT, c) | board.pieces(BISHOP, c)) & backRank); + if (undeveloped >= 2) { + auto it = addr_to_idx.find(&earlyQueenPenalty); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * (s * stm_sign) * (-undeveloped); + } + } + } + + { + Bitboard pinMask = board.pin_mask(); + Bitboard occ = board.occ(), occ2 = occ; + + while (occ) { + Square sq = Square(pop_lsb(occ)); + auto p = board.at(sq); + Color pc = color_of(p); + double _sign = (pc == WHITE) ? 1.0 : -1.0; + double eff = _sign * stm_sign; + Square _sq = _sign == 1.0 ? sq : square_mirror(sq); + auto pt = piece_of(p); + if (pt == NO_PIECE_TYPE) + continue; + + { + auto *mg_arr = mgPst[pt]; + auto *eg_arr = egPst[pt]; + auto it_mg = addr_to_idx.find(&mg_arr[_sq]); + auto it_eg = addr_to_idx.find(&eg_arr[_sq]); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff * phase_mg; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff * phase_eg; + } + + auto add_mat = [&](auto *addr) { + auto it = addr_to_idx.find(addr); + if (it != addr_to_idx.end()) { + gradient[it->second] += common_factor * eff * phase_mg; + gradient[it->second] += common_factor * eff * phase_eg; + } + }; + switch (pt) { + case PAWN: + add_mat(&PawnValue); + break; + case KNIGHT: + add_mat(&KnightValue); + break; + case BISHOP: + add_mat(&BishopValue); + break; + case ROOK: + add_mat(&RookValue); + break; + case QUEEN: + add_mat(&QueenValue); + break; + default: + break; + } + + Bitboard atks = 0; + if (pt == KNIGHT) + atks = chess::attacks::knight(sq); + else if (pt == BISHOP) + atks = chess::attacks::bishop(sq, occ2); + else if (pt == ROOK) + atks = chess::attacks::rook(sq, occ2); + else if (pt == QUEEN) + atks = chess::attacks::queen(sq, occ2); + atks &= ~board.us(pc); + int mobility = popcount(atks); + int cmob = std::min(mobility, 7); + if ((1ULL << sq) & pinMask) + cmob = std::min(cmob / 4, 7); + { + auto it_mg = addr_to_idx.find(&mgMobilityCnt[pt][cmob]); + auto it_eg = addr_to_idx.find(&egMobilityCnt[pt][cmob]); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff * phase_mg; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff * phase_eg; + } + + if (pt != PAWN && pt != KING) { + int kd = square_distance(sq, board.kingSq(~pc)); + double coeff = 7 - kd; + auto it_mg = addr_to_idx.find(&kingTropismMg[pt]); + auto it_eg = addr_to_idx.find(&kingTropismEg[pt]); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff * coeff * phase_mg; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff * coeff * phase_eg; + } + + if (pt != PAWN && pt != KING) { + int kdist = square_distance(sq, board.kingSq(pc)); + int ki = std::min(kdist, 5); + double coeff = 1.0 / (1 + ki); + auto it_mg = addr_to_idx.find(&kingProtector[pt][0]); + auto it_eg = addr_to_idx.find(&kingProtector[pt][1]); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff * coeff * phase_mg; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff * coeff * phase_eg; + } + + if (pt == KNIGHT || pt == BISHOP) { + Rank r = rank_of(sq); + bool onOutpost = (pc == WHITE && r >= RANK_5) || (pc == BLACK && r <= RANK_4); + if (onOutpost && !(pawnAtks[~pc] & (1ULL << sq))) { + bool defended = (pawnAtks[pc] & (1ULL << sq)) != 0; + int idx = defended ? 1 : 0; + auto *arr = (pt == KNIGHT) ? outpostBonusKnight : outpostBonusBishop; + auto it = addr_to_idx.find(&arr[idx]); + if (it != addr_to_idx.end()) { + gradient[it->second] += common_factor * eff * phase_mg; + gradient[it->second] += common_factor * eff * phase_eg; + } + } + } + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + if (board.count(BISHOP, c) >= 2) { + double eff = s * stm_sign; + auto it_mg = addr_to_idx.find(&bishopPairMg); + auto it_eg = addr_to_idx.find(&bishopPairEg); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff * phase_mg; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff * phase_eg; + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Square fSq[2] = { relative_square(c, SQ_B2), relative_square(c, SQ_G2) }; + Square pSq[2] = { relative_square(c, SQ_B3), relative_square(c, SQ_G3) }; + for (int i = 0; i < 2; i++) { + if (board.at(fSq[i]) == BISHOP && board.at(fSq[i]) == c && board.at(pSq[i]) == PAWN && + board.at(pSq[i]) == c) { + auto it = addr_to_idx.find(&fianchettoBonus); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * (s * stm_sign); + } + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Square trapSq[2] = { relative_square(c, SQ_A2), relative_square(c, SQ_H2) }; + Square kingAdj[2] = { relative_square(c, SQ_B1), relative_square(c, SQ_G1) }; + for (int i = 0; i < 2; i++) { + if (board.at(trapSq[i]) == BISHOP && board.at(trapSq[i]) == c && board.kingSq(c) == kingAdj[i]) { + auto it = addr_to_idx.find(&trappedBishopPenalty); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * (-s * stm_sign); + } + } + } + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Color opp = ~c; + Square sqs[2] = { relative_square(c, SQ_A7), relative_square(c, SQ_H7) }; + Square pSqs[2] = { relative_square(c, SQ_B6), relative_square(c, SQ_G6) }; + for (int i = 0; i < 2; i++) { + if (board.at(sqs[i]) == BISHOP && board.at(sqs[i]) == c) { + if (board.at(pSqs[i]) == PAWN && board.at(pSqs[i]) == opp && + !board.is_attacked_by(c, sqs[i])) { + auto it = addr_to_idx.find(&trappedBishopPenalty); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * (-s * stm_sign); + } + } + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Bitboard rooks = board.pieces(ROOK, c); + while (rooks) { + Square sq = Square(pop_lsb(rooks)); + File f = file_of(sq); + Bitboard fileMask = attacks::MASK_FILE[f]; + bool hasOwn = (board.pieces(PAWN, c) & fileMask) != 0; + bool hasEnemy = (board.pieces(PAWN, ~c) & fileMask) != 0; + double eff = s * stm_sign; + if (!hasOwn && !hasEnemy) { + auto it_mg = addr_to_idx.find(&rookOpenFileMg); + auto it_eg = addr_to_idx.find(&rookOpenFileEg); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff; + } else if (!hasOwn) { + auto it_mg = addr_to_idx.find(&rookSemiOpenFileMg); + auto it_eg = addr_to_idx.find(&rookSemiOpenFileEg); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff; + } + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Bitboard rooks = board.pieces(ROOK, c); + while (rooks) { + Square sq = Square(pop_lsb(rooks)); + Rank r7 = c == WHITE ? RANK_7 : RANK_2; + if (rank_of(sq) != r7) + continue; + Bitboard ep = board.pieces(PAWN, ~c) & attacks::MASK_RANK[r7]; + int cnt = 0; + Bitboard tmp = ep; + while (tmp) { + Square psq = Square(pop_lsb(tmp)); + if (!board.is_attacked_by(~c, psq)) + cnt++; + } + if (cnt >= 2) { + double eff = s * stm_sign; + auto it = addr_to_idx.find(&rookOnSeventhBonus); + if (it != addr_to_idx.end()) { + gradient[it->second] += common_factor * eff * 0.5 * phase_mg; + gradient[it->second] += common_factor * eff * phase_eg; + } + } + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + double eff = s * stm_sign; + Bitboard pawns = pawnBB[c]; + Bitboard enemyPawns = pawnBB[~c]; + bool fileHasPawn[8] = { false }; + int fileCount[8] = { 0 }; + Bitboard tmp = pawns; + while (tmp) { + Square sq = Square(pop_lsb(tmp)); + fileCount[file_of(sq)]++; + fileHasPawn[file_of(sq)] = true; + } + + for (int f = 0; f < 8; f++) + if (fileCount[f] >= 2) { + double coeff = -(fileCount[f] - 1); + auto it_mg = addr_to_idx.find(&doubledPawnMg); + auto it_eg = addr_to_idx.find(&doubledPawnEg); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff * coeff; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff * coeff; + } + + tmp = pawns; + while (tmp) { + Square sq = Square(pop_lsb(tmp)); + File f = file_of(sq); + Rank relRank = relative_rank(c, sq); + + bool isolated = true; + if (f > FILE_A && fileHasPawn[f - 1]) + isolated = false; + if (f < FILE_H && fileHasPawn[f + 1]) + isolated = false; + if (isolated) { + auto it_mg = addr_to_idx.find(&isolatedPawnMg); + auto it_eg = addr_to_idx.find(&isolatedPawnEg); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff * (-1.0); + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff * (-1.0); + } + + if (relRank >= RANK_2 && (passed_pawn_mask(c, sq) & enemyPawns) == 0) { + auto it_mg = addr_to_idx.find(&passedBonusMg[relRank]); + auto it_eg = addr_to_idx.find(&passedBonusEg[relRank]); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff; + } + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Bitboard pawns = pawnBB[c]; + while (pawns) { + Square sq = Square(pop_lsb(pawns)); + Square forward = sq + pawn_push(c); + if (forward >= SQ_A1 && forward <= SQ_H8 && (pawnBB[~c] & (1ULL << forward))) { + auto it = addr_to_idx.find(&rammedPawnPenalty); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * (-s * stm_sign); + } + } + } + + { + Bitboard centerMask = (1ULL << SQ_D4) | (1ULL << SQ_E4) | (1ULL << SQ_D5) | (1ULL << SQ_E5); + int wc = popcount(pawnAtks[WHITE] & centerMask); + int bc = popcount(pawnAtks[BLACK] & centerMask); + auto it = addr_to_idx.find(¢erWeight); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * stm_sign * (wc - bc); + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Bitboard safe = ~pawnAtks[~c]; + Bitboard camp = c == WHITE ? (attacks::MASK_RANK[4] | attacks::MASK_RANK[5] | attacks::MASK_RANK[6]) + : (attacks::MASK_RANK[3] | attacks::MASK_RANK[2] | attacks::MASK_RANK[1]); + int space = popcount(pawnAtks[c] & safe & camp); + auto it = addr_to_idx.find(&spaceWeight); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * (s * stm_sign) * space; + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Square kingSq = board.kingSq(c); + File kf = file_of(kingSq); + Rank kr = rank_of(kingSq); + Bitboard pawns = board.pieces(PAWN, c); + int startF = std::max(0, (int)kf - 1); + int endF = std::min(7, (int)kf + 1); + double eff_base = s * stm_sign; + for (int adjF = startF; adjF <= endF; adjF++) { + Bitboard fileMask = attacks::MASK_FILE[adjF]; + auto check_rank = [&](int r) { + if (!(pawns & (fileMask & attacks::MASK_RANK[r]))) + return; + double dist = (c == WHITE) ? (r - (int)kr - 1) : ((int)kr - r - 1); + auto it_bmg = addr_to_idx.find(&kingShelterBaseMg); + auto it_beg = addr_to_idx.find(&kingShelterBaseEg); + auto it_dmg = addr_to_idx.find(&kingShelterDecayMg); + auto it_deg = addr_to_idx.find(&kingShelterDecayEg); + if (it_bmg != addr_to_idx.end()) + gradient[it_bmg->second] += common_factor * eff_base; + if (it_beg != addr_to_idx.end()) + gradient[it_beg->second] += common_factor * eff_base; + if (it_dmg != addr_to_idx.end()) + gradient[it_dmg->second] += common_factor * eff_base * (-dist); + if (it_deg != addr_to_idx.end()) + gradient[it_deg->second] += common_factor * eff_base * (-dist); + }; + if (c == WHITE) { + for (int r = (int)kr + 1; r <= std::min(7, (int)kr + 3); r++) + check_rank(r); + } else { + for (int r = (int)kr - 1; r >= std::max(0, (int)kr - 3); r--) + check_rank(r); + } + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + double eff = s * stm_sign; + int oppCount = popcount(board.occ(~c)); + Square oppKing = board.kingSq(~c); + File ef = file_of(oppKing); + Rank er = rank_of(oppKing); + int edgeDist = std::min(std::min((int)ef, 7 - (int)ef), std::min((int)er, 7 - (int)er)); + + if (board.count(QUEEN, c) >= 1 && oppCount == 1) { + Square myKing = board.kingSq(c); + int kd = std::max(std::abs((int)file_of(myKing) - (int)file_of(oppKing)), + std::abs((int)rank_of(myKing) - (int)rank_of(oppKing))); + auto it1 = addr_to_idx.find(&kqkDistWeight); + auto it2 = addr_to_idx.find(&kqkEdgeWeight); + if (it1 != addr_to_idx.end()) + gradient[it1->second] += common_factor * eff * (14 - kd); + if (it2 != addr_to_idx.end()) + gradient[it2->second] += common_factor * eff * (7 - edgeDist); + } + + if (board.count(ROOK, c) >= 1 && oppCount == 1 && board.count(QUEEN, c) == 0 && board.count(BISHOP, c) == 0 && + board.count(KNIGHT, c) == 0 && board.count(PAWN, c) == 0) { + Square myKing = board.kingSq(c); + int kd = std::max(std::abs((int)file_of(myKing) - (int)file_of(oppKing)), + std::abs((int)rank_of(myKing) - (int)rank_of(oppKing))); + auto it1 = addr_to_idx.find(&krkDistWeight); + auto it2 = addr_to_idx.find(&krkEdgeWeight); + if (it1 != addr_to_idx.end()) + gradient[it1->second] += common_factor * eff * (14 - kd); + if (it2 != addr_to_idx.end()) + gradient[it2->second] += common_factor * eff * (7 - edgeDist); + } + + if (board.count(PAWN, c) >= 1 && oppCount == 1 && board.count(QUEEN, c) == 0 && board.count(ROOK, c) == 0 && + board.count(BISHOP, c) == 0 && board.count(KNIGHT, c) == 0) { + Bitboard ps = pawnBB[c]; + while (ps) { + Square psq = Square(pop_lsb(ps)); + Rank pr = relative_rank(c, psq); + if (pr < RANK_4) + continue; + File pf = file_of(psq); + Square promSq = make_sq(pf, c == WHITE ? RANK_8 : RANK_1); + Square myKing = board.kingSq(c); + int pawnDist = (c == WHITE) ? (7 - (int)rank_of(psq)) : (int)rank_of(psq); + int pkDist = + std::max(std::abs((int)file_of(oppKing) - (int)pf), std::abs((int)rank_of(oppKing) - (int)rank_of(promSq))); + int mkDist = std::max(std::abs((int)file_of(myKing) - (int)file_of(psq)), + std::abs((int)rank_of(myKing) - (int)rank_of(psq))); + bool winning = pkDist > pawnDist || mkDist <= pawnDist + 1 || + (pr >= RANK_6 && file_of(myKing) == pf && + ((c == WHITE && rank_of(myKing) >= RANK_6) || (c == BLACK && rank_of(myKing) <= RANK_3))); + if ((pf == FILE_A || pf == FILE_H) && oppKing == promSq) + winning = false; + if (winning) { + auto it = addr_to_idx.find(&kpkWeight); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * eff * pawnDist; + } + } + } + + Bitboard ourMat = board.occ(c) & ~board.pieces(PAWN) & ~board.pieces(KING); + Bitboard theirMat = board.occ(~c) & ~board.pieces(PAWN) & ~board.pieces(KING); + if (ourMat && !theirMat && oppCount <= 2) { + Square myKing = board.kingSq(c); + int kd = std::max(std::abs((int)file_of(myKing) - (int)file_of(oppKing)), + std::abs((int)rank_of(myKing) - (int)rank_of(oppKing))); + auto it1 = addr_to_idx.find(&mopUpKingDistWeight); + auto it2 = addr_to_idx.find(&mopUpEdgeDistWeight); + if (it1 != addr_to_idx.end()) + gradient[it1->second] += common_factor * eff * (14 - kd); + if (it2 != addr_to_idx.end()) + gradient[it2->second] += common_factor * eff * (7 - edgeDist); + } + } + + for (Color c : { WHITE, BLACK }) { + double s = (c == WHITE) ? 1.0 : -1.0; + Color opp = ~c; + Bitboard ourPieces = board.occ(c) & ~board.pieces(PAWN) & ~board.pieces(KING); + while (ourPieces) { + Square sq = Square(pop_lsb(ourPieces)); + PieceType pt = piece_of(board.at(sq)); + bool attacked = board.is_attacked_by(opp, sq); + if (!attacked) + continue; + bool defended = board.is_attacked_by(c, sq); + Bitboard occ = board.occ(); + int attackers = popcount(board.attackers_mask(opp, sq, occ)); + int defenders = defended ? popcount(board.attackers_mask(c, sq, occ)) : 0; + bool hanging = !defended; + bool overloaded = defenders == 1 && attackers >= 2; + bool attackedByMinor = + (board.attackers_mask(opp, sq, occ) & (board.pieces(KNIGHT, opp) | board.pieces(BISHOP, opp))) != 0; + bool attackedByRook = (board.attackers_mask(opp, sq, occ) & board.pieces(ROOK, opp)) != 0; + + double eff_threat = -s * stm_sign; + + if (hanging) { + double mg_hf = (pt == QUEEN) ? 0.5 : 1.0; + double eg_hf = (pt == QUEEN) ? 0.25 : 0.5; + auto it = addr_to_idx.find(&hangingScore); + if (it != addr_to_idx.end()) { + gradient[it->second] += common_factor * eff_threat * mg_hf * phase_mg; + gradient[it->second] += common_factor * eff_threat * eg_hf * phase_eg; + } + } + + if (overloaded) { + auto it = addr_to_idx.find(&overloadScore); + if (it != addr_to_idx.end()) { + gradient[it->second] += common_factor * eff_threat * phase_mg; + gradient[it->second] += common_factor * eff_threat * 0.5 * phase_eg; + } + } + + if (attackedByMinor) { + auto it_mg = addr_to_idx.find(&threatByMinor[pt][0]); + auto it_eg = addr_to_idx.find(&threatByMinor[pt][1]); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff_threat; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff_threat; + } + + if (attackedByRook) { + auto it_mg = addr_to_idx.find(&threatByRook[pt][0]); + auto it_eg = addr_to_idx.find(&threatByRook[pt][1]); + if (it_mg != addr_to_idx.end()) + gradient[it_mg->second] += common_factor * eff_threat; + if (it_eg != addr_to_idx.end()) + gradient[it_eg->second] += common_factor * eff_threat; + } + + Rank relRank = relative_rank(c, sq); + auto it = addr_to_idx.find(&threatByRankScore); + if (it != addr_to_idx.end()) { + gradient[it->second] += common_factor * eff_threat * relRank * phase_mg; + gradient[it->second] += common_factor * eff_threat * relRank * phase_eg; + } + } + } + + { + int pc[2][7] = { { 0 } }; + for (Color c : { WHITE, BLACK }) + for (PieceType pt : { PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING }) + pc[c][pt] = board.count(pt, c); + int ourM = pc[WHITE][KNIGHT] + pc[WHITE][BISHOP]; + int theirM = pc[BLACK][KNIGHT] + pc[BLACK][BISHOP]; + auto add_imb = [&](auto *addr, int coeff) { + auto it = addr_to_idx.find(addr); + if (it != addr_to_idx.end()) + gradient[it->second] += common_factor * stm_sign * coeff; + }; + add_imb(&minorImWt, ourM - theirM); + add_imb(&bishopImWt, pc[WHITE][BISHOP] - pc[BLACK][BISHOP]); + add_imb(&rookImWt, pc[WHITE][ROOK] - pc[BLACK][ROOK]); + add_imb(&queenImWt, pc[WHITE][QUEEN] - pc[BLACK][QUEEN]); + } +} + +void texel_tune(TuneData &all, + const std::vector &train_idx, + const std::vector &holdout_idx, + std::vector ¶ms, + int iterations, + const std::string &weights_header) { + auto addr_map = build_addr_map(params); + int dim = (int)params.size(); + int num_threads = (int)std::thread::hardware_concurrency(); + if (num_threads < 1) + num_threads = 1; + size_t n = train_idx.size(); + + double base_loss = eval_loss(all, train_idx); + std::cout << "info string texel_tune: dim=" << dim << " positions=" << n << " initial_loss=" << base_loss * train_idx.size() + << std::endl; + + double lr = 1.0; + + std::vector best_x(dim); + for (int i = 0; i < dim; i++) + best_x[i] = *params[i].value_ptr; + double best_loss = base_loss; + + for (int iter = 0; iter < iterations; iter++) { + auto t0 = std::chrono::steady_clock::now(); + + std::vector gradient(dim, 0.0); + std::vector threads; + std::vector partial_loss(num_threads, 0.0); + std::vector> partial_grad(num_threads, std::vector(dim, 0.0)); + size_t chunk = (n + num_threads - 1) / num_threads; + + auto worker = [&](int tid, size_t start, size_t end) { + chess::Position pos; + std::string fen; + double ploss = 0.0; + auto &pg = partial_grad[tid]; + + for (size_t j = start; j < end; j++) { + size_t idx = train_idx[j]; + fen.assign(all.fen_buf.data() + all.offsets[idx]); + pos.set_fen(fen); + + auto comp = eval::eval_components(pos); + const int sign = pos.side_to_move() == chess::WHITE ? 1 : -1; + Value score = (((comp.mg * comp.phase) + (comp.eg * (256 - comp.phase))) * sign) / 256 + eval::tempo; + + double sig = sigmoid(score); + double error = sig - all.result(idx); + ploss += error * error; + + // Avoid division by zero in gradient + if (std::abs(error) < 1e-12) + continue; + + double sig_deriv = 0.004 * sig * (1.0 - sig); + double common_factor = 2.0 * error * sig_deriv; + double phase_mg = comp.phase / 256.0; + double phase_eg = (256 - comp.phase) / 256.0; + double stm_sign_val = (pos.side_to_move() == chess::WHITE) ? 1.0 : -1.0; + + accumulate_gradient(pos, common_factor, addr_map, phase_mg, phase_eg, stm_sign_val, pg); + } + partial_loss[tid] = ploss; + }; + + for (int t = 0; t < num_threads; t++) { + size_t start = t * chunk; + size_t end = std::min(start + chunk, n); + if (start >= n) + break; + threads.emplace_back(worker, t, start, end); + } + for (auto &t : threads) + t.join(); + + double loss_sum = std::accumulate(partial_loss.begin(), partial_loss.end(), 0.0); + double train_loss = loss_sum / n; + + // Accumulate gradients from all threads + for (int i = 0; i < dim; i++) { + gradient[i] = 0.0; + for (int t = 0; t < num_threads; t++) { + gradient[i] += partial_grad[t][i]; + } + } + + std::vector old_x(dim); + for (int i = 0; i < dim; i++) + old_x[i] = *params[i].value_ptr; + + // Gradient clipping to prevent exploding gradients + double max_abs_grad = 0.0; + for (int i = 0; i < dim; i++) { + // Clip gradient magnitude + if (std::abs(gradient[i]) > 1000.0) { + gradient[i] = (gradient[i] > 0) ? 1000.0 : -1000.0; + } + max_abs_grad = std::max(max_abs_grad, std::abs(gradient[i])); + } + if (max_abs_grad < 1e-12) + max_abs_grad = 1.0; + + double step_size = lr; + double new_loss = train_loss; + int halvings = 0; + + for (int attempt = 0; attempt < 12; attempt++) { + double scale = step_size / max_abs_grad; + bool any_change = false; + + // Store current parameter values before making changes + std::vector current_values(dim); + for (int i = 0; i < dim; i++) { + current_values[i] = *params[i].value_ptr; + } + + // Apply gradient step + for (int i = 0; i < dim; i++) { + double nv = old_x[i] - scale * gradient[i]; + nv = std::clamp(nv, (double)params[i].lower, (double)params[i].upper); + int vi = int(std::round(nv)); + if (vi != *params[i].value_ptr) + any_change = true; + *params[i].value_ptr = vi; + } + + if (!any_change) { + // No parameter changed, restore and break + for (int i = 0; i < dim; i++) { + *params[i].value_ptr = current_values[i]; + } + break; + } + + // Evaluate new loss + new_loss = eval_loss(all, train_idx); + + if (new_loss < train_loss) { + // Improvement found, keep changes and update learning rate + lr = step_size; + break; + } else { + // No improvement, restore parameters and try smaller step + for (int i = 0; i < dim; i++) { + *params[i].value_ptr = current_values[i]; + } + step_size *= 0.5; + halvings++; + } + } + + double hold_loss = eval_loss(all, holdout_idx); + + if (hold_loss < best_loss) { + best_loss = hold_loss; + for (int i = 0; i < dim; i++) + best_x[i] = *params[i].value_ptr; + } + + auto t1 = std::chrono::steady_clock::now(); + double elapsed = std::chrono::duration_cast(t1 - t0).count() / 1000.0; + + std::cout << "info string iter=" << iter << " train=" << train_loss << " hold=" << hold_loss << " lr=" << lr + << " halvings=" << halvings << " max_grad=" << max_abs_grad << " time=" << elapsed << "s" << std::endl; + + if (new_loss >= best_loss && halvings > 0 && step_size < 1.0 / 128.0) { + std::cout << "info string no further improvement, stopping" << std::endl; + break; + } + } + + for (int i = 0; i < dim; i++) + *params[i].value_ptr = int(std::round(best_x[i])); + + // Export the best weights found + std::fstream file(weights_header, std::ios::out); + if (file.is_open()) + Tune::export_weights(file); + else + std::cerr << "info string failed to open " << weights_header << " for writing\n"; +} + +} // namespace + +void tune_command(const std::string &csv_path, int iterations, int max_pos, const std::string &weights_header) { + if (max_pos <= 0) { + std::cerr << "info string max_pos must be positive\n"; + return; + } + if (iterations <= 0) { + std::cerr << "info string iterations must be positive\n"; + return; + } + auto start_time = std::chrono::steady_clock::now(); + + csv::CSVReader reader(csv_path); + TuneData all; + std::random_device rd; + std::mt19937_64 load_rng(rd()); + size_t seen = 0; + for (auto &row : reader) { + auto fen = row["fen"].get<>(); + auto result = row["result"].get(); + seen++; + if (all.size() < (size_t)max_pos) { + all.add(fen, result); + } else { + std::uniform_int_distribution dist(0, seen - 1); + size_t j = dist(load_rng); + if (j < all.size()) { + all.offsets[j] = all.fen_buf.size(); + all.fen_buf.insert(all.fen_buf.end(), fen.begin(), fen.end()); + all.fen_buf.push_back('\0'); + all.results[j] = result; + } + } + } + if (all.size() == 0) { + std::cerr << "info string no positions loaded from CSV\n"; + return; + } + std::cout << "info string loaded " << all.size() << " positions via reservoir sampling (seen " << seen << ")" << std::endl; + + size_t n = all.size(); + size_t holdout_size = std::min(size_t(2000), n / 5); + + std::vector idx(n); + for (size_t i = 0; i < n; i++) + idx[i] = i; + std::mt19937 rng(rd()); + std::shuffle(idx.begin(), idx.end(), rng); + + std::vector holdout_idx(idx.begin(), idx.begin() + holdout_size); + std::vector train_idx(idx.begin() + holdout_size, idx.end()); + + auto params = collect_params(); + int dim = (int)params.size(); + + std::cout << "info string loaded " << n << " positions, dim=" << dim << " train=" << train_idx.size() + << " holdout=" << holdout_idx.size() << std::endl; + + double base_train = eval_loss(all, train_idx); + double base_hold = eval_loss(all, holdout_idx); + std::cout << "info string baseline train=" << base_train << " holdout=" << base_hold << std::endl; + + texel_tune(all, train_idx, holdout_idx, params, iterations, weights_header); + + double final_hold = eval_loss(all, holdout_idx); + auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start_time).count(); + + std::cout << "info string tune done in " << elapsed << "s, best holdout=" << final_hold * holdout_size + << " (baseline=" << base_hold * holdout_size << ")" << std::endl; + std::fstream file(weights_header, std::ios::out); + if (file.is_open()) + Tune::export_weights(file); + else + std::cerr << "info string failed to open " << weights_header << " for writing\n"; +} + +} // namespace engine diff --git a/tune_cmd.h b/tune_cmd.h new file mode 100644 index 0000000..bade97c --- /dev/null +++ b/tune_cmd.h @@ -0,0 +1,5 @@ +#pragma once +#include +namespace engine { +void tune_command(const std::string &csv_path, int iterations, int max_pos, const std::string &weights_header); +} // namespace engine diff --git a/uci.cpp b/uci.cpp index 20aab9a..230d882 100644 --- a/uci.cpp +++ b/uci.cpp @@ -1,200 +1,297 @@ #include "uci.h" +#include "eval.h" #include "search.h" #include "timeman.h" +#include "tune.h" +#ifdef USE_CSV_PARSER +#include "tune_cmd.h" +#endif #include "ucioption.h" #include +#include +#include +#include #include #include #include #include #include using namespace engine; +bool quit{ false }; chess::Position pos; OptionsMap engine::options; std::thread searchThread; + +namespace { +std::string strip_optional_quotes(std::string fen) { + auto notSpace = [](unsigned char c) { return !std::isspace(c); }; + + fen.erase(fen.begin(), std::find_if(fen.begin(), fen.end(), notSpace)); + fen.erase(std::find_if(fen.rbegin(), fen.rend(), notSpace).base(), fen.end()); + + if (fen.size() >= 2 && fen.front() == '"' && fen.back() == '"') + return fen.substr(1, fen.size() - 2); + + if (!fen.empty() && fen.front() == '"') + fen.erase(fen.begin()); + + if (!fen.empty() && fen.back() == '"') + fen.pop_back(); + + return fen; +} +} // namespace + void engine::stop() { - search::stop(); - if (searchThread.joinable()) { search::stop(); - searchThread.join(); - } + if (searchThread.joinable()) { + searchThread.join(); + } } void handlePosition(std::istringstream &is) { - stop(); - std::string token, fen; - - is >> token; - - if (token == "startpos") { - fen = chess::Position::START_FEN; - is >> token; // Consume the "moves" token, if any - } else if (token == "fen") - while (is >> token && token != "moves") - fen += token + " "; - else - return; - pos.setFEN(fen); - - while (is >> token) { - pos.push_uci(token); - } + stop(); + std::string token, fen; + + is >> token; + + if (token == "startpos") { + fen = chess::Position::START_FEN; + is >> token; // Consume the "moves" token, if any + } else if (token == "fen") + while (is >> token && token != "moves") + fen += token + " "; + else + return; + + try { + pos.setFEN(strip_optional_quotes(fen)); + } catch (const std::exception &e) { + std::cout << "info string Invalid FEN: " << e.what() << std::endl; + return; + } + + while (is >> token) { + try { + pos.push_uci(token); + } catch (const std::exception &e) { + std::cout << "info string Invalid move " << token << ": " << e.what() << std::endl; + return; + } + } } timeman::LimitsType parse_limits(std::istream &is) { - timeman::LimitsType limits; - std::string token; - - limits.startTime = timeman::now(); // The search starts as early as possible - - while (is >> token) - if (token == "searchmoves") // Needs to be the last command on the line - while (is >> token) { - std::transform(token.begin(), token.end(), token.begin(), - [](auto c) { return std::tolower(c); }); - limits.searchmoves.push_back(token); - } - else if (token == "wtime") - is >> limits.time[chess::WHITE]; - else if (token == "btime") - is >> limits.time[chess::BLACK]; - else if (token == "winc") - is >> limits.inc[chess::WHITE]; - else if (token == "binc") - is >> limits.inc[chess::BLACK]; - else if (token == "movestogo") - is >> limits.movestogo; - else if (token == "depth") - is >> limits.depth; - else if (token == "nodes") - is >> limits.nodes; - else if (token == "movetime") - is >> limits.movetime; - else if (token == "mate") - is >> limits.mate; - else if (token == "perft") - is >> limits.perft; - else if (token == "infinite") - limits.infinite = 1; - else if (token == "ponder") - ; - - return limits; + timeman::LimitsType limits; + std::string token; + + limits.startTime = timeman::now(); // The search starts as early as possible + + while (is >> token) + if (token == "searchmoves") // Needs to be the last command on the line + while (is >> token) { + std::transform(token.begin(), token.end(), token.begin(), [](auto c) { return std::tolower(c); }); + limits.searchmoves.push_back(token); + } + else if (token == "wtime") + is >> limits.time[chess::WHITE]; + else if (token == "btime") + is >> limits.time[chess::BLACK]; + else if (token == "winc") + is >> limits.inc[chess::WHITE]; + else if (token == "binc") + is >> limits.inc[chess::BLACK]; + else if (token == "movestogo") + is >> limits.movestogo; + else if (token == "depth") + is >> limits.depth; + else if (token == "nodes") + is >> limits.nodes; + else if (token == "movetime") + is >> limits.movetime; + else if (token == "mate") + is >> limits.mate; + else if (token == "perft") + is >> limits.perft; + else if (token == "infinite") + limits.infinite = 1; + else if (token == "ponder") { + limits.ponderMode = true; + } + + return limits; } void handleGo(std::istringstream &ss) { - stop(); - chess::Position copy = pos; + stop(); + if (searchThread.joinable()) + searchThread.join(); + chess::Position copy = pos; - searchThread = std::thread([copy, ss = std::move(ss)]() mutable { - search::search(copy, parse_limits(ss)); - }); + searchThread = std::thread([copy, ss = std::move(ss)]() mutable { search::search(copy, parse_limits(ss)); }); } template struct overload : Ts... { - using Ts::operator()...; + using Ts::operator()...; }; template overload(Ts...) -> overload; std::string engine::format_score(const Score &s) { - const auto format = overload{ - [](Score::Mate mate) -> std::string { - auto m = (mate.plies > 0 ? (mate.plies + 1) : mate.plies) / 2; - return std::string("mate ") + std::to_string(m); - }, - [](Score::Tablebase tb) -> std::string { - constexpr int TB_CP = 20000; - return std::string("cp ") + - std::to_string((tb.win ? TB_CP - tb.plies : -TB_CP - tb.plies)); - }, - [](Score::InternalUnits units) -> std::string { - return std::string("cp ") + std::to_string(units.value); - }}; - - return s.visit(format); + const auto format = + overload{ [](Score::Mate mate) -> std::string { + auto m = (mate.plies > 0 ? (mate.plies + 1) : mate.plies) / 2; + return std::string("mate ") + std::to_string(m); + }, + [](Score::Tablebase tb) -> std::string { + constexpr int TB_CP = 20000; + return std::string("cp ") + std::to_string((tb.win ? TB_CP - tb.plies : -TB_CP - tb.plies)); + }, + [](Score::InternalUnits units) -> std::string { return std::string("cp ") + std::to_string(units.value); } }; + + return s.visit(format); } void engine::report(const InfoShort &info) { - std::cout << "info depth " << info.depth << " score " - << format_score(info.score) << std::endl; + std::cout << "info depth " << info.depth << " score " << format_score(info.score) << std::endl; } void engine::report(const InfoFull &info, bool showWDL) { - std::stringstream ss; - - ss << "info"; - ss << " depth " << info.depth // - << " seldepth " << info.selDepth // - << " multipv " << info.multiPV // - << " score " << format_score(info.score); // + std::stringstream ss; - if (!info.bound.empty()) - ss << " " << info.bound; + ss << "info"; + ss << " depth " << info.depth // + << " seldepth " << info.selDepth // + << " multipv " << info.multiPV // + << " score " << format_score(info.score); // - if (showWDL) - ss << " wdl " << info.wdl; + if (!info.bound.empty()) + ss << " " << info.bound; - ss << " nodes " << info.nodes // - << " nps " << info.nps // - << " hashfull " << info.hashfull // - << " tbhits " << info.tbHits // - << " time " << info.timeMs // - << " pv " << info.pv; // + if (showWDL) + ss << " wdl " << info.wdl; - std::cout << ss.str() << std::endl; + ss << " nodes " << info.nodes // + << " nps " << info.nps // + << " hashfull " << info.hashfull // + << " tbhits " << info.tbHits // + << " time " << info.timeMs // + << " pv " << info.pv; // + if (!info.extrainfo.empty()) + ss << "\ninfo string extras " << info.extrainfo; + std::cout << ss.str() << std::endl; } void engine::report(const InfoIteration &info) { - std::stringstream ss; + std::stringstream ss; - ss << "info"; - ss << " depth " << info.depth // - << " currmove " << info.currmove // - << " currmovenumber " << info.currmovenumber; // + ss << "info"; + ss << " depth " << info.depth // + << " currmove " << info.currmove // + << " currmovenumber " << info.currmovenumber; // - std::cout << ss.str() << std::endl; + std::cout << ss.str() << std::endl; } void engine::report(std::string_view bestmove) { - std::cout << "bestmove " << bestmove; - std::cout << std::endl; + std::cout << "bestmove " << bestmove; + std::cout << std::endl; } -void engine::loop() { - std::string line; - pos.setFEN(chess::Position::START_FEN); - - while (std::getline(std::cin, line)) { +void execCmd(const std::string &line) { std::istringstream ss(line); std::string token; - stop(); + while (ss >> token) { - if (token == "uci") { - std::cout << "id name cppchess_engine\n"; - std::cout << "id author winapiadmin\n"; - std::cout << options << '\n'; - std::cout << "uciok\n"; - break; - } else if (token == "isready") { - std::cout << "readyok\n"; - break; - } else if (token == "position") { - handlePosition(ss); - break; - } else if (token == "go") { - handleGo(ss); - break; // rest belongs to go - } else if (token == "ucinewgame") { - search::tt.clear(); - break; - } else if (token == "stop") { - break; - } else if (token == "quit") { - return; - } else if (token == "setoption") { - options.setoption(ss); - break; - } else if (token == "visualize" || token == "d") { - std::cout << pos << std::endl; - break; - } + if (token == "uci") { + std::cout << "id name cppchess_engine\n"; + std::cout << "id author winapiadmin\n"; + std::cout << options << '\n'; + std::cout << "uciok\n"; + break; + } else if (token == "isready") { + std::cout << "readyok\n"; + break; + } else if (token == "position") { + handlePosition(ss); + break; + } else if (token == "go") { + handleGo(ss); + break; // rest belongs to go + } else if (token == "ucinewgame") { + search::tt.clear(); + break; + } else if (token == "stop") { + break; + } else if (token == "quit") { + quit = true; + return; + } else if (token == "setoption") { + options.setoption(ss); + break; + } else if (token == "visualize" || token == "d") { + std::cout << pos << std::endl; + break; + } else if (token == "eval") { + handlePosition(ss); // auto-handle it if any + int score = eval::eval(pos); + if (pos.side_to_move() != chess::WHITE) + score = -score; + std::cout << score << std::endl; + break; + } else if (token == "export_weights") { + std::string weights_header = "Weights.h"; + ss >> weights_header; + std::fstream file(weights_header, std::ios::out); + if (file.is_open()) { + Tune::export_weights(file); + std::cout << "info string dumped weights to " << weights_header << '\n'; + } else { + std::cout << "info string failed to open " << weights_header << " for writing\n"; + } + break; + } else if (token == "tune") { +#ifdef USE_CSV_PARSER + std::string csv_path, out_file = "Weights.h"; + int iters = 50, max_pos = 20000; + ss >> csv_path >> iters >> max_pos; + tune_command(csv_path, iters, max_pos, out_file); +#else + std::cout << "info string engine built without tuning support" << std::endl; +#endif + break; + } else if (token == "evalbatch") { + char c; + bool first = true; + while (ss >> c) { + if (c != '"') + continue; + std::string fen; + while (ss.get(c) && c != '"') + fen += c; + if (!first) + std::cout << ' '; + first = false; + try { + chess::Position bp; + bp.setFEN(fen); + int sc = eval::eval(bp); + if (bp.side_to_move() != chess::WHITE) + sc = -sc; + std::cout << sc; + } catch (...) { + std::cout << "0"; + } + } + std::cout << std::endl; + break; + } + } +} +void engine::loop() { + std::string line; + pos.setFEN(chess::Position::START_FEN); + + while (!quit && std::getline(std::cin, line)) { + stop(); + execCmd(line); } - } - stop(); + stop(); + if (searchThread.joinable()) + searchThread.join(); } diff --git a/uci.h b/uci.h index 800535d..18d5e17 100644 --- a/uci.h +++ b/uci.h @@ -4,27 +4,28 @@ #include namespace engine { struct InfoShort { - int depth; - Score score; + int depth; + Score score; }; struct InfoFull : InfoShort { - int selDepth; - size_t multiPV; - std::string_view wdl; - std::string_view bound; - size_t timeMs; - size_t nodes; - size_t nps; - size_t tbHits; - std::string_view pv; - int hashfull; + int selDepth; + size_t multiPV; + std::string_view wdl; + std::string_view bound; + size_t timeMs; + size_t nodes; + size_t nps; + size_t tbHits; + std::string pv; + std::string extrainfo; + int hashfull; }; struct InfoIteration { - int depth; - std::string_view currmove; - size_t currmovenumber; + int depth; + std::string currmove; + size_t currmovenumber; }; std::string format_score(const Score &s); void report(const InfoFull &info, bool showWDL = false); diff --git a/ucioption.cpp b/ucioption.cpp index 68fc9e6..3d62ca3 100644 --- a/ucioption.cpp +++ b/ucioption.cpp @@ -28,102 +28,91 @@ namespace engine { -bool CaseInsensitiveLess::operator()(const std::string &s1, - const std::string &s2) const { +bool CaseInsensitiveLess::operator()(const std::string &s1, const std::string &s2) const { - return std::lexicographical_compare( - s1.begin(), s1.end(), s2.begin(), s2.end(), - [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }); + return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char c1, char c2) { + return std::tolower(c1) < std::tolower(c2); + }); } -void OptionsMap::add_info_listener(InfoListener &&message_func) { - info = std::move(message_func); -} +void OptionsMap::add_info_listener(InfoListener &&message_func) { info = std::move(message_func); } void OptionsMap::setoption(std::istringstream &is) { - std::string token, name, value; + std::string token, name, value; - is >> token; // Consume the "name" token + is >> token; // Consume the "name" token - // Read the option name (can contain spaces) - while (is >> token && token != "value") - name += (name.empty() ? "" : " ") + token; + // Read the option name (can contain spaces) + while (is >> token && token != "value") + name += (name.empty() ? "" : " ") + token; - // Read the option value (can contain spaces) - while (is >> token) - value += (value.empty() ? "" : " ") + token; + // Read the option value (can contain spaces) + while (is >> token) + value += (value.empty() ? "" : " ") + token; - if (options_map.count(name)) - options_map[name] = value; - else - std::cerr << "No such option: " << name << std::endl; + if (options_map.count(name)) + options_map[name] = value; + else + std::cerr << "No such option: " << name << std::endl; } const Option &OptionsMap::operator[](const std::string &name) const { - auto it = options_map.find(name); - assert(it != options_map.end()); - return it->second; + auto it = options_map.find(name); + assert(it != options_map.end()); + return it->second; } // Inits options and assigns idx in the correct printing order void OptionsMap::add(const std::string &name, const Option &option) { - if (!options_map.count(name)) { - static size_t insert_order = 0; + if (!options_map.count(name)) { + static size_t insert_order = 0; - options_map[name] = option; + options_map[name] = option; - options_map[name].parent = this; - options_map[name].idx = insert_order++; - } else { - std::cerr << "Option \"" << name << "\" was already added!" << std::endl; - std::exit(EXIT_FAILURE); - } + options_map[name].parent = this; + options_map[name].idx = insert_order++; + } else { + std::cerr << "Option \"" << name << "\" was already added!" << std::endl; + std::exit(EXIT_FAILURE); + } } -std::size_t OptionsMap::count(const std::string &name) const { - return options_map.count(name); -} +std::size_t OptionsMap::count(const std::string &name) const { return options_map.count(name); } Option::Option(const OptionsMap *map) : parent(map) {} -Option::Option(const char *v, OnChange f) - : type("string"), min(0), max(0), on_change(std::move(f)) { - defaultValue = currentValue = v; +Option::Option(const char *v, OnChange f) : type("string"), min(0), max(0), on_change(std::move(f)) { + defaultValue = currentValue = v; } -Option::Option(bool v, OnChange f) - : type("check"), min(0), max(0), on_change(std::move(f)) { - defaultValue = currentValue = (v ? "true" : "false"); +Option::Option(bool v, OnChange f) : type("check"), min(0), max(0), on_change(std::move(f)) { + defaultValue = currentValue = (v ? "true" : "false"); } -Option::Option(OnChange f) - : type("button"), min(0), max(0), on_change(std::move(f)) {} +Option::Option(OnChange f) : type("button"), min(0), max(0), on_change(std::move(f)) {} -Option::Option(int v, int minv, int maxv, OnChange f) - : type("spin"), min(minv), max(maxv), on_change(std::move(f)) { - defaultValue = currentValue = std::to_string(v); +Option::Option(int v, int minv, int maxv, OnChange f) : type("spin"), min(minv), max(maxv), on_change(std::move(f)) { + defaultValue = currentValue = std::to_string(v); } -Option::Option(const char *v, const char *cur, OnChange f) - : type("combo"), min(0), max(0), on_change(std::move(f)) { - defaultValue = v; - currentValue = cur; +Option::Option(const char *v, const char *cur, OnChange f) : type("combo"), min(0), max(0), on_change(std::move(f)) { + defaultValue = v; + currentValue = cur; } Option::operator int() const { - assert(type == "check" || type == "spin"); - return (type == "spin" ? std::stoi(currentValue) : currentValue == "true"); + assert(type == "check" || type == "spin"); + return (type == "spin" ? std::stoi(currentValue) : currentValue == "true"); } Option::operator std::string() const { - assert(type == "string"); - return currentValue; + assert(type == "string"); + return currentValue; } bool Option::operator==(const char *s) const { - assert(type == "combo"); - return !CaseInsensitiveLess()(currentValue, s) && - !CaseInsensitiveLess()(s, currentValue); + assert(type == "combo"); + return !CaseInsensitiveLess()(currentValue, s) && !CaseInsensitiveLess()(s, currentValue); } bool Option::operator!=(const char *s) const { return !(*this == s); } @@ -133,61 +122,58 @@ bool Option::operator!=(const char *s) const { return !(*this == s); } // from the user by console window, so let's check the bounds anyway. Option &Option::operator=(const std::string &v) { - assert(!type.empty()); + assert(!type.empty()); - if ((type != "button" && type != "string" && v.empty()) || - (type == "check" && v != "true" && v != "false") || - (type == "spin" && (std::stoi(v) < min || std::stoi(v) > max))) - return *this; + if ((type != "button" && type != "string" && v.empty()) || (type == "check" && v != "true" && v != "false") || + (type == "spin" && (std::stoi(v) < min || std::stoi(v) > max))) + return *this; + + if (type == "combo") { + OptionsMap comboMap; // To have case insensitive compare + std::string token; + std::istringstream ss(defaultValue); + while (ss >> token) + comboMap.add(token, Option()); + if (!comboMap.count(v) || v == "var") + return *this; + } - if (type == "combo") { - OptionsMap comboMap; // To have case insensitive compare - std::string token; - std::istringstream ss(defaultValue); - while (ss >> token) - comboMap.add(token, Option()); - if (!comboMap.count(v) || v == "var") - return *this; - } - - if (type == "string") - currentValue = v == "" ? "" : v; - else if (type != "button") - currentValue = v; - - if (on_change) { - const auto ret = on_change(*this); - - if (ret && parent != nullptr && parent->info != nullptr) - parent->info(ret); - } - - return *this; + if (type == "string") + currentValue = v == "" ? "" : v; + else if (type != "button") + currentValue = v; + + if (on_change) { + const auto ret = on_change(*this); + + if (ret && parent != nullptr && parent->info != nullptr) + parent->info(ret); + } + + return *this; } std::ostream &operator<<(std::ostream &os, const OptionsMap &om) { - for (size_t idx = 0; idx < om.options_map.size(); ++idx) - for (const auto &it : om.options_map) - if (it.second.idx == idx) { - const Option &o = it.second; - os << "\noption name " << it.first << " type " << o.type; + for (size_t idx = 0; idx < om.options_map.size(); ++idx) + for (const auto &it : om.options_map) + if (it.second.idx == idx) { + const Option &o = it.second; + os << "\noption name " << it.first << " type " << o.type; - if (o.type == "check" || o.type == "combo") - os << " default " << o.defaultValue; + if (o.type == "check" || o.type == "combo") + os << " default " << o.defaultValue; - else if (o.type == "string") { - std::string defaultValue = - o.defaultValue.empty() ? "" : o.defaultValue; - os << " default " << defaultValue; - } + else if (o.type == "string") { + std::string defaultValue = o.defaultValue.empty() ? "" : o.defaultValue; + os << " default " << defaultValue; + } - else if (o.type == "spin") - os << " default " << stoi(o.defaultValue) << " min " << o.min - << " max " << o.max; + else if (o.type == "spin") + os << " default " << stoi(o.defaultValue) << " min " << o.min << " max " << o.max; - break; - } + break; + } - return os; + return os; } } // namespace engine diff --git a/ucioption.h b/ucioption.h index 012bc22..813b4c7 100644 --- a/ucioption.h +++ b/ucioption.h @@ -30,76 +30,76 @@ namespace engine { // Define a custom comparator, because the UCI options should be // case-insensitive struct CaseInsensitiveLess { - bool operator()(const std::string &, const std::string &) const; + bool operator()(const std::string &, const std::string &) const; }; class OptionsMap; // The Option class implements each option as specified by the UCI protocol class Option { -public: - using OnChange = std::function(const Option &)>; - - Option(const OptionsMap *); - Option(OnChange = nullptr); - Option(bool v, OnChange = nullptr); - Option(const char *v, OnChange = nullptr); - Option(int v, int minv, int maxv, OnChange = nullptr); - Option(const char *v, const char *cur, OnChange = nullptr); - - Option &operator=(const std::string &); - operator int() const; - operator std::string() const; - bool operator==(const char *) const; - bool operator!=(const char *) const; - - friend std::ostream &operator<<(std::ostream &, const OptionsMap &); - - int operator<<(const Option &) = delete; - -private: - friend class OptionsMap; - friend class Engine; - friend class Tune; - - std::string defaultValue, currentValue, type; - int min, max; - size_t idx; - OnChange on_change; - const OptionsMap *parent = nullptr; + public: + using OnChange = std::function(const Option &)>; + + Option(const OptionsMap *); + Option(OnChange = nullptr); + Option(bool v, OnChange = nullptr); + Option(const char *v, OnChange = nullptr); + Option(int v, int minv, int maxv, OnChange = nullptr); + Option(const char *v, const char *cur, OnChange = nullptr); + + Option &operator=(const std::string &); + operator int() const; + operator std::string() const; + bool operator==(const char *) const; + bool operator!=(const char *) const; + + friend std::ostream &operator<<(std::ostream &, const OptionsMap &); + + int operator<<(const Option &) = delete; + + private: + friend class OptionsMap; + friend class Engine; + friend class Tune; + + std::string defaultValue, currentValue, type; + int min, max; + size_t idx; + OnChange on_change; + const OptionsMap *parent = nullptr; }; class OptionsMap { -public: - using InfoListener = std::function)>; + public: + using InfoListener = std::function)>; - OptionsMap() = default; - OptionsMap(const OptionsMap &) = delete; - OptionsMap(OptionsMap &&) = delete; - OptionsMap &operator=(const OptionsMap &) = delete; - OptionsMap &operator=(OptionsMap &&) = delete; + OptionsMap() = default; + OptionsMap(const OptionsMap &) = delete; + OptionsMap(OptionsMap &&) = delete; + OptionsMap &operator=(const OptionsMap &) = delete; + OptionsMap &operator=(OptionsMap &&) = delete; - void add_info_listener(InfoListener &&); + void add_info_listener(InfoListener &&); - void setoption(std::istringstream &); + void setoption(std::istringstream &); - const Option &operator[](const std::string &) const; + const Option &operator[](const std::string &) const; - void add(const std::string &, const Option &option); + void add(const std::string &, const Option &option); - std::size_t count(const std::string &) const; + std::size_t count(const std::string &) const; -private: - friend class Engine; - friend class Option; + private: + friend class Engine; + friend class Option; - friend std::ostream &operator<<(std::ostream &, const OptionsMap &); + friend std::ostream &operator<<(std::ostream &, const OptionsMap &); - // The options container is defined as a std::map - using OptionsStore = std::map; + // The options container is defined as a std::map + using OptionsStore = std::map; - OptionsStore options_map; - InfoListener info; + OptionsStore options_map; + InfoListener info; }; } // namespace engine