Skip to content

Commit aa570b3

Browse files
committed
Added ClearChunk to TT that allows to delete a rolling sub-sectin of the TT.
Root moves are added to the TT with a special depth (9999) that protects them against being overwritten and allows them to overwrite history moves (9998) so that the PV extraction always includes at least one move. ExtractPV from TT is now more sophisticated and stops on a repetition and sets a boolean flag so that the search can handle the upcoming 3-fold-repetition. Reduced the security margin from 20ms to 15ms in the TimeControl and added a dynamic compoment based on the Sqrt() of the remaining time. Bumped version to 0.5.2
1 parent 24d774f commit aa570b3

File tree

8 files changed

+67
-37
lines changed

8 files changed

+67
-37
lines changed

MinimalChess/IterativeSearch.cs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
44

55
namespace MinimalChess
66
{
7-
87
public class IterativeSearch
98
{
109
const int QUERY_TC_FREQUENCY = 25;
1110

1211
public long NodesVisited { get; private set; }
1312
public int Depth { get; private set; }
1413
public int Score { get; private set; }
15-
public Move[] PrincipalVariation => Transpositions.ExtractPV(new Board(_root), new List<Move>());
14+
public bool GameOver { get; private set; }
15+
public Move[] PrincipalVariation { get; private set; }
1616
public bool Aborted => NodesVisited >= _maxNodes || _killSwitch.Get(NodesVisited % QUERY_TC_FREQUENCY == 0);
17-
public bool GameOver => Evaluation.IsCheckmate(Score);
1817

1918
Board _root = null;
2019
KillerMoves _killers;
@@ -28,21 +27,28 @@ public IterativeSearch(Board board, long maxNodes = long.MaxValue)
2827
_maxNodes = maxNodes;
2928
}
3029

31-
public void Search(int maxDepth)
30+
public IterativeSearch(int searchDepth, Board board) : this(board)
3231
{
33-
while (!GameOver && Depth < maxDepth)
32+
while (!GameOver && Depth < searchDepth)
3433
SearchDeeper();
3534
}
3635

3736
public void SearchDeeper(Func<bool> killSwitch = null)
3837
{
39-
if (GameOver)
38+
if (GameOver || Aborted)
4039
return;
4140

4241
Depth++;
43-
_killers.Grow(Depth);
42+
_killers.Resize(Depth);
4443
_killSwitch = new KillSwitch(killSwitch);
45-
Score = EvalPosition(_root, Depth, SearchWindow.Infinite);
44+
int score = EvalPosition(_root, Depth, SearchWindow.Infinite);
45+
46+
if (!Aborted)
47+
{
48+
PrincipalVariation = Transpositions.ExtractPV(_root, Depth, out bool isDraw);
49+
GameOver = Evaluation.IsCheckmate(score) || isDraw;
50+
Score = score;
51+
}
4652
}
4753

4854
private int EvalPositionTT(Board position, int depth, SearchWindow window, bool isNullMove = false)
@@ -102,7 +108,9 @@ private int EvalPosition(Board position, int depth, SearchWindow window, bool is
102108
int score = EvalPositionTT(child, depth - 1, window);
103109
if (window.Inside(score, color))
104110
{
105-
Transpositions.Store(position.ZobristHash, depth, window, score, move);
111+
//store move & score in TT. For root nodes a special depth is used to protect them against replacement (ROOT > HISTORY)
112+
int ttDepth = depth == Depth ? Transpositions.ROOT : depth;
113+
Transpositions.Store(position.ZobristHash, ttDepth, window, score, move);
106114
//...and maybe get a beta cutoff
107115
if (window.Cut(score, color))
108116
{

MinimalChess/KillerMoves.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public KillerMoves(int width)
1616
_width = width;
1717
}
1818

19-
public void Grow(int depth)
19+
public void Resize(int depth)
2020
{
2121
_depth = Math.Max(_depth, depth);
2222
Array.Resize(ref _moves, _depth * _width);

MinimalChess/Transpositions.cs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ public static class Transpositions
1212
public static int Count => _table.Length;
1313
public static int Empty => _table.Count(entry => entry.Hash == default);
1414

15-
1615
public enum ScoreType : byte
1716
{
1817
GreaterOrEqual,
@@ -31,20 +30,19 @@ public struct HashEntry
3130
// 16 Bytes
3231
}
3332

34-
public static short PERSISTENT = 9999;
33+
public const short ROOT = 9999;
34+
public const short HISTORY = 9998;
3535
public const int DEFAULT_SIZE_MB = 50;
3636
const int ENTRY_SIZE = 16; //BYTES
3737
static HashEntry[] _table;
3838

3939
static int Index(in ulong hash)
4040
{
4141
int i0 = (int)(hash % (ulong)_table.Length);
42-
return i0;
43-
4442
if (_table[i0].Hash == hash)
4543
return i0;
4644

47-
//try other entry in 'bucket' if not in first one
45+
//try other 'bucket' if not in first one
4846
int i1 = i0 ^ 1;
4947
if (_table[i1].Hash == hash)
5048
return i1;
@@ -69,16 +67,40 @@ public static void Clear()
6967
Array.Clear(_table, 0, _table.Length);
7068
}
7169

72-
public static Move[] ExtractPV(Board position, List<Move> pv)
70+
public static void ClearChunk(int counter, int count)
71+
{
72+
int chunk = counter % count;
73+
int stride = _table.Length / count; //a 'remainder' will never be cleared!
74+
Array.Clear(_table, chunk * stride, stride);
75+
}
76+
77+
public static Move[] ExtractPV(Board root, int depth, out bool repeatsHistory)
78+
{
79+
var pv = new List<Move>();
80+
repeatsHistory = ExtractPV(new Board(root), pv, depth);
81+
return pv.ToArray();
82+
}
83+
84+
public static bool ExtractPV(Board position, List<Move> pv, int depth)
7385
{
7486
ulong zobristHash = position.ZobristHash;
7587
ref HashEntry entry = ref _table[Index(zobristHash)];
76-
if (entry.Hash != zobristHash || entry.BestMove == default)
77-
return pv.Count > 0 ? pv.ToArray() : new Move[1] { default };
88+
89+
//Quit because entry is not about this position
90+
if (entry.Hash != zobristHash)
91+
return false;
92+
93+
//Quit because this position is flagged as a repetition
94+
if (entry.Depth == HISTORY)
95+
return true;
96+
97+
//Quit because the requested depth has been reached or no best move available
98+
if (depth == 0 || entry.BestMove == default)
99+
return false;
78100

79101
pv.Add(entry.BestMove);
80102
position.Play(entry.BestMove);
81-
return ExtractPV(position, pv);
103+
return ExtractPV(position, pv, --depth);
82104
}
83105

84106
public static void Store(ulong zobristHash, int depth, SearchWindow window, int score, Move bestMove)
@@ -90,9 +112,9 @@ public static void Store(ulong zobristHash, int depth, SearchWindow window, int
90112
if (entry.Hash != default && entry.Hash != zobristHash)
91113
HashOverwrites++;
92114

93-
//don't overwrite a bestmove unless it's a new position OR the new bestMove is explored to a greater depth
94-
if (entry.Hash != zobristHash || (depth >= entry.Depth && bestMove != default))
95-
_table[index].BestMove = bestMove;
115+
//don't overwrite a bestmove with 'default' unless it's a new position
116+
if (entry.Hash != zobristHash || bestMove != default)
117+
entry.BestMove = bestMove;
96118

97119
entry.Hash = zobristHash;
98120
entry.Depth = (short)Math.Max(0, depth);

MinimalChessBoard/Program.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ static void Main()
7272
else if (command == "!")
7373
{
7474
int depth = tokens.Length > 1 ? int.Parse(tokens[1]) : 4;
75-
IterativeSearch search = new IterativeSearch(board);
76-
search.Search(depth);
75+
IterativeSearch search = new IterativeSearch(depth, board);
7776
move = search.PrincipalVariation[0];
7877
Console.WriteLine($"{board.SideToMove} >> {move}");
7978
board.Play(move);
@@ -171,8 +170,7 @@ private static void SetColor(Piece piece, int rank, int file, Move move)
171170

172171
private static void ListMoves(Board board, int depth)
173172
{
174-
IterativeSearch search = new IterativeSearch(board);
175-
search.Search(depth);
173+
IterativeSearch search = new IterativeSearch(depth, board);
176174
Move[] line = search.PrincipalVariation;
177175

178176
int i = 1;
@@ -305,9 +303,8 @@ private static void CompareBestMove(int depth, string filePath, int maxCount)
305303
{
306304
ParseEpd(file.ReadLine(), out Board board, ref bestMoves);
307305
Transpositions.Clear();
308-
IterativeSearch search = new IterativeSearch(board);
309306
long t0 = Stopwatch.GetTimestamp();
310-
search.Search(depth);
307+
IterativeSearch search = new IterativeSearch(depth, board);
311308
long t1 = Stopwatch.GetTimestamp();
312309
long dt = t1 - t0;
313310
totalTime += dt;

MinimalChessEngine/Engine.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ private void StartSearch(int maxDepth, int maxNodes)
9292
//do the first iteration. it's cheap, no time check, no thread
9393
Uci.Log($"Search scheduled to take {_time.TimePerMoveWithMargin}ms!");
9494

95-
Transpositions.Clear();
95+
//clear a rolling quarter of the TT so that it doesn't get filled with high-depth but obsolete old positions
96+
Transpositions.ClearChunk(_history.Count, 8);
9697
//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
9798
foreach (var position in _history)
98-
Transpositions.Store(position.ZobristHash, Transpositions.PERSISTENT, SearchWindow.Infinite, 0, default);
99+
Transpositions.Store(position.ZobristHash, Transpositions.HISTORY, SearchWindow.Infinite, 0, default);
99100

100101
_search = new IterativeSearch(_board, maxNodes);
101102
_time.StartInterval();

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.1";
10+
const string NAME_VERSION = "MinimalChess 0.5.2";
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.1 Windows</PublishDir>
11+
<PublishDir>D:\Projekte\Chess\Builds\MinimalChess 0.5.2 Windows</PublishDir>
1212
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
1313
<SelfContained>true</SelfContained>
1414
<PublishSingleFile>True</PublishSingleFile>

MinimalChessEngine/TimeControl.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace MinimalChessEngine
55
{
66
class TimeControl
77
{
8-
const int TIME_MARGIN = 20;
8+
const int BASE_MARGIN = 20;
99
const int BRANCHING_FACTOR_ESTIMATE = 5;
1010
const int MAX_TIME_REMAINING = int.MaxValue / 3; //large but not too large to cause overlow issues
1111

@@ -15,13 +15,15 @@ class TimeControl
1515
private long _t0 = -1;
1616
private long _tN = -1;
1717

18-
public int TimePerMoveWithMargin => (_remaining + (_movesToGo - 1) * _increment) / _movesToGo - TIME_MARGIN;
19-
public int TimeRemainingWithMargin => _remaining - TIME_MARGIN;
20-
21-
private long Now => Stopwatch.GetTimestamp();
18+
public int TimePerMove => (_remaining + (_movesToGo - 1) * _increment) / _movesToGo;
19+
public int TimePerMoveWithMargin => WithMargin(TimePerMove);
20+
public int TimeRemainingWithMargin => WithMargin(_remaining);
2221
public int Elapsed => MilliSeconds(Now - _t0);
2322
public int ElapsedInterval => MilliSeconds(Now - _tN);
2423

24+
private long Now => Stopwatch.GetTimestamp();
25+
private int WithMargin(int time) => time - BASE_MARGIN - (int)Math.Sqrt(time);
26+
2527
private int MilliSeconds(long ticks)
2628
{
2729
double dt = ticks / (double)Stopwatch.Frequency;

0 commit comments

Comments
 (0)