Skip to content

Commit 4c860a4

Browse files
committed
refactor: simple reimplementation
Implemented a new negamax algorithm, with only NMP so far; a new MovePicker which now only partially generate the moves for better speed; incorporated a new NNUE which improved a little bit on evaluation accuracy. A quick Elo test displayed an estimated Elo of 1450 on CCRL.
1 parent f9cd7f7 commit 4c860a4

File tree

12 files changed

+593
-85
lines changed

12 files changed

+593
-85
lines changed

.clang-format

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ BraceWrapping:
2828
IndentBraces: false
2929
BreakBeforeBraces: Attach
3030
BreakConstructorInitializers: AfterColon
31+
BreakTemplateDeclarations: Yes
3132
ColumnLimit: 100
3233
ConstructorInitializerAllOnOneLineOrOnePerLine: true
3334
ConstructorInitializerIndentWidth: 4

config.json

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"pgn": {
2626
"event_name": "Fastchess Tournament",
2727
"site": "?",
28-
"file": "elo_20250809-111634.pgn",
28+
"file": "elo_20250817-231021.pgn",
2929
"notation": 0,
3030
"track_nodes": false,
3131
"track_seldepth": false,
@@ -48,7 +48,7 @@
4848
},
4949
"config_name": "",
5050
"output": 0,
51-
"seed": 15386497898281713509,
51+
"seed": 6913807169441360982,
5252
"variant": 0,
5353
"ratinginterval": 10,
5454
"scoreinterval": 1,
@@ -90,9 +90,9 @@
9090
"variant": 0
9191
},
9292
{
93-
"name": "neuromancer",
93+
"name": "engine.0.1.1.core-avx2",
9494
"dir": "",
95-
"cmd": "build/neuromancer.exe",
95+
"cmd": "build/engine.0.1.1.core-avx2.exe",
9696
"args": "",
9797
"options": [],
9898
"limit": {
@@ -110,27 +110,27 @@
110110
}
111111
],
112112
"stats": {
113-
"neuromancer vs engine": {
114-
"wins": 0,
115-
"losses": 0,
113+
"engine.0.1.1.core-avx2 vs engine": {
114+
"wins": 2,
115+
"losses": 10,
116116
"draws": 0,
117117
"penta_WW": 0,
118118
"penta_WD": 0,
119-
"penta_WL": 0,
119+
"penta_WL": 2,
120120
"penta_DD": 0,
121121
"penta_LD": 0,
122-
"penta_LL": 0
122+
"penta_LL": 4
123123
},
124-
"engine vs neuromancer": {
125-
"wins": 0,
124+
"engine vs engine.0.1.1.core-avx2": {
125+
"wins": 4,
126126
"losses": 2,
127-
"draws": 0,
128-
"penta_WW": 0,
129-
"penta_WD": 0,
130-
"penta_WL": 0,
127+
"draws": 2,
128+
"penta_WW": 1,
129+
"penta_WD": 1,
130+
"penta_WL": 1,
131131
"penta_DD": 0,
132-
"penta_LD": 0,
133-
"penta_LL": 1
132+
"penta_LD": 1,
133+
"penta_LL": 0
134134
}
135135
}
136136
}

src/eval.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ class NNUEState {
8080
}
8181
}
8282

