Skip to content

Commit e175da0

Browse files
committed
Version 0.6.1 improves mate finding capabilities: Now encoding the mate distance into the checkmate score instead of counting moves in the PV. Shortest mates are preferred over late ones by search. Search doesn't terminate automatically when a mate is found. When the last iteration resulted in a mate the use of null move pruning is vastly reduced. This allows MMC to find the best move (shortest mate) in many more mate puzzles.
1 parent 078826f commit e175da0

File tree

9 files changed

+125
-73
lines changed

9 files changed

+125
-73
lines changed

MinimalChess/Evaluation.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,19 @@ private void RemovePiece(Piece piece, int squareIndex)
5656
}
5757
}
5858

59+
const int CheckmateBase = 9000;
5960
const int CheckmateScore = 9999;
6061

61-
public static bool IsCheckmate(int score) => Math.Abs(score) == CheckmateScore;
62+
public static int GetMateDistance(int score)
63+
{
64+
int plies = CheckmateScore - Math.Abs(score);
65+
int moves = (plies + 1) / 2;
66+
return moves;
67+
}
68+
69+
public static bool IsCheckmate(int score) => Math.Abs(score) > CheckmateBase;
6270

63-
public static int Checkmate(Color color) => (int)color * -CheckmateScore;
71+
public static int Checkmate(Color color, int ply) => (int)color * (ply - CheckmateScore);
6472

6573
public static double Linstep(double edge0, double edge1, double value)
6674
{

MinimalChess/IterativeSearch.cs

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public IterativeSearch(Board board, long maxNodes = long.MaxValue)
3232

3333
public IterativeSearch(int searchDepth, Board board) : this(board)
3434
{
35-
while (!GameOver && Depth < searchDepth)
35+
while (Depth < searchDepth)
3636
SearchDeeper();
3737
}
3838

@@ -43,35 +43,36 @@ public void SearchDeeper(Func<bool> killSwitch = null)
4343
_history.Scale();
4444
StorePVinTT(PrincipalVariation, Depth);
4545
_killSwitch = new KillSwitch(killSwitch);
46-
(Score, PrincipalVariation) = EvalPosition(_root, Depth, SearchWindow.Infinite);
46+
(Score, PrincipalVariation) = EvalPosition(_root, 0, Depth, SearchWindow.Infinite);
4747
}
4848

4949
private void StorePVinTT(Move[] pv, int depth)
5050
{
5151
Board position = new Board(_root);
52-
foreach (Move move in pv)
52+
for (int ply = 0; ply < pv.Length; ply++)
5353
{
54-
Transpositions.Store(position.ZobristHash, --depth, SearchWindow.Infinite, Score, move);
54+
Move move = pv[ply];
55+
Transpositions.Store(position.ZobristHash, --depth, ply, SearchWindow.Infinite, Score, move);
5556
position.Play(move);
5657
}
5758
}
5859

59-
private (int Score, Move[] PV) EvalPositionTT(Board position, int depth, SearchWindow window)
60+
private (int Score, Move[] PV) EvalPositionTT(Board position, int ply, int depth, SearchWindow window)
6061
{
61-
if (Transpositions.GetScore(position.ZobristHash, depth, window, out int ttScore))
62+
if (Transpositions.GetScore(position.ZobristHash, depth, ply, window, out int ttScore))
6263
return (ttScore, Array.Empty<Move>());
6364

64-
var result = EvalPosition(position, depth, window);
65-
Transpositions.Store(position.ZobristHash, depth, window, result.Score, default);
65+
var result = EvalPosition(position, ply, depth, window);
66+
Transpositions.Store(position.ZobristHash, depth, ply, window, result.Score, result.PV.Length > 0 ? result.PV[0] : default);
6667
return result;
6768
}
6869

