diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9914b5..fa9b3e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,47 +13,33 @@ jobs: strategy: matrix: config: - #- { - # name: "Ubuntu g++ popcnt", - # os: ubuntu-latest, - # compiler: g++-11, - # arch: popcnt, - # target: BlackCore-popcnt-linux, - #} - #- { - # name: "Ubuntu g++ modern", - # os: ubuntu-latest, - # compiler: g++-11, - # arch: modern, - # target: BlackCore-modern-linux, - #} - #- { - # name: "Ubuntu g++ avx2", - # os: ubuntu-latest, - # compiler: g++-11, - # arch: avx2, - # target: BlackCore-avx2-linux, - #} - #- { - # name: "Ubuntu g++ bmi2", - # os: ubuntu-latest, - # compiler: g++-11, - # arch: bmi2, - # target: BlackCore-bmi2-linux, - #} - { - name: "Windows g++ popcnt", - os: windows-latest, - compiler: g++, + name: "Ubuntu g++ popcnt", + os: ubuntu-latest, + compiler: g++-11, arch: popcnt, - target: BlackCore-popcnt-win.exe, + target: BlackCore-popcnt-linux, + } + - { + name: "Ubuntu g++ avx2", + os: ubuntu-latest, + compiler: g++-11, + arch: avx2, + target: BlackCore-avx2-linux, } - { - name: "Windows g++ modern", + name: "Ubuntu g++ bmi2", + os: ubuntu-latest, + compiler: g++-11, + arch: bmi2, + target: BlackCore-bmi2-linux, + } + - { + name: "Windows g++ popcnt", os: windows-latest, compiler: g++, arch: popcnt, - target: BlackCore-modern-win.exe, + target: BlackCore-popcnt-win.exe, } - { name: "Windows g++ avx2", @@ -96,3 +82,4 @@ jobs: with: name: BlackCore path: src/${{matrix.config.target}} + diff --git a/.gitignore b/.gitignore index da6ed1a..df2dbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ # Project exclude paths -/cmake-build-debug/ \ No newline at end of file +/release/ +/testing/ +/.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 1fe69af..2f4378b 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,62 @@ ## Overview -BlackCore is a grandmaster level UCI compatible c++ chess engine written from scratch. +BlackCore is a UCI compatible c++ chess engine written from scratch. Its alpha beta search uses various pruning techniques, powered by a neural network evaluation and a blazing fast move generator. +## Playing strength - Last updated 2022. dec. 26. + +| Version | CCRL Blitz elo | CCRL 40/15 elo | +|:----------|:------------------:|-----------------------------:| +| v4.0 4CPU | ~3200 (estiamtion) | ~3200 (estiamtion) | +| v4.0 1CPU | ~3100 (estiamtion) | ~3100 (estiamtion) | +| v3.0 1CPU | 3069 | 3035 | +| v2.0 1CPU | N/A | 2982 | +| v1.0 1CPU | 2134 | N/A | + +## Installation + +### Downloading prebuilt binary + +You can download the latest release here both for +Windows and Linux. +To select the right binary use the first instruction set that your CPU supports (doesn't crash), in the order of BMI2 -> +AVX2 -> popcnt + +### Building from source (recommended) + +After downloading the source, you can run the following commands, to build +a native binary. +This option gives the best performance. +**Please update your compiler before building!** + +With any questions or problems feel free to create a github issue. + +``` +cd src +make clean build CXX=g++ ARCH=native +``` + +ARCH = popcnt/avx2/bmi2/native + +CXX = the compiler of your choice (I recommend using g++, as it gives the best performance) + +## Usage + +BlackCore in itself is a command line program, and requires a UCI compatible +Chess GUI (like Cute Chess +or Arena) for the best user experience. + +### UCI Options + +- **Hash** - The size of the Hash table in MB. +- **Threads** - The amount of threads that can be used in the search +- **Move Overhead** - The delay (in ms) between finding the best move and the GUI reacting to it. You may want to make + this + higher if you notice that the engine often runs out of time. + + ## Files This project contains the following files: @@ -42,9 +94,12 @@ This project contains the following files: * Entry aging * Bucket system * Principal variation search - * Late move reduction + * Late move reduction/extension * R = max(2, LMR_BASE + (ln(moveIndex) * ln(depth) / LMR_SCALE))); * Move count/late move pruning + * Futility pruning + * Singular extension + * Check extension * Razoring * Reverse futility pruning * Null move pruning @@ -55,8 +110,10 @@ This project contains the following files: * Move ordering * Hash move * MVV-LVA and SEE - * Killer and history heuristics - * Fast repetition detection + * Killer, counter and history heuristics + * History difference - killer move replacement + * Multithreading support + * Lazy SMP * Time management based on search stability * NNUE evaluation * Trained using CoreTrainer @@ -64,47 +121,6 @@ This project contains the following files: * Support for AVX2 architecture for vectorized accumulator updates * Net embedded using incbin (for license see /src/incbin/UNLICENSE) -## Installation - -### Building from source (recommended) - -After downloading the source, you can run the following commands, to build -a native binary. -This option gives the best performance. -**Please update your compiler before building!** - -With any questions or problems feel free to create a github issue. - -``` -cd src -make clean build CXX=g++ ARCH=native -``` - -ARCH = popcnt/modern/avx2/bmi2/native - -CXX = the compiler of your choice (I recommend using g++, as it gives the best performance) - -### Downloading prebuilt binary - -You can download the latest release here both for -Windows and Linux. -To select the right binary use the first instruction set that your CPU supports (doesn't crash), in the order of BMI2 -> -AVX2 -> modern -> popcnt - -## Usage - -BlackCore in itself is a command line program, and requires a UCI compatible -Chess GUI (like Cute Chess -or Arena) for the best user experience. - -### UCI Options - -- **Hash** - The size of the Hash table in MB. -- **Threads** - Currently BlackCore only supports single threaded search, but this will probably change in the future. -- **Move Overhead** - The delay (in ms) between finding the best move and the GUI reacting to it. You may want to make - this - higher if you notice that the engine often runs out of time. - ## Special thanks to ### Chess Programming Wiki diff --git a/scripts/make_release.sh b/scripts/make_release.sh new file mode 100644 index 0000000..625755d --- /dev/null +++ b/scripts/make_release.sh @@ -0,0 +1,11 @@ +#!/bin/bash +cd .. +mkdir release +cd src +for ARCH in 'popcnt' 'avx2' 'bmi2' +do + make clean + make -j CXX=x86_64-w64-mingw32-g++-posix EXE=../release/BlackCore-$ARCH-win.exe ARCH=$ARCH + make clean + make -j CXX=g++ EXE=../release/BlackCore-$ARCH-linux ARCH=$ARCH +done \ No newline at end of file diff --git a/src/Makefile b/src/Makefile index 3303477..1096cd8 100644 --- a/src/Makefile +++ b/src/Makefile @@ -2,7 +2,7 @@ CXX = g++ TARGET_FLAGS = -static -static-libgcc -static-libstdc++ ARCH=native NAME = BlackCore -VERSION_MAJOR = 3 +VERSION_MAJOR = 4 VERSION_MINOR = 0 OBJECT_DIR = objects SOURCES := $(wildcard *.cpp) @@ -26,19 +26,15 @@ ifeq ($(ARCH), native) endif ifeq ($(ARCH), bmi2) - ARCH_FLAGS = -march=x86-64 -mpopcnt -msse -msse2 -mssse3 -msse4.1 -mavx2 -mbmi2 + ARCH_FLAGS = -march=x86-64 -mpopcnt -msse -msse2 -mssse3 -msse4.1 -mavx2 -mbmi -mbmi2 DEFINE_FLAGS = -DAVX2 -DBMI2 endif ifeq ($(ARCH), avx2) - ARCH_FLAGS = -march=x86-64 -mpopcnt -msse -msse2 -mssse3 -msse4.1 -mavx2 + ARCH_FLAGS = -march=x86-64 -mpopcnt -msse -msse2 -mssse3 -msse4.1 -mavx2 -mbmi DEFINE_FLAGS = -DAVX2 endif -ifeq ($(ARCH), modern) - ARCH_FLAGS = -march=x86-64 -mpopcnt -msse -msse2 -mssse3 -msse4.1 -endif - ifeq ($(ARCH), popcnt) ARCH_FLAGS = -march=x86-64 -mpopcnt endif diff --git a/src/bench.cpp b/src/bench.cpp index 08d05d9..ec97753 100644 --- a/src/bench.cpp +++ b/src/bench.cpp @@ -92,9 +92,12 @@ void testSearch() { info.maxDepth = SEARCH_DEPTH; info.uciMode = false; startSearch(info, pos, 1); - joinThread(true); - totalNodes += nodeCount; - nps += getNps(); + + while (!stopped) {} + totalNodes += getTotalNodes(); + nps += getNps(getTotalNodes()); + + joinThreads(true); } std::cout << totalNodes << " nodes " << nps / posCount << " nps" << std::endl; diff --git a/src/move_ordering.cpp b/src/move_ordering.cpp deleted file mode 100644 index dbdbdec..0000000 --- a/src/move_ordering.cpp +++ /dev/null @@ -1,96 +0,0 @@ -// BlackCore is a UCI Chess engine -// Copyright (c) 2022 SzilBalazs -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "move_ordering.h" -#include "search.h" -#include "tt.h" - -#include - -constexpr Score MVV_LVA[6][6] = { - // KING PAWN KNIGHT BISHOP ROOK QUEEN - {0, 0, 0, 0, 0, 0}, // KING - {0, 800004, 800104, 800204, 800304, 800404},// PAWN - {0, 800003, 800103, 800203, 800303, 800403},// KNIGHT - {0, 800002, 800102, 800202, 800302, 800402},// BISHOP - {0, 800001, 800101, 800201, 800301, 800401},// ROOK - {0, 800000, 800100, 800200, 800300, 800400},// QUEEN -}; - -constexpr Score winningCapture = 800000; -constexpr Score losingCapture = 200000; - -Move killerMoves[MAX_PLY + 1][2]; - -// TODO Counter move history -Score historyTable[2][64][64]; - -void clearTables() { - std::memset(killerMoves, 0, sizeof(killerMoves)); - std::memset(historyTable, 0, sizeof(historyTable)); -} - -void recordKillerMove(Move m, Ply ply) { - killerMoves[ply][1] = killerMoves[ply][0]; - killerMoves[ply][0] = m; -} - -void recordHHMove(Move move, Color color, Depth depth) { - historyTable[color][move.getFrom()][move.getTo()] += depth * depth; -} - -Score scoreQMove(const Position &pos, Move m) { - if (m == getHashMove(pos.getHash())) { - return 1000000; - } else if (m.isPromo()) { - if (m.isSpecial1() && m.isSpecial2()) {// Queen promo - return 900000; - } else {// Anything else, under promotions should only be played in really few cases - return -100000; - } - } else { - Score seeScore = see(pos, m); - - if (seeScore >= 0) - return winningCapture + seeScore; - else - return losingCapture + seeScore; - } -} - -Score scoreMove(const Position &pos, Move m, Ply ply) { - if (m == getHashMove(pos.getHash())) { - return 1000000; - } else if (m.isPromo()) { - if (m.isSpecial1() && m.isSpecial2()) {// Queen promo - return 900000; - } else {// Anything else, under promotions should only be played in really few cases - return -100000; - } - } else if (m.isCapture()) { - Score seeScore = see(pos, m); - - if (see(pos, m) >= 0) - return winningCapture + seeScore; - else - return losingCapture + seeScore; - } else if (killerMoves[ply][0] == m) { - return 750000; - } else if (killerMoves[ply][1] == m) { - return 700000; - } - return historyTable[pos.getSideToMove()][m.getFrom()][m.getTo()]; -} \ No newline at end of file diff --git a/src/move_ordering.h b/src/move_ordering.h deleted file mode 100644 index 6c218ef..0000000 --- a/src/move_ordering.h +++ /dev/null @@ -1,35 +0,0 @@ -// BlackCore is a UCI Chess engine -// Copyright (c) 2022 SzilBalazs -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#ifndef BLACKCORE_MOVE_ORDERING_H -#define BLACKCORE_MOVE_ORDERING_H - -#include "move.h" -#include "position.h" - -extern Move killerMoves[MAX_PLY + 1][2]; - -Score scoreMove(const Position &pos, Move m, Ply ply); - -Score scoreQMove(const Position &pos, Move m); - -void clearTables(); - -void recordKillerMove(Move m, Ply ply); - -void recordHHMove(Move move, Color color, Depth depth); - -#endif//BLACKCORE_MOVE_ORDERING_H diff --git a/src/movegen.h b/src/movegen.h index e77a87c..17567d4 100644 --- a/src/movegen.h +++ b/src/movegen.h @@ -18,8 +18,8 @@ #define BLACKCORE_MOVEGEN_H #include "move.h" -#include "move_ordering.h" #include "position.h" +#include "threads.h" template inline Bitboard getAttackers(const Position &pos, Square square) { @@ -51,20 +51,13 @@ struct MoveList { unsigned int count; - MoveList(const Position &pos, Ply ply, bool capturesOnly) { + MoveList(const Position &pos, ThreadData &td, Move prevMove, bool capturesOnly, bool rootNode) { movesEnd = generateMoves(pos, moves, capturesOnly); index = 0; count = movesEnd - moves; - // Scoring moves - if (capturesOnly) { - for (unsigned int i = 0; i < count; i++) { - scores[i] = scoreQMove(pos, moves[i]); - } - } else { - for (unsigned int i = 0; i < count; i++) { - scores[i] = scoreMove(pos, moves[i], ply); - } + for (unsigned int i = 0; i < count; i++) { + scores[i] = rootNode ? td.scoreRootNode(moves[i]) : td.scoreMove(pos, prevMove, moves[i]); } } diff --git a/src/position.cpp b/src/position.cpp index e131c51..96bb6cd 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -25,8 +25,6 @@ using std::cout, std::string; -U64 nodeCount = 0; - void Position::clearPosition() { for (auto &i : pieceBB) { i = 0; @@ -214,27 +212,30 @@ void Position::loadPositionFromRawState(const RawState &rawState) { state->stm = rawState.stm; state->epSquare = rawState.epSquare; state->castlingRights = rawState.castlingRights; + state->hash = rawState.hash; allPieceBB[WHITE] = rawState.allPieceBB[WHITE]; allPieceBB[BLACK] = rawState.allPieceBB[BLACK]; - + for (int i = 0; i < 6; i++) { pieceBB[i] = rawState.pieceBB[i]; } -#ifndef TUNE + for (Square sq = A1; sq < 64; sq += 1) { board[sq] = rawState.board[sq]; } -#endif + + state->accumulator.refresh(*this); } -RawState Position::getRawState() { +RawState Position::getRawState() const { RawState rawState; rawState.stm = getSideToMove(); rawState.epSquare = getEpSquare(); rawState.castlingRights = getCastlingRights(); + rawState.hash = getHash(); rawState.allPieceBB[WHITE] = allPieceBB[WHITE]; rawState.allPieceBB[BLACK] = allPieceBB[BLACK]; - + for (int i = 0; i < 6; i++) { rawState.pieceBB[i] = pieceBB[i]; } diff --git a/src/position.h b/src/position.h index 0750833..f7af164 100644 --- a/src/position.h +++ b/src/position.h @@ -23,8 +23,6 @@ #include "utils.h" #include -extern U64 nodeCount; - struct BoardState { Color stm = COLOR_EMPTY; Square epSquare = NULL_SQUARE; @@ -46,11 +44,12 @@ struct RawState { Color stm = COLOR_EMPTY; Square epSquare = NULL_SQUARE; unsigned char castlingRights = 0; + U64 hash = 0; }; struct StateStack { - BoardState stateStart[1000]; + BoardState stateStart[500]; BoardState *currState; StateStack() { @@ -198,7 +197,7 @@ class Position { void loadPositionFromRawState(const RawState &rawState); - RawState getRawState(); + RawState getRawState() const; Position(); @@ -290,7 +289,6 @@ void Position::movePiece(Square from, Square to) { template void Position::makeMove(Move move) { - nodeCount++; BoardState newState; diff --git a/src/search.cpp b/src/search.cpp index b0c0bb4..e0f1b9a 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -16,12 +16,14 @@ #include "search.h" #include "eval.h" +#include "threads.h" #include "timeman.h" #include "tt.h" #include "uci.h" #include #include +#include #ifdef TUNE @@ -53,12 +55,23 @@ Score SEE_MARGIN = 2; #endif -Ply selectiveDepth = 0; -Move bestPV; - // Move index -> depth Depth reductions[200][MAX_PLY + 1]; +std::mutex mNodesSearched; +U64 nodesSearched[64][64]; + +std::vector tds; +std::vector ths; + +U64 getTotalNodes() { + U64 totalNodes = 0; + for (ThreadData &td : tds) { + totalNodes += td.nodes; + } + return totalNodes; +} + void initLmr() { for (int moveIndex = 0; moveIndex < 200; moveIndex++) { for (Depth depth = 0; depth < MAX_PLY; depth++) { @@ -131,16 +144,16 @@ Score see(const Position &pos, Move move) { } template -Score quiescence(Position &pos, Score alpha, Score beta, Ply ply) { +Score quiescence(Position &pos, ThreadData &td, Score alpha, Score beta, Ply ply) { constexpr bool pvNode = type != NON_PV_NODE; constexpr bool nonPvNode = !pvNode; - if (shouldEnd()) + if (shouldEnd(td.nodes, getTotalNodes())) return UNKNOWN_SCORE; - if (ply > selectiveDepth) { - selectiveDepth = ply; + if (ply > td.selectiveDepth) { + td.selectiveDepth = ply; } bool ttHit = false; @@ -163,7 +176,7 @@ Score quiescence(Position &pos, Score alpha, Score beta, Ply ply) { alpha = staticEval; } - MoveList moves = {pos, ply, true}; + MoveList moves = {pos, td, Move(), true, false}; EntryFlag ttFlag = ALPHA; Move bestMove; @@ -181,13 +194,14 @@ Score quiescence(Position &pos, Score alpha, Score beta, Ply ply) { if (alpha > -WORST_MATE && see(pos, m) < -SEE_MARGIN) continue; + td.nodes++; pos.makeMove(m); - Score score = -quiescence(pos, -beta, -alpha, ply + 1); + Score score = -quiescence(pos, td, -beta, -alpha, ply + 1); pos.undoMove(m); - if (shouldEnd()) + if (shouldEnd(td.nodes, getTotalNodes())) return UNKNOWN_SCORE; if (score >= beta) { @@ -207,26 +221,27 @@ Score quiescence(Position &pos, Score alpha, Score beta, Ply ply) { } template -Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score beta, Ply ply) { +Score search(Position &pos, ThreadData &td, SearchStack *stack, Depth depth, Score alpha, Score beta, Ply ply) { constexpr bool rootNode = type == ROOT_NODE; constexpr bool pvNode = type != NON_PV_NODE; constexpr bool notRootNode = !rootNode; constexpr bool nonPvNode = !pvNode; constexpr NodeType nextPv = rootNode ? PV_NODE : type; + const bool isSingularRoot = !stack->excludedMove.isNull(); + + td.pvLength[ply] = ply; - if (shouldEnd()) + if (shouldEnd(td.nodes, getTotalNodes())) return UNKNOWN_SCORE; if (notRootNode && pos.getMove50() >= 3 && pos.isRepetition()) { - alpha = DRAW_VALUE; - if (alpha >= beta) - return alpha; + return DRAW_VALUE; } bool ttHit = false; Score matePly = MATE_VALUE - ply; - TTEntry *ttEntry = ttProbe(pos.getHash(), ttHit, depth, alpha, beta); + TTEntry *ttEntry = isSingularRoot ? nullptr : ttProbe(pos.getHash(), ttHit, depth, alpha, beta); if (ttHit && nonPvNode && ttEntry->depth >= depth && (ttEntry->flag == EXACT || (ttEntry->flag == ALPHA && ttEntry->eval <= alpha) || (ttEntry->flag == BETA && ttEntry->eval >= beta))) { @@ -248,7 +263,7 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score } if (depth <= 0) - return quiescence(pos, alpha, beta, ply); + return quiescence(pos, td, alpha, beta, ply); Color color = pos.getSideToMove(); bool inCheck = bool(getAttackers(pos, pos.pieces(color).lsb())); @@ -257,11 +272,11 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score bool improving = ply >= 2 && staticEval >= (stack - 2)->eval; - if (notRootNode && !inCheck) { + if (notRootNode && !inCheck && !isSingularRoot) { // Razoring if (depth == 1 && nonPvNode && staticEval + RAZOR_MARGIN < alpha) { - return quiescence(pos, alpha, beta, ply); + return quiescence(pos, td, alpha, beta, ply); } // Reverse futility pruning @@ -280,7 +295,7 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score stack->move = Move(); pos.makeNullMove(); - Score score = -search(pos, stack + 1, depth - R, -beta, -beta + 1, ply + 1); + Score score = -search(pos, td, stack + 1, depth - R, -beta, -beta + 1, ply + 1); pos.undoNullMove(); if (score >= beta) { @@ -298,15 +313,14 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score depth--; if (depth <= 0) - return quiescence(pos, alpha, beta, ply); + return quiescence(pos, td, alpha, beta, ply); } - // Check extension - if (inCheck) - depth++; - - MoveList moves = {pos, ply, false}; + MoveList moves = {pos, td, (ply >= 1 ? (stack - 1)->move : Move()), false, (rootNode && depth >= 6)}; if (moves.count == 0) { + if (isSingularRoot) + return alpha; + if (inCheck) { return -matePly; } else { @@ -317,13 +331,22 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score Move bestMove; EntryFlag ttFlag = ALPHA; int index = 0; - + std::vector quiets; while (!moves.empty()) { Move m = moves.nextMove(); stack->move = m; + if (m == stack->excludedMove) continue; + + U64 nodesBefore = td.nodes; + + if (rootNode && td.uciMode) { + if (getSearchTime() > 6000) out("info", "depth", depth, "currmove", m, "currmovenumber", index + 1); + } + Score score; + Score history = td.historyTable[color][m.getFrom()][m.getTo()]; // We can prune the move in some cases if (notRootNode && nonPvNode && !inCheck && alpha > -WORST_MATE) { @@ -336,9 +359,34 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score // Late move/movecount pruning // This will also prune losing captures if (depth <= LMP_DEPTH && index >= LMP_MOVES + depth * depth && m.isQuiet()) - break; + continue; + } + + // Extensions + Depth extensions = 0; + + if (inCheck) extensions = 1; + else if (notRootNode && depth >= SINGULAR_DEPTH && ttHit && m == ttEntry->hashMove && !isSingularRoot && ttEntry->flag == BETA && ttEntry->depth >= depth - 3) { + // This implementation is heavily inspired by StockFish & Alexandria + Score singularBeta = ttEntry->eval - depth * 3; + Depth singularDepth = (depth - 1) / 2; + + stack->excludedMove = m; + score = search(pos, td, stack, singularDepth, singularBeta - 1, singularBeta, ply); + stack->excludedMove = Move(); + + if (score < singularBeta) { + extensions = 1; + } else if (singularBeta >= beta) { + return singularBeta; + } else if (ttEntry->eval >= beta) { + extensions = -1; + } } + Depth newDepth = depth - 1 + extensions; + + td.nodes++; pos.makeMove(m); ttPrefetch(pos.getHash()); @@ -349,39 +397,55 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score Depth R = reductions[index][depth]; - R += improving; + R += !improving; R -= pvNode; + R -= std::clamp(history / 3000, -1, 1); + R -= (td.killerMoves[ply][0] == m || td.killerMoves[ply][1] == m) || (ply >= 1 && td.counterMoves[(stack - 1)->move.getFrom()][(stack - 1)->move.getTo()] == m); - Depth newDepth = std::clamp(depth - R, 1, depth - 1); + Depth D = std::clamp(newDepth - R, 1, newDepth + 1); - score = -search(pos, stack + 1, newDepth, + score = -search(pos, td, stack + 1, D, -alpha - 1, -alpha, ply + 1); - if (score > alpha && R > 1) { - score = -search(pos, stack + 1, depth - 1, -alpha - 1, -alpha, ply + 1); + if (score > alpha && R > 0) { + score = -search(pos, td, stack + 1, newDepth, -alpha - 1, -alpha, ply + 1); } } else if (nonPvNode || index != 0) { - score = -search(pos, stack + 1, depth - 1, -alpha - 1, -alpha, ply + 1); + score = -search(pos, td, stack + 1, newDepth, -alpha - 1, -alpha, ply + 1); } if (pvNode && (index == 0 || (score > alpha && score < beta))) { - score = -search(pos, stack + 1, depth - 1, -beta, -alpha, ply + 1); + score = -search(pos, td, stack + 1, newDepth, -beta, -alpha, ply + 1); } pos.undoMove(m); - if (shouldEnd()) + if (rootNode) { + td.updateNodesSearched(m, td.nodes - nodesBefore); + } + + if (shouldEnd(td.nodes, getTotalNodes())) return UNKNOWN_SCORE; if (score >= beta) { - if (m.isQuiet()) { - recordKillerMove(m, ply); - recordHHMove(m, color, depth); + if (!isSingularRoot) { + if (m.isQuiet()) { + + td.updateHistoryDifference(color, m, pos.occupied()); + td.updateKillerMoves(m, ply); + if (ply >= 1 && !(stack - 1)->move.isNull()) td.updateCounterMoves((stack - 1)->move, m); + td.updateHH(m, color, depth * depth); + + for (Move move : quiets) { + td.updateHH(move, color, -depth * depth); + } + } + + ttSave(pos.getHash(), depth, beta, BETA, m); } - ttSave(pos.getHash(), depth, beta, BETA, m); return beta; } @@ -389,33 +453,39 @@ Score search(Position &pos, SearchStack *stack, Depth depth, Score alpha, Score alpha = score; bestMove = m; ttFlag = EXACT; + + td.pvArray[ply][ply] = m; + for (int i = ply + 1; i < td.pvLength[ply + 1]; i++) { + td.pvArray[ply][i] = td.pvArray[ply + 1][i]; + } + td.pvLength[ply] = td.pvLength[ply + 1]; } + if (m.isQuiet()) quiets.push_back(m); index++; } - ttSave(pos.getHash(), depth, alpha, ttFlag, bestMove); + if (!isSingularRoot) + ttSave(pos.getHash(), depth, alpha, ttFlag, bestMove); return alpha; } -std::string getPvLine(Position &pos) { - Move m = getHashMove(pos.getHash()); - if (!pos.isRepetition() && !m.isNull()) { - pos.makeMove(m); - std::string str = m.str() + " " + getPvLine(pos); - pos.undoMove(m); - return str; - } else { - return ""; +std::string getPvLine(ThreadData &td) { + std::string pv; + + for (int i = 0; i < td.pvLength[0]; i++) { + pv += td.pvArray[0][i].str() + " "; } + + return pv; } -Score searchRoot(Position &pos, Score prevScore, Depth depth, bool uci) { +Score searchRoot(Position &pos, ThreadData &td, Score prevScore, Depth depth) { + + if (td.threadId == 0) globalAge++; + td.clear(); - globalAge++; - clearTables(); - selectiveDepth = 0; SearchStack stateStack[MAX_PLY + 1]; Score alpha = -INF_SCORE; Score beta = INF_SCORE; @@ -427,7 +497,7 @@ Score searchRoot(Position &pos, Score prevScore, Depth depth, bool uci) { int iter = 1; while (true) { - if (shouldEnd()) + if (shouldEnd(td.nodes, getTotalNodes())) return UNKNOWN_SCORE; if (alpha < -ASPIRATION_BOUND) @@ -435,7 +505,7 @@ Score searchRoot(Position &pos, Score prevScore, Depth depth, bool uci) { if (beta > ASPIRATION_BOUND) beta = INF_SCORE; - Score score = search(pos, stateStack + 1, depth, alpha, beta, 0); + Score score = search(pos, td, stateStack + 1, depth, alpha, beta, 0); if (score == UNKNOWN_SCORE) return UNKNOWN_SCORE; @@ -445,10 +515,9 @@ Score searchRoot(Position &pos, Score prevScore, Depth depth, bool uci) { } else if (score >= beta) { beta = std::min(beta + iter * iter * ASPIRATION_DELTA, INF_SCORE); } else { - bestPV = getHashMove(pos.getHash()); - std::string pvLine = getPvLine(pos); - if (uci) { + std::string pvLine = getPvLine(td); + if (td.uciMode) { Score absScore = std::abs(score); int mateDepth = MATE_VALUE - absScore; std::string scoreStr = "cp " + std::to_string(score); @@ -465,8 +534,8 @@ Score searchRoot(Position &pos, Score prevScore, Depth depth, bool uci) { scoreStr = "mate " + std::to_string(matePly); } - out("info", "depth", depth, "seldepth", selectiveDepth, "nodes", nodeCount, "score", scoreStr, "time", - getSearchTime(), "nps", getNps(), "pv", pvLine); + out("info", "depth", depth, "seldepth", td.selectiveDepth, "nodes", getTotalNodes(), "score", scoreStr, "time", + getSearchTime(), "nps", getNps(getTotalNodes()), "pv", pvLine); } return score; @@ -476,8 +545,9 @@ Score searchRoot(Position &pos, Score prevScore, Depth depth, bool uci) { } } -void iterativeDeepening(Position pos, Depth depth, bool uci) { +void iterativeDeepening(Position pos, ThreadData &td, Depth depth) { + td.reset(); pos.getState()->accumulator.refresh(pos); Score prevScore; @@ -486,13 +556,13 @@ void iterativeDeepening(Position pos, Depth depth, bool uci) { int stability = 0; for (Depth currDepth = 1; currDepth <= depth; currDepth++) { - Score score = searchRoot(pos, prevScore, currDepth, uci); + Score score = searchRoot(pos, td, prevScore, currDepth + (td.threadId & 1)); if (score == UNKNOWN_SCORE) break; // We only care about stability if we searched enough depth - if (currDepth >= 16) { - if (bestMove != bestPV) { + if (currDepth >= 14 && td.threadId == 0) { + if (bestMove != td.pvArray[0][0]) { stability -= 10; } else { if (std::abs(prevScore - score) >= std::max(prevScore / 10, 50)) { @@ -506,31 +576,43 @@ void iterativeDeepening(Position pos, Depth depth, bool uci) { } prevScore = score; - bestMove = bestPV; + bestMove = td.pvArray[0][0]; } - if (uci) { + if (td.uciMode) { out("bestmove", bestMove); } - searchStopped() = true; + stopped = true; } -#include - -std::thread th; - -void joinThread(bool waitToFinish) { +void joinThreads(bool waitToFinish) { if (!waitToFinish) - stopSearch(); + stopped = true; + + for (std::thread &th : ths) { + if (th.joinable()) + th.join(); + } - if (th.joinable()) - th.join(); + ths.clear(); + tds.clear(); } void startSearch(SearchInfo &searchInfo, Position &pos, int threadCount) { - joinThread(false); + joinThreads(false); + + for (int idx = 0; idx < threadCount; idx++) { + ThreadData td; + td.threadId = idx; + td.uciMode = searchInfo.uciMode && idx == 0; + tds.emplace_back(td); + } + + for (int idx = 0; idx < threadCount; idx++) { + tds[idx].position.loadPositionFromRawState(pos.getRawState()); + } Color stm = pos.getSideToMove(); if (stm == WHITE) { @@ -539,5 +621,7 @@ void startSearch(SearchInfo &searchInfo, Position &pos, int threadCount) { initTimeMan(searchInfo.btime, searchInfo.binc, searchInfo.movestogo, searchInfo.movetime, searchInfo.maxNodes); } - th = std::thread(iterativeDeepening, pos, searchInfo.maxDepth, searchInfo.uciMode); + for (int idx = 0; idx < threadCount; idx++) { + ths.emplace_back(iterativeDeepening, tds[idx].position, std::ref(tds[idx]), searchInfo.maxDepth); + } } \ No newline at end of file diff --git a/src/search.h b/src/search.h index 61459dc..614573f 100644 --- a/src/search.h +++ b/src/search.h @@ -64,7 +64,7 @@ constexpr Depth NULL_MOVE_BASE_R = 4; constexpr Depth NULL_MOVE_R_SCALE = 2; constexpr Depth LMR_DEPTH = 3; -constexpr double LMR_BASE = 1; +constexpr double LMR_BASE = 0; constexpr double LMR_SCALE = 1.65; constexpr int LMR_INDEX = 2; @@ -82,13 +82,17 @@ constexpr Score ASPIRATION_BOUND = 3000; constexpr Score SEE_MARGIN = 2; +constexpr Depth SINGULAR_DEPTH = 7; + #endif struct SearchStack { - Move move; + Move move, excludedMove; Score eval = 0; }; +U64 getTotalNodes(); + void initLmr(); inline void initSearch() { @@ -99,7 +103,7 @@ inline void initSearch() { Score see(const Position &pos, Move move); -void joinThread(bool waitToFinish); +void joinThreads(bool waitToFinish); void startSearch(SearchInfo &searchInfo, Position &pos, int threadCount); diff --git a/src/threads.h b/src/threads.h new file mode 100644 index 0000000..2e556c1 --- /dev/null +++ b/src/threads.h @@ -0,0 +1,147 @@ +// BlackCore is a UCI Chess engine +// Copyright (c) 2022 SzilBalazs +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef BLACKCORE_THREADS_H +#define BLACKCORE_THREADS_H + +#include "move.h" +#include "tt.h" + +#include +#include + +const int HISTORY_DIFF_SLOTS = 4; + +extern std::mutex mNodesSearched; +extern U64 nodesSearched[64][64]; + +Score see(const Position &pos, Move move); + +struct ThreadData { + + int threadId; + Position position; + + U64 nodes = 0; + Depth selectiveDepth = 0; + + bool uciMode = false; + + Move pvArray[MAX_PLY + 1][MAX_PLY + 1]; + int pvLength[MAX_PLY + 1]; + + // Move ordering + Move killerMoves[MAX_PLY + 1][2]; + Move counterMoves[64][64]; + Score historyTable[2][64][64]; + Bitboard historyDiff[2][64][64][HISTORY_DIFF_SLOTS]; + int historyDiffReplace[2][64][64]; + + inline void clear() { + selectiveDepth = 0; + + std::memset(pvArray, 0, sizeof(pvArray)); + std::memset(pvLength, 0, sizeof(pvLength)); + std::memset(killerMoves, 0, sizeof(killerMoves)); + std::memset(counterMoves, 0, sizeof(counterMoves)); + std::memset(historyTable, 0, sizeof(historyTable)); + std::memset(historyDiffReplace, 0, sizeof(historyDiffReplace)); + std::memset(historyDiff, 0, sizeof(historyDiff)); + } + + inline void reset() { + nodes = 0; + + mNodesSearched.lock(); + std::memset(nodesSearched, 0, sizeof(nodesSearched)); + mNodesSearched.unlock(); + + clear(); + } + + int getHistoryDifference(Color stm, Move move, Bitboard occ) { + int diff = 100; + for (int idx = 0; idx < HISTORY_DIFF_SLOTS; idx++) { + diff = std::min(diff, (historyDiff[stm][move.getFrom()][move.getTo()][idx] ^ occ).popCount()); + } + return diff; + } + + void updateHistoryDifference(Color stm, Move move, Bitboard pieces) { + Square from = move.getFrom(); + Square to = move.getTo(); + historyDiff[stm][from][to][historyDiffReplace[stm][from][to]] = pieces; + historyDiffReplace[stm][from][to]++; + historyDiffReplace[stm][from][to] %= HISTORY_DIFF_SLOTS; + } + + void updateKillerMoves(Move m, Ply ply) { + killerMoves[ply][1] = killerMoves[ply][0]; + killerMoves[ply][0] = m; + } + + void updateCounterMoves(Move prevMove, Move move) { + counterMoves[prevMove.getFrom()][prevMove.getTo()] = move; + } + + void updateHH(Move move, Color color, Score bonus) { + historyTable[color][move.getFrom()][move.getTo()] += bonus; + } + + void updateNodesSearched(Move m, U64 totalNodes) { + mNodesSearched.lock(); + nodesSearched[m.getFrom()][m.getTo()] += totalNodes; + mNodesSearched.unlock(); + } + + Score scoreRootNode(Move m) { + return nodesSearched[m.getFrom()][m.getTo()] / 1000; + } + + Score scoreMove(const Position &pos, Move prevMove, Move m) { + if (m == getHashMove(pos.getHash())) { + return 10000000; + } else if (m.isPromo()) { + if (m.isSpecial1() && m.isSpecial2()) {// Queen promo + return 9000000; + } else {// Anything else, under promotions should only be played in really few cases + return -3000000; + } + } else if (m.isCapture()) { + Score seeScore = see(pos, m); + + if (see(pos, m) >= 0) + return 8000000 + seeScore; + else + return 2000000 + seeScore; + } else if (counterMoves[prevMove.getFrom()][prevMove.getTo()] == m) { + return 5000000; + } + Color stm = pos.getSideToMove(); + Bitboard occ = pos.occupied(); + int diff = getHistoryDifference(stm, m, occ); + Score diffBonus = 0; + if (diff == 0) + diffBonus = 5600000; + else if (diff == 1) + diffBonus = 5500000; + else if (diff == 2) + diffBonus = 5400000; + return diffBonus + historyTable[stm][m.getFrom()][m.getTo()]; + } +}; + +#endif//BLACKCORE_THREADS_H diff --git a/src/timeman.cpp b/src/timeman.cpp index 1e0a515..140b9b3 100644 --- a/src/timeman.cpp +++ b/src/timeman.cpp @@ -15,17 +15,15 @@ // along with this program. If not, see . #include "timeman.h" -#include "position.h" #include unsigned int MOVE_OVERHEAD = 10; -constexpr U64 mask = 1023; +constexpr U64 mask = 2047; U64 startedSearch, shouldSearch, searchTime, maxSearch, stabilityTime, maxNodes; -bool stopping = true; -bool stopped = true; +std::atomic stopped = true; U64 getTime() { return std::chrono::duration_cast( @@ -34,13 +32,11 @@ U64 getTime() { } void initTimeMan(U64 time, U64 inc, U64 movesToGo, U64 moveTime, U64 nodes) { - nodeCount = 0; movesToGo = movesToGo == 0 ? 20 : movesToGo + 1; startedSearch = getTime(); stabilityTime = 0; - stopping = false; stopped = false; maxNodes = nodes; @@ -65,31 +61,23 @@ void initTimeMan(U64 time, U64 inc, U64 movesToGo, U64 moveTime, U64 nodes) { searchTime = shouldSearch; } -void stopSearch() { - stopping = true; -} - -bool &searchStopped() { - return stopped; -} - void allocateTime(int stability) { U64 newSearchTime = shouldSearch - stability * stabilityTime; searchTime = std::min(maxSearch, newSearchTime); } -bool shouldEnd() { - if ((nodeCount & mask) == 0 && !stopping) { - stopping = (maxSearch != 0 && getSearchTime() >= searchTime) || (maxNodes != 0 && nodeCount > maxNodes); +bool shouldEnd(U64 nodes, U64 totalNodes) { + if ((nodes & mask) == 0 && !stopped) { + stopped = (maxSearch != 0 && getSearchTime() >= searchTime) || (maxNodes != 0 && totalNodes > maxNodes); } - return stopping; + return stopped; } U64 getSearchTime() { return getTime() - startedSearch; } -U64 getNps() { +U64 getNps(U64 nodes) { U64 millis = getSearchTime(); - return millis == 0 ? 0 : nodeCount * 1000 / millis; + return millis == 0 ? 0 : nodes * 1000 / millis; } diff --git a/src/timeman.h b/src/timeman.h index ef6d339..97e80dc 100644 --- a/src/timeman.h +++ b/src/timeman.h @@ -18,21 +18,19 @@ #define BLACKCORE_TIMEMAN_H #include "constants.h" +#include extern unsigned int MOVE_OVERHEAD; +extern std::atomic stopped; void initTimeMan(U64 time, U64 inc, U64 movesToGo, U64 moveTime, U64 nodes); -bool shouldEnd(); - -void stopSearch(); - -bool &searchStopped(); +bool shouldEnd(U64 nodes, U64 totalNodes); void allocateTime(int stability); U64 getSearchTime(); -U64 getNps(); +U64 getNps(U64 nodes); #endif//BLACKCORE_TIMEMAN_H diff --git a/src/uci.cpp b/src/uci.cpp index 9927bce..c43c733 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -77,7 +77,7 @@ void uciLoop() { // We tell the GUI what options we have out("option", "name", "Hash", "type", "spin", "default", 32, "min", 1, "max", 4096); - out("option", "name", "Threads", "type", "spin", "default", 1, "min", 1, "max", 1); + out("option", "name", "Threads", "type", "spin", "default", 1, "min", 1, "max", 64); out("option", "name", "Ponder", "type", "check", "default", "false"); out("option", "name", "Move Overhead", "type", "spin", "default", 10, "min", 0, "max", 10000); @@ -133,10 +133,10 @@ void uciLoop() { if (command == "isready") { out("readyok"); } else if (command == "quit") { - joinThread(false); + joinThreads(false); break; } else if (command == "stop") { - joinThread(false); + joinThreads(false); } else if (command == "ucinewgame") { ttClear(); } else if (command == "setoption") { @@ -147,6 +147,8 @@ void uciLoop() { MOVE_OVERHEAD = std::stoi(tokens[4]); } else if (tokens[1] == "Ponder") { + } else if (tokens[1] == "Threads") { + threadCount = std::stoi(tokens[3]); } else { #ifdef TUNE if (tokens[1] == "DELTA_MARGIN") {