83-
template <bool activate> void update(const Piece piece, const Square square) {
83+
template <bool activate>
84+
void update(const Piece piece, const Square square) {
8485
update<activate>(piece.color(), piece.type(), square);
8586
}
8687

87-
template <bool activate> void update(const Color color, const PieceType pt, const Square sq) {
88+
template <bool activate>
89+
void update(const Color color, const PieceType pt, const Square sq) {
8890
const auto [wi, bi] = getFeatureIndices(color, pt, sq);
8991
constexpr int multiplier = (activate ? 1 : -1);
9092
for (int i = 0; i < FEATURE_SIZE; ++i) {
@@ -130,7 +132,7 @@ class NNUEState {
130132
}
131133
// Accumulate
132134
int y = w.fc2_bias + temp[0] / 127 + temp[1] / 127 + temp[2] / 127 + temp[3] / 127;
133-
y = y / 140;
135+
y = y / 170;
134136
return y;
135137
}
136138
};

src/history.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ class SearchHistory {
4141
};
4242

4343
public:
44-
KillerTable killerTable;
44+
KillerTable killerTable[MAX_PLY];
4545

46-
void clear() { killerTable.clear(); }
46+
void clear() {
47+
for (int i = 0; i < MAX_PLY; i++) {
48+
killerTable[i].clear();
49+
}
50+
}
4751
};

src/main.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#include "tt.h"
2-
#include "uci.h"
2+
#include "uci.h" // for ENGINE_VERSION
33
#include <iostream>
44

55
using namespace std;
66

77
int main() {
8-
cout << "Emerald Chess Engine by UndefinedCpp, version " << ENGINE_VERSION
9-
<< endl;
8+
cout << "Emerald Chess Engine by UndefinedCpp, version " << ENGINE_VERSION << endl;
109
tt.init(8 * 1024 * 1024); // todo refactor, currently fixed at 128MB
1110

11+
// Begin UCI loop
1212
std::string input;
1313
while (std::getline(std::cin, input)) {
1414
if (input == "quit") {

src/movepick.cpp

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,226 @@
11
#include "movepick.h"
22
#include <algorithm>
3+
4+
// clang-format off
5+
constexpr int16_t MVV_LVA_TABLE[7][7] = {
6+
// P N B R Q K none
7+
/* P */{ 0, 200, 250, 450, 900, 0, 0},
8+
/* N */{-200, 10, 50, 250, 700, 0, 0},
9+
/* B */{-250, -50, 5, 200, 650, 0, 0},
10+
/* R */{-450, -250, -200, 15, 450, 0, 0},
11+
/* Q */{-900, -700, -650, -450, 20, 0, 0},
12+
/* K */{ 0, 0, 0, 0, 0, 0, 0},
13+
/* */{ 0, 0, 0, 0, 0, 0, 0},
14+
};
15+
16+
constexpr int16_t CHECK_BONUS = 200;
17+
constexpr int16_t PROMOTION_BONUS = 200;
18+
// clang-format on
19+
20+
Move MovePicker::next() {
21+
switch (stage) {
22+
case MovePickerStage::TT: {
23+
// Generate TT move. First check if such move is legal.
24+
// If not, simply ignore it.
25+
Move ttMove = Move(ttMoveCode);
26+
if (ttMove.isValid() && pos.isLegal(ttMove)) {
27+
stage = MovePickerStage::GEN_NOISY;
28+
return ttMove;
29+
}
30+
}
31+
[[fallthrough]];
32+
33+
case MovePickerStage::GEN_NOISY:
34+
// Generate noisy moves
35+
generateNoisyMoves();
36+
stage = MovePickerStage::GOOD_NOISY;
37+
[[fallthrough]];
38+
39+
case MovePickerStage::GOOD_NOISY:
40+
// Pick a good noisy move
41+
while (!noisyBuffer.empty()) {
42+
const auto& scoredMove = noisyBuffer.back();
43+
if (scoredMove.score < 0) { // not a good move anymore
44+
break; // we are done
45+
}
46+
noisyBuffer.pop_back();
47+
if (scoredMove.moveCode == ttMoveCode) {
48+
continue; // do not yield the same move twice
49+
}
50+
return scoredMove.move();
51+
}
52+
stage = MovePickerStage::KILLER_1;
53+
[[fallthrough]];
54+
55+
case MovePickerStage::KILLER_1: {
56+
// Killer moves are never tactic moves, so they should never appear
57+
// in the noisy buffer.
58+
const Move killer1 = history.killerTable[ply].killer1;
59+
if (killer1.isValid() && pos.isLegal<movegen::MoveGenType::QUIET>(killer1)) {
60+
stage = MovePickerStage::KILLER_2;
61+
return killer1;
62+
}
63+
}
64+
[[fallthrough]];
65+
66+
case MovePickerStage::KILLER_2: {
67+
const Move killer2 = history.killerTable[ply].killer2;
68+
if (killer2.isValid() && pos.isLegal<movegen::MoveGenType::QUIET>(killer2)) {
69+
stage = MovePickerStage::GEN_QUIET;
70+
return killer2;
71+
}
72+
}
73+
[[fallthrough]];
74+
75+
case MovePickerStage::GEN_QUIET:
76+
if (_skipQuiet) { // Maybe used in pruning
77+
stage = MovePickerStage::END_NORMAL;
78+
} else {
79+
generateQuietMoves();
80+
stage = MovePickerStage::GOOD_QUIET;
81+
}
82+
[[fallthrough]];
83+
84+
case MovePickerStage::GOOD_QUIET:
85+
while (!quietBuffer.empty()) {
86+
const auto& scoredMove = quietBuffer.back();
87+
if (scoredMove.score < 0) { // not a good move anymore
88+
break; // we are done
89+
}
90+
quietBuffer.pop_back();
91+
if (scoredMove.moveCode == ttMoveCode ||
92+
scoredMove.moveCode == history.killerTable[ply].killer1 ||
93+
scoredMove.moveCode == history.killerTable[ply].killer2) {
94+
continue; // do not yield the same move twice
95+
}
96+
return scoredMove.move();
97+
}
98+
stage = MovePickerStage::BAD_NOISY;
99+
[[fallthrough]];
100+
101+
case MovePickerStage::BAD_NOISY:
102+
while (!noisyBuffer.empty()) {
103+
const auto& scoredMove = noisyBuffer.back();
104+
noisyBuffer.pop_back();
105+
if (scoredMove.moveCode == ttMoveCode) {
106+
continue;
107+
}
108+
return scoredMove.move();
109+
}
110+
stage = MovePickerStage::BAD_QUIET;
111+
[[fallthrough]];
112+
113+
case MovePickerStage::BAD_QUIET:
114+
while (!quietBuffer.empty()) {
115+
const auto& scoredMove = quietBuffer.back();
116+
quietBuffer.pop_back();
117+
if (scoredMove.moveCode == ttMoveCode ||
118+
scoredMove.moveCode == history.killerTable[ply].killer1 ||
119+
scoredMove.moveCode == history.killerTable[ply].killer2) {
120+
continue;
121+
}
122+
return scoredMove.move();
123+
}
124+
stage = MovePickerStage::END_NORMAL;
125+
[[fallthrough]];
126+
127+
case MovePickerStage::END_NORMAL:
128+
return Move(Move::NO_MOVE);
129+
130+
case MovePickerStage::GEN_QSEARCH:
131+
if (inCheck) {
132+
generateEvasionMoves();
133+
} else {
134+
generateNoisyMoves();
135+
}
136+
stage = MovePickerStage::GOOD_QSEARCH;
137+
[[fallthrough]];
138+
139+
case MovePickerStage::GOOD_QSEARCH:
140+
while (!noisyBuffer.empty()) {
141+
const auto& scoredMove = noisyBuffer.back();
142+
if (!inCheck && scoredMove.score < 0) {
143+
break;
144+
}
145+
noisyBuffer.pop_back();
146+
if (scoredMove.moveCode == ttMoveCode) {
147+
continue;
148+
}
149+
return scoredMove.move();
150+
}
151+
stage = MovePickerStage::END_QSEARCH;
152+
[[fallthrough]];
153+
154+
case MovePickerStage::END_QSEARCH:
155+
return Move(Move::NO_MOVE);
156+
157+
default:
158+
return Move(Move::NO_MOVE);
159+
}
160+
}
161+
162+
void MovePicker::skipQuiet() {
163+
_skipQuiet = true;
164+
}
165+
166+
void MovePicker::generateNoisyMoves() {
167+
Movelist noisyMoves;
168+
movegen::legalmoves<movegen::MoveGenType::CAPTURE>(noisyMoves, pos);
169+
// Assign scores to moves based on MVV/LVA & SEE
170+
for (const Move& move : noisyMoves) {
171+
int16_t score = 0;
172+
const auto fromSq = move.from();
173+
const auto toSq = move.to();
174+
const PieceType attacker = pos.at(fromSq).type();
175+
const PieceType victim =
176+
(move.typeOf() == Move::ENPASSANT) ? PieceType::PAWN : pos.at(toSq).type();
177+
const int16_t mvvlva = MVV_LVA_TABLE[(int) attacker][(int) victim];
178+
if (pos.see(move, 0)) { // static exchange evaluation indicates an acceptable capture
179+
score = mvvlva;
180+
// Additional bonus for checks
181+
if (pos.isCheckMove(move)) {
182+
score += CHECK_BONUS;
183+
}
184+
} else { // a losing capture
185+
score = mvvlva - 1000;
186+
}
187+
188+
noisyBuffer.emplace_back(ScoredMove {move.move(), score});
189+
}
190+
// Sort moves by score in ascending order
191+
std::sort(noisyBuffer.begin(), noisyBuffer.end());
192+
}
193+
194+
void MovePicker::generateQuietMoves() {
195+
Movelist quietMoves;
196+
movegen::legalmoves<movegen::MoveGenType::QUIET>(quietMoves, pos);
197+
198+
for (const Move& move : quietMoves) {
199+
int16_t score = 0;
200+
// Bonus for checks
201+
if (pos.isCheckMove(move)) {
202+
score += CHECK_BONUS;
203+
}
204+
// Promotion bonus
205+
if (move.typeOf() == Move::PROMOTION && move.promotionType() == PieceType::QUEEN) {
206+
score += PROMOTION_BONUS;
207+
}
208+
// Penalty for moving into squares controlled by opponent pawns
209+
if (pos.at(move.from()).type() != PieceType::PAWN &&
210+
(attacks::pawn(pos.sideToMove(), move.to()) &
211+
pos.pieces(PieceType::PAWN, ~pos.sideToMove()))) {
212+
score -= 200;
213+
}
214+
215+
quietBuffer.emplace_back(ScoredMove {move.move(), score});
216+
}
217+
std::sort(quietBuffer.begin(), quietBuffer.end());
218+
}
219+
220+
void MovePicker::generateEvasionMoves() {
221+
Movelist moves;
222+
movegen::legalmoves(moves, pos);
223+
for (const Move& move : moves) {
224+
noisyBuffer.emplace_back(ScoredMove {move.move(), 0});
225+
}
226+
}

0 commit comments

Comments
 (0)