1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
|
#include "bot.hpp"
#include "board/board.hpp"
#include "evaluate.hpp"
#include "moves.hpp"
#include "opening_book.hpp"
#include "zobrist.hpp"
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <ctime>
#include <iostream>
#include <iterator>
#include <vector>
constexpr int MAXIMUM_DEPTH = 6;
constexpr int MAXIMUM_TIME_PER_MOVE = 7;
constexpr int Q_DEPTH_LIMIT = 4;
constexpr int MATE = 10000;
// Material + PST bonuses can never reach this, so anything at/above it is a
// forced mate. Used to stop iterative deepening once a mate is found.
constexpr int MATE_THRESHOLD = MATE - 1000;
constexpr int INF = 100000000;
uint64_t Nodes = 0;
static int ScoreMove(const Game *board, const Move &move,
const Move *bestMove) {
// Indexed by PieceType (NONE, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING).
static constexpr int PIECE_VALUES[] = {
0, // NONEPIECE
100, // PAWN
320, // KNIGHT
330, // BISHOP
500, // ROOK
900, // QUEEN
20000, // KING (never captured in legal play)
};
static_assert(std::size(PIECE_VALUES) == KING + 1);
int score = 0;
// Search the previous iteration's best move first (iterative deepening).
if (bestMove != nullptr && move == *bestMove) {
score += 1000000;
}
const Piece moving = board->pieces[PositionToIndex(move.From)];
const Piece captured = board->pieces[PositionToIndex(move.To)];
// MVV-LVA: value the capture by what we win, penalise by what we spend.
if (captured.type != NONEPIECE) {
score += 10000;
score += PIECE_VALUES[captured.type] * 10;
score -= PIECE_VALUES[moving.type];
}
if (move.promotion != NONEPIECE) {
score += 8000;
}
return score;
}
static std::vector<Move> GetSortedLegalMoves(Game *g, bool generateQuietMoves,
const Move *bestMove) {
auto moves = GetLegalMoves(g, generateQuietMoves);
std::ranges::sort(moves, [&](const Move &a, const Move &c) {
return ScoreMove(g, a, bestMove) > ScoreMove(g, c, bestMove);
});
return moves;
}
// Quiescence search: only captures, so the eval isn't blind to hanging pieces.
// `standPat` lets the side to move decline every capture. Terminal positions
// are scored with the same `MATE - ply` convention as the main search so that
// mate distances stay consistent (a mate found inside quiescence must not look
// faster than a real mate-in-1). White perspective throughout.
static int quiescenceSearch(Game *b, int qdepth, int alpha, int beta, int ply) {
Nodes++;
// Runs GetNewGameState internally, so b->state is up to date afterwards.
const int standPat = EvaluateBoard(b);
switch (b->state) {
case WHITE_WON:
case BLACK_WON:
return -(MATE - ply);
break;
case DRAW:
return 0;
break;
case TURN:
break;
}
if (qdepth >= Q_DEPTH_LIMIT) {
return standPat;
}
if (standPat >= beta) {
return beta;
}
alpha = std::max(alpha, standPat);
auto moves = GetSortedLegalMoves(b, false, nullptr);
for (Move move : moves) {
UndoMove undo = MakeMove(move, b);
int score = -quiescenceSearch(b, qdepth + 1, -beta, -alpha, ply + 1);
UnMakeMove(undo, b);
if (score >= beta) {
return beta;
}
alpha = std::max(alpha, score);
};
return alpha;
};
static int search(int depth, Game *b, int alpha, int beta, int ply) {
Nodes++;
const uint64_t gameHash = GenerateZobristKey(b);
auto repIt = b->ThreeFoldMap.find(gameHash);
if (repIt != b->ThreeFoldMap.end() && repIt->second >= 2) {
return 0; // repetition -> draw
}
TranspositionsEntry *entry = nullptr;
if (auto it = b->Transpositions->find(gameHash);
it != b->Transpositions->end()) {
entry = &it->second;
if (entry->depth >= depth) {
if (entry->flag == EXACT) {
return entry->Eval;
}
if (entry->flag == LOWERBOUND) {
alpha = std::max(alpha, entry->Eval);
}
if (entry->flag == UPPERBOUND) {
beta = std::min(beta, entry->Eval);
}
if (alpha >= beta) {
return entry->Eval;
}
}
}
if (depth <= 0) {
return quiescenceSearch(b, 0, alpha, beta, ply);
}
Move ttBestMove = entry != nullptr ? entry->bestMove : Move{};
std::vector<Move> moves = GetSortedLegalMoves(b, true, &ttBestMove);
if (moves.empty()) {
const Position king = FindKing(b, b->turn);
if (IsSquareAttacked(b, king, !b->turn)) {
return -(MATE - ply); // mated
}
return 0; // stalemate
}
Move bestMove = moves[0];
const int alphaOrig = alpha;
int bestScore = -INF;
for (Move move : moves) {
UndoMove undo = MakeMove(move, b);
int score = -search(depth - 1, b, -beta, -alpha, ply + 1);
UnMakeMove(undo, b);
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
alpha = std::max(alpha, score);
if (alpha >= beta) {
break;
}
}
const Flag flag = bestScore <= alphaOrig ? UPPERBOUND
: bestScore >= beta ? LOWERBOUND
: EXACT;
(*b->Transpositions)[gameHash] = {
.depth = depth, .Eval = bestScore, .flag = flag, .bestMove = bestMove};
return bestScore;
}
struct SearchResult {
Move bestMove;
int score;
};
// Searches every root move to `depth` plies and returns the best one.
// `previousBest` is the best move from the previous iteration (used for move
// ordering, the core win of iterative deepening).
static SearchResult SearchDepth(Game *b, int depth, const Move *previousBest) {
auto moves = GetSortedLegalMoves(b, true, previousBest);
if (moves.empty()) {
return {{}, EvaluateBoardForWhite(b)};
}
Move bestMove = moves[0];
int bestEval = -INF;
int alpha = -INF;
int beta = INF;
for (Move move : moves) {
UndoMove undo = MakeMove(move, b);
int eval = -search(depth - 1, b, -beta, -alpha, 1);
UnMakeMove(undo, b);
if (eval > bestEval) {
bestEval = eval;
bestMove = move;
}
alpha = std::max(alpha, eval);
}
return {bestMove, bestEval};
}
static void PrintInfo(const int depth, const int engineScore,
const uint64_t nps) {
if (std::abs(engineScore) >= MATE_THRESHOLD) {
int movesToMate = (MATE - std::abs(engineScore) + 1) / 2;
if (movesToMate < 1) {
movesToMate = 1;
}
std::cout << "info depth " << depth << " score mate "
<< (engineScore > 0 ? movesToMate : -movesToMate) << "\n"
<< std::flush;
} else {
std::cout << "info depth " << depth << " score cp " << engineScore
<< " nodes " << Nodes << " nps " << nps << "\n"
<< std::flush;
}
}
Move GetBestMove(Game *b, const int maxDepth) {
Nodes = 0;
Polyglot_Book book;
book.Load("book.bin");
int actualDepth = maxDepth > 0 ? maxDepth : MAXIMUM_DEPTH;
bool usingDefaultDepth = actualDepth == MAXIMUM_DEPTH;
auto legalMoves = GetSortedLegalMoves(b, true, nullptr);
if (legalMoves.empty()) {
assert(false && "GetBestMove called with no legal moves");
return {};
}
Move bestMove = legalMoves[0];
time_t start = time(0);
auto startMili = std::chrono::steady_clock::now();
bool continueSearching = true;
int depth = 1;
while (continueSearching) {
if (book.HasMove(*b)) {
bestMove = book.GetMove(*b);
continueSearching = false;
break;
}
SearchResult result = SearchDepth(b, depth, &bestMove);
bestMove = result.bestMove;
auto now = std::chrono::steady_clock::now();
double seconds = std::chrono::duration<double>(now - startMili).count();
uint64_t nps =
seconds > 0
? static_cast<uint64_t>(static_cast<double>(Nodes) / seconds)
: Nodes;
PrintInfo(depth, result.score, nps);
// A mate was found; deeper searches can only find a faster one.
if (std::abs(result.score) >= MATE_THRESHOLD) {
break;
}
depth++;
if (depth >= actualDepth) {
continueSearching = false;
}
int since_start = static_cast<int>(difftime(time(0), start));
if (since_start >= MAXIMUM_TIME_PER_MOVE && usingDefaultDepth) {
continueSearching = false;
}
}
return bestMove;
}
|