Skip to content

Commit 2d1a66d

Browse files
committed
The board now knows its evaluation and updates it incrementally. Only This is cheaper than recomputing the positional score from scratch whenever needed. It doesn't change the search except making it ~15% faster. Worth about 20 ELO.
1 parent 5f6d037 commit 2d1a66d

File tree

6 files changed

+77
-41
lines changed

6 files changed

+77
-41
lines changed

MinimalChess/Board.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ public enum CastlingRights
4242
private Color _sideToMove = Color.White;
4343
private int _enPassantSquare = -1;
4444
private ulong _zobristHash = 0;
45+
private Evaluation.Eval _eval;
4546
/*** STATE DATA ***/
4647

48+
public int Score => _eval.Score;
49+
4750
public ulong ZobristHash => _zobristHash;
4851

4952
public Color SideToMove
@@ -83,16 +86,19 @@ public void Copy(Board board)
8386
_enPassantSquare = board._enPassantSquare;
8487
_castlingRights = board._castlingRights;
8588
_zobristHash = board._zobristHash;
89+
_eval = board._eval;
8690
}
8791

8892
public Piece this[int square]
8993
{
9094
get => _state[square];
9195
private set
9296
{
97+
_eval.Update(_state[square], value, square);
9398
_zobristHash ^= Zobrist.PieceSquare(_state[square], square);
9499
_state[square] = value;
95100
_zobristHash ^= Zobrist.PieceSquare(_state[square], square);
101+
Debug.Assert(Score == new Evaluation.Eval(this).Score);
96102
}
97103
}
98104

@@ -146,8 +152,11 @@ public void SetupPosition(string fen)
146152
//Set en-passant square
147153
_enPassantSquare = fields[3] == "-" ? -1 : Notation.ToSquare(fields[3]);
148154

155+
//Init incremental eval
156+
_eval = new Evaluation.Eval(this);
157+
149158
//Initialze Hash
150-
_zobristHash = GetZobristHash();
159+
InitZobristHash();
151160
}
152161

153162

@@ -666,18 +675,17 @@ public bool Equals(Board other)
666675
return true;
667676
}
668677

669-
public ulong GetZobristHash()
678+
private void InitZobristHash()
670679
{
671680
//Side to move
672-
ulong hash = Zobrist.SideToMove(_sideToMove);
681+
_zobristHash = Zobrist.SideToMove(_sideToMove);
673682
//Pieces
674683
for (int square = 0; square < 64; square++)
675-
hash ^= Zobrist.PieceSquare(_state[square], square);
684+
_zobristHash ^= Zobrist.PieceSquare(_state[square], square);
676685
//En passent
677-
hash ^= Zobrist.Castling(_castlingRights);
686+
_zobristHash ^= Zobrist.Castling(_castlingRights);
678687
//Castling
679-
hash ^= Zobrist.EnPassant(_enPassantSquare);
680-
return hash;
688+
_zobristHash ^= Zobrist.EnPassant(_enPassantSquare);
681689
}
682690
}
683691
}