69-
private (int Score, Move[] PV) EvalPosition(Board position, int depth, SearchWindow window)
70+
private (int Score, Move[] PV) EvalPosition(Board position, int ply, int depth, SearchWindow window)
7071
{
7172
if (depth <= 0)
7273
{
7374
_mobilityBonus = Evaluation.ComputeMobility(position);
74-
return (QEval(position, window), Array.Empty<Move>());
75+
return (QEval(position, ply, window), Array.Empty<Move>());
7576
}
7677

7778
NodesVisited++;
@@ -80,16 +81,20 @@ private void StorePVinTT(Move[] pv, int depth)
8081

8182
Color color = position.SideToMove;
8283
bool isChecked = position.IsChecked(color);
84+
//if the previous iteration found a mate we check the first few plys without null move to try and find the shortest mate or escape
85+
bool allowNullMove = Evaluation.IsCheckmate(Score) ? (ply > Depth/4) : true;
8386

84-
//should we try null move pruning?
85-
if (depth >= 2 && !isChecked)
87+
//should we try null move pruning?
88+
if (allowNullMove && depth >= 2 && !isChecked && window.CanFailHigh(color))
8689
{
8790
const int R = 2;
91+
//evaluate the position at reduced depth with a null-window around beta
92+
SearchWindow beta = window.GetUpperBound(color);
8893
//skip making a move
8994
Board nullChild = Playmaker.PlayNullMove(position);
90-
//evaluate the position at reduced depth with a null-window around beta
91-
(int score, _) = EvalPositionTT(nullChild, depth - R - 1, window.GetUpperBound(color));
95+
(int score, _) = EvalPositionTT(nullChild, ply + 1, depth - R - 1, beta);
9296
//is the evaluation "too good" despite null-move? then don't waste time on a branch that is likely going to fail-high
97+
//if the static eval look much worse the alpha also skip it
9398
if (window.FailHigh(score, color))
9499
return (score, Array.Empty<Move>());
95100
}
@@ -100,41 +105,39 @@ private void StorePVinTT(Move[] pv, int depth)
100105
foreach ((Move move, Board child) in Playmaker.Play(position, depth, _killers, _history))
101106
{
102107
expandedNodes++;
108+
bool interesting = expandedNodes == 1 || isChecked || child.IsChecked(child.SideToMove);
103109

104-
//moves after the PV node are unlikely to raise alpha. try to avoid a full evaluation!
105-
if(expandedNodes > 1)
110+
//some near the leaves that appear hopeless can be skipped without evaluation
111+
if (depth <= 4 && !interesting)
106112
{
107-
bool tactical = isChecked || child.IsChecked(child.SideToMove);
108-
109-
//some moves are hopeless and can be skipped without deeper evaluation
110-
if (depth <= 4 && !tactical)
111-
{
112-
int futilityMargin = (int)color * depth * MAX_GAIN_PER_PLY;
113-
if (window.FailLow(child.Score + futilityMargin, color))
114-
continue;
115-
}
116-
117-
//other moves are searched with a null-sized window and skipped if they don't raise alpha
118-
if (depth >= 2)
119-
{
120-
//non-tactical late moves are searched at a reduced depth to make this test even faster!
121-
int R = (tactical || expandedNodes < 4) ? 0 : 2;
122-
(int score, _) = EvalPositionTT(child, depth - R - 1, window.GetLowerBound(color));
123-
if (window.FailLow(score, color))
124-
continue;
125-
}
113+
//if the static eval look much worse the alpha also skip it
114+
int futilityMargin = (int)color * depth * MAX_GAIN_PER_PLY;
115+
if (window.FailLow(child.Score + futilityMargin, color))
116+
continue;
117+
}
118+
119+
//moves after the PV node are unlikely to raise alpha.
120+
//avoid a full evaluation by searching with a null-sized window around alpha first
121+
//...we expect it to fail low but if it does not we have to research it!
122+
if (depth >= 2 && expandedNodes > 1)
123+
{
124+
//non-tactical late moves are searched at a reduced depth to make this test even faster!
125+
int R = (interesting || expandedNodes < 4) ? 0 : 2;
126+
(int score, _) = EvalPositionTT(child, ply + 1, depth - R - 1, window.GetLowerBound(color));
127+
if (window.FailLow(score, color))
128+
continue;
126129
}
127130

128-
//this move is expected to raise alpha so we search at full depth!
129-
var eval = EvalPositionTT(child, depth - 1, window);
131+
//this move is expected to raise alpha so we search it at full depth!
132+
var eval = EvalPositionTT(child, ply + 1, depth - 1, window);
130133
if (window.FailLow(eval.Score, color))
131134
{
132135
_history.Bad(position, move, depth);
133136
continue;
134137
}
135138

136139
//the position has a new best move and score!
137-
Transpositions.Store(position.ZobristHash, depth, window, eval.Score, move);
140+
Transpositions.Store(position.ZobristHash, depth, ply, window, eval.Score, move);
138141
//set the PV to this move, followed by the PV of the childnode
139142
pv = Merge(move, eval.PV);
140143
//...and maybe we even get a beta cutoff
@@ -147,15 +150,21 @@ private void StorePVinTT(Move[] pv, int depth)
147150
_killers.Add(move, depth);
148151
}
149152

150-
return (window.GetScore(color), pv);
153+
return (GetScore(window, color), pv);
151154
}
152155
}
153156

