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") {