MinimalChess/Evaluation.cs

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,64 @@ namespace MinimalChess
44
{
55
public class Evaluation
66
{
7-
const int CheckmateScore = 9999;
8-
9-
public static bool IsCheckmate(int score) => Math.Abs(score) == CheckmateScore;
7+
public struct Eval
8+
{
9+
public short MidgameScore;
10+
public short EndgameScore;
11+
public short Phase;
12+
public int Score
13+
{
14+
get
15+
{
16+
//linearily interpolate between midGame and endGame score based on current phase (tapered eval)
17+
double phase = Linstep(Midgame, Endgame, Phase);
18+
double score = MidgameScore + phase * (EndgameScore - MidgameScore);
19+
return (int)score;
20+
}
21+
}
1022

11-
public static int Checkmate(Color color) => (int)color * -CheckmateScore;
23+
public Eval(Board board) : this()
24+
{
25+
for (int i = 0; i < 64; i++)
26+
if (board[i] != Piece.None)
27+
AddPiece(board[i], i);
28+
}
1229

13-
public static int Evaluate(Board board)
14-
{
15-
int midGame = 0;
16-
int endGame = 0;
17-
int phaseValue = 0;
18-
for (int i = 0; i < 64; i++)
30+
public void Update(Piece oldPiece, Piece newPiece, int index)
1931
{
20-
Piece piece = board[i];
21-
if (piece == Piece.None)
22-
continue;
32+
if (oldPiece != Piece.None)
33+
RemovePiece(oldPiece, index);
34+
if (newPiece != Piece.None)
35+
AddPiece(newPiece, index);
36+
}
2337

24-
int sign = (int)piece.Color();
38+
private void AddPiece(Piece piece, int squareIndex)
39+
{
40+
short color = (short)piece.Color();
2541
int pieceIndex = PieceTableIndex(piece);
26-
int squareIndex = SquareTableIndex(i, piece);
27-
phaseValue += PhaseValues[pieceIndex];
28-
midGame += sign * MidgameTables[pieceIndex, squareIndex];
29-
endGame += sign * EndgameTables[pieceIndex, squareIndex];
42+
int tableIndex = SquareTableIndex(squareIndex, piece);
43+
Phase += PhaseValues[pieceIndex];
44+
MidgameScore += (short)(color * MidgameTables[pieceIndex, tableIndex]);
45+
EndgameScore += (short)(color * EndgameTables[pieceIndex, tableIndex]);
3046
}
3147

32-
//linearily interpolate between midGame and endGame score based on current phase (tapered eval)
33-
double phase = Linstep(Midgame, Endgame, phaseValue);
34-
double score = midGame + phase * (endGame - midGame);
35-
return (int)score;
48+
private void RemovePiece(Piece piece, int squareIndex)
49+
{
50+
int color = (int)piece.Color();
51+
int pieceIndex = PieceTableIndex(piece);
52+
int tableIndex = SquareTableIndex(squareIndex, piece);
53+
Phase -= PhaseValues[pieceIndex];
54+
MidgameScore -= (short)(color * MidgameTables[pieceIndex, tableIndex]);
55+
EndgameScore -= (short)(color * EndgameTables[pieceIndex, tableIndex]);
56+
}
3657
}
3758

59+
const int CheckmateScore = 9999;
60+
61+
public static bool IsCheckmate(int score) => Math.Abs(score) == CheckmateScore;
62+
63+
public static int Checkmate(Color color) => (int)color * -CheckmateScore;
64+
3865
public static double Linstep(double edge0, double edge1, double value)
3966
{
4067
return Math.Min(1, Math.Max(0, (value - edge0) / (edge1 - edge0)));
@@ -50,13 +77,13 @@ public static double Linstep(double edge0, double edge1, double value)
5077
MeanSquareError(k=102): 0.23835033815795936
5178
*/
5279

53-
const int Midgame = 5255;
54-
const int Endgame = 435;
80+
const short Midgame = 5255;
81+
const short Endgame = 435;
5582

5683
//Pawn = 0, Knight = 155, Bishop = 305; Rook = 405, Queen = 1050, King = 0
57-
static readonly int[] PhaseValues = new int[6] { 0, 155, 305, 405, 1050, 0 };
84+
static readonly short[] PhaseValues = new short[6] { 0, 155, 305, 405, 1050, 0 };
5885

59-
static readonly int[,] MidgameTables = new int[6, 64]{
86+
static readonly short[,] MidgameTables = new short[6, 64]{
6087
{ //PAWN MG
6188
100, 100, 100, 100, 100, 100, 100, 100,
6289
176, 214, 147, 194, 189, 214, 132, 77,
@@ -119,7 +146,7 @@ public static double Linstep(double edge0, double edge1, double value)
119146
}
120147
};
121148

122-
static readonly int[,] EndgameTables = new int[6, 64]{
149+
static readonly short[,] EndgameTables = new short[6, 64]{
123150
{ //PAWN EG
124151
100, 100, 100, 100, 100, 100, 100, 100,
125152
277, 270, 252, 229, 240, 233, 264, 285,
@@ -181,7 +208,7 @@ public static double Linstep(double edge0, double edge1, double value)
181208
-55, -40, -23, -6, -20, -8, -28, -47,
182209
}};
183210

184-
public static int[] MobilityValues = new int[13 * 6]
211+
public static short[] MobilityValues = new short[13 * 6]
185212
{
186213
// opponent friendly
187214
// - P N B R Q K P N B R Q K

MinimalChess/IterativeSearch.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace MinimalChess
66
public class IterativeSearch
77
{
88
const int QUERY_TC_FREQUENCY = 25;
9+
const int MAX_GAIN_PER_PLY = 70;
910

1011
public long NodesVisited { get; private set; }
1112
public int Depth { get; private set; }
@@ -100,8 +101,8 @@ private void StorePVinTT(Move[] pv, int depth)
100101
expandedNodes++;
101102

102103
//moves after the PV node are unlikely to raise alpha. skip those that appear clearly futile!
103-
int futilityMargin = (int)color * depth * 70;
104-
if (expandedNodes > 1 && depth <= 4 && !isChecked && !window.Inside(Evaluation.Evaluate(child) + futilityMargin, color) && !child.IsChecked(child.SideToMove))
104+
int futilityMargin = (int)color * depth * MAX_GAIN_PER_PLY;
105+
if (expandedNodes > 1 && depth <= 4 && !isChecked && !window.Inside(child.Score + futilityMargin, color) && !child.IsChecked(child.SideToMove))
105106
continue;
106107

107108
//moves after the PV node are unlikely to raise alpha. searching with a null-sized window can save a lot of nodes
@@ -159,7 +160,7 @@ private int QEval(Board position, SearchWindow window)
159160
//if inCheck we can't use standPat, need to escape check!
160161
if (!inCheck)
161162
{
162-
int standPatScore = Evaluation.DynamicScore + Evaluation.Evaluate(position);
163+
int standPatScore = Evaluation.DynamicScore + position.Score;
163164
//Cut will raise alpha and perform beta cutoff when standPatScore is too good
164165
if (window.Cut(standPatScore, color))
165166
return window.GetScore(color);

MinimalChessBoard/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private static void Print(Board board, Move move = default)
127127
Console.WriteLine($"|{rank + 1}"); //ranks aren't zero-indexed
128128
}
129129
Console.WriteLine(" '----------------'");
130-
int pstScore = Evaluation.Evaluate(board);
130+
int pstScore = board.Score;
131131
int mobScore = Evaluation.ComputeMobility(board);
132132
Console.WriteLine($" A B C D E F G H {(pstScore + mobScore):+0.00;-0.00} (PST:{pstScore:+0.00;-0.00}, Mobility:{mobScore})");
133133
}

MinimalChessEngine/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace MinimalChessEngine
77
{
88
public static class Program
99
{
10-
const string NAME_VERSION = "MinimalChess 0.5.8";
10+
const string NAME_VERSION = "MinimalChess 0.5.9";
1111

1212
static Engine _engine = new Engine();
1313
static async Task Main()

MinimalChessEngine/Properties/PublishProfiles/Windows x64.pubxml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
88
<Configuration>Release</Configuration>
99
<Platform>Any CPU</Platform>
1010
<TargetFramework>net5.0</TargetFramework>
11-
<PublishDir>D:\Projekte\Chess\Builds\MinimalChess 0.5.8 Windows</PublishDir>
11+
<PublishDir>D:\Projekte\Chess\Builds\MinimalChess 0.5.9 Windows</PublishDir>
1212
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
1313
<SelfContained>true</SelfContained>
1414
<PublishSingleFile>True</PublishSingleFile>

0 commit comments

Comments
 (0)