154157
//checkmate or draw?
155158
if (expandedNodes == 0)
156-
return (position.IsChecked(color) ? Evaluation.Checkmate(color) : 0, Array.Empty<Move>());
159+
return (position.IsChecked(color) ? Evaluation.Checkmate(color, ply) : 0, Array.Empty<Move>());
157160

158-
return (window.GetScore(color), pv);
161+
return (GetScore(window, color), pv);
162+
}
163+
164+
private static int GetScore(SearchWindow window, Color color)
165+
{
166+
int score = window.GetScore(color);
167+
return score;
159168
}
160169

161170
private static Move[] Merge(Move move, Move[] pv)
@@ -166,7 +175,7 @@ private static Move[] Merge(Move move, Move[] pv)
166175
return result;
167176
}
168177

169-
private int QEval(Board position, SearchWindow window)
178+
private int QEval(Board position, int ply, SearchWindow window)
170179
{
171180
NodesVisited++;
172181
if (Aborted)
@@ -180,7 +189,7 @@ private int QEval(Board position, SearchWindow window)
180189
int standPatScore = position.Score + _mobilityBonus;
181190
//Cut will raise alpha and perform beta cutoff when standPatScore is too good
182191
if (window.Cut(standPatScore, color))
183-
return window.GetScore(color);
192+
return GetScore(window, color);
184193
}
185194

186195
int expandedNodes = 0;
@@ -189,7 +198,7 @@ private int QEval(Board position, SearchWindow window)
189198
{
190199
expandedNodes++;
191200
//recursively evaluate the resulting position (after the capture) with QEval
192-
int score = QEval(child, window);
201+
int score = QEval(child, ply + 1, window);
193202

194203
//Cut will raise alpha and perform beta cutoff when the move is too good
195204
if (window.Cut(score, color))
@@ -198,14 +207,14 @@ private int QEval(Board position, SearchWindow window)
198207

199208
//checkmate?
200209
if (expandedNodes == 0 && inCheck)
201-
return Evaluation.Checkmate(color);
210+
return Evaluation.Checkmate(color, ply);
202211

203212
//stalemate?
204213
if (expandedNodes == 0 && !LegalMoves.HasMoves(position))
205214
return 0;
206215

207216
//can't capture. We return the 'alpha' which may have been raised by "stand pat"
208-
return window.GetScore(color);
217+
return GetScore(window, color);
209218
}
210219
}
211220
}

MinimalChess/SearchWindow.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace MinimalChess
1+
using System;
2+
3+
namespace MinimalChess
24
{
35
public struct SearchWindow
46
{
@@ -45,5 +47,7 @@ public bool Cut(int score, Color color)
4547
public bool FailHigh(int score, Color color) => color == Color.White ? (score >= Ceiling) : (score <= Floor);
4648

4749
public int GetScore(Color color) => color == Color.White ? Floor : Ceiling;
50+
51+
public bool CanFailHigh(Color color) => color == Color.White ? (Ceiling < short.MaxValue) : (Floor > short.MinValue);
4852
}
4953
}

MinimalChess/Transpositions.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public static void Clear()
7474
Array.Clear(_table, 0, _table.Length);
7575
}
7676

77-
public static void Store(ulong zobristHash, int depth, SearchWindow window, int score, Move bestMove)
77+
public static void Store(ulong zobristHash, int depth, int ply, SearchWindow window, int score, Move bestMove)
7878
{
7979
ref HashEntry entry = ref _table[Index(zobristHash)];
8080

@@ -86,6 +86,12 @@ public static void Store(ulong zobristHash, int depth, SearchWindow window, int
8686
entry.Depth = depth < 0 ? default : (byte)depth;
8787
entry.Age = 0;
8888

89+
//a checkmate score is reduced by the number of plies from the root so that shorter mates are preferred
90+
//but when we talk about a position being 'mate in X' then X is independent of the root distance. So we store
91+
//the score relative to the position by adding the current ply to the encoded mate distance (from the root).
92+
if (Evaluation.IsCheckmate(score))
93+
score += Math.Sign(score) * ply;
94+
8995
if (score >= window.Ceiling)
9096
{
9197
entry.Type = ScoreType.GreaterOrEqual;
@@ -109,7 +115,7 @@ public static bool GetBestMove(Board position, out Move bestMove)
109115
return bestMove != default;
110116
}
111117

112-
public static bool GetScore(ulong zobristHash, int depth, SearchWindow window, out int score)
118+
public static bool GetScore(ulong zobristHash, int depth, int ply, SearchWindow window, out int score)
113119
{
114120
score = 0;
115121
if (!Index(zobristHash, out int index))
@@ -120,6 +126,13 @@ public static bool GetScore(ulong zobristHash, int depth, SearchWindow window, o
120126
return false;
121127

122128
score = entry.Score;
129+
130+
//a checkmate score is reduced by the number of plies from the root so that shorter mates are preferred
131+
//but when we store it in the TT the score is made relative to the current position. So when we want to
132+
//retrieve the score we have to subtract the current ply to make it relative to the root again.
133+
if (Evaluation.IsCheckmate(score))
134+
score -= Math.Sign(score) * ply;
135+
123136
//1.) score is exact and within window
124137
if (entry.Type == ScoreType.Exact)
125138
return true;

MinimalChessBoard/MinimalChessBoard.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
</ItemGroup>
1919

2020
<ItemGroup>
21+
<None Update="arves-mates.epd">
22+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
23+
</None>
2124
<None Update="ECM.epd">
2225
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2326
</None>

MinimalChessEngine/Engine.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ internal void Play(Move move)
5454
//*** Search ***
5555
//**************
5656

57-
internal void Go(int maxDepth, int maxTime, int maxNodes)
57+
internal void Go(int maxDepth, int maxTime, long maxNodes)
5858
{
5959
Stop();
6060
_time.Go(maxTime);
6161
StartSearch(maxDepth, maxNodes);
6262
}
6363

64-
internal void Go(int maxTime, int increment, int movesToGo, int maxDepth, int maxNodes)
64+
internal void Go(int maxTime, int increment, int movesToGo, int maxDepth, long maxNodes)
6565
{
6666
Stop();
6767
_time.Go(maxTime, increment, movesToGo);
@@ -83,14 +83,14 @@ public void Stop()
8383
//*** INTERNALS ***
8484
//*****************
8585

86-
private void StartSearch(int maxDepth, int maxNodes)
86+
private void StartSearch(int maxDepth, long maxNodes)
8787
{
8888
//do the first iteration. it's cheap, no time check, no thread
8989
Uci.Log($"Search scheduled to take {_time.TimePerMoveWithMargin}ms!");
9090

9191
//add all history positions with a score of 0 (Draw through 3-fold repetition) and freeze them by setting a depth that is never going to be overwritten
9292
foreach (var position in _history)
93-
Transpositions.Store(position.ZobristHash, Transpositions.HISTORY, SearchWindow.Infinite, 0, default);
93+
Transpositions.Store(position.ZobristHash, Transpositions.HISTORY, 0, SearchWindow.Infinite, 0, default);
9494

9595
_search = new IterativeSearch(_board, maxNodes);
9696
_time.StartInterval();
@@ -125,7 +125,7 @@ private void Search()
125125
private bool CanSearchDeeper()
126126
{
127127
//max depth reached or game over?
128-
if (_search.Depth >= _maxSearchDepth || _search.GameOver)
128+
if (_search.Depth >= _maxSearchDepth)
129129
return false;
130130

131131
//otherwise it's only time that can stop us!

0 commit comments

Comments
 (0)