Skip to content

Commit e695270

Browse files
committed
Added history heuristic and a simple late move reduction. Many variants are possible. Currently any move that raises alpha (and not only beta cutoffs) are remembered as good moves and the reduction is only applied during the null-window search that proofs a node is below alpha. Research happens at full depth.
1 parent a16900b commit e695270

File tree

10 files changed

+310
-12
lines changed

10 files changed

+310
-12
lines changed

MinimalChess/History.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace MinimalChess
8+
{
9+
public static class History
10+
{
11+
private const int Squares = 64;
12+
private const int Pieces = 12;
13+
private static int[,] Positive = new int[Squares, Pieces];
14+
private static int[,] Negative = new int[Squares, Pieces];
15+
16+
public static int Max
17+
{
18+
get
19+
{
20+
int max = 0;
21+
for (int square = 0; square < Squares; square++)
22+
for (int piece = 0; piece < Pieces; piece++)
23+
{
24+
max = Math.Max(max, Positive[square, piece]);
25+
max = Math.Max(max, Negative[square, piece]);
26+
}
27+
28+
return max;
29+
}
30+
}
31+
32+
public static void Clear()
33+
{
34+
for (int square = 0; square < Squares; square++)
35+
for (int piece = 0; piece < Pieces; piece++)
36+
{
37+
Positive[square, piece] = 0;
38+
Negative[square, piece] = 0;
39+
}
40+
}
41+
42+
43+
public static void Shrink()
44+
{
45+
for (int square = 0; square < Squares; square++)
46+
for(int piece = 0; piece < Pieces; piece++)
47+
{
48+
Positive[square, piece] /= 2;
49+
Negative[square, piece] /= 2;
50+
}
51+
}
52+
53+
public static void Good(Piece piece, int square, int depth)
54+
{
55+
int iPiece = ((byte)piece >> 1) - 2; //BlackPawn = 0...
56+
Positive[square, iPiece] += depth * depth;
57+
}
58+
59+
public static void Bad(Piece piece, int square, int depth)
60+
{
61+
int iPiece = ((byte)piece >> 1) - 2; //BlackPawn = 0...
62+
Negative[square, iPiece] += depth * depth;
63+
}
64+
65+
public static float Value(Piece piece, int square)
66+
{
67+
int iPiece = ((byte)piece >> 1) - 2; //BlackPawn = 0...
68+
float a = Positive[square, iPiece] + 1;
69+
float b = Negative[square, iPiece] + 2;
70+
return a / b;
71+
}
72+
}
73+
}

MinimalChess/IterativeSearch.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public IterativeSearch(Board board, long maxNodes = long.MaxValue)
2525
_root = new Board(board);
2626
_killers = new KillerMoves(4);
2727
_maxNodes = maxNodes;
28+
History.Clear();
2829
}
2930

3031
public IterativeSearch(int searchDepth, Board board) : this(board)
@@ -40,6 +41,7 @@ public void SearchDeeper(Func<bool> killSwitch = null)
4041

4142
Depth++;
4243
_killers.Resize(Depth);
44+
History.Shrink();
4345
StorePVinTT(PrincipalVariation, Depth);
4446
_killSwitch = new KillSwitch(killSwitch);
4547
(Score, PrincipalVariation) = EvalPosition(_root, Depth, SearchWindow.Infinite);
@@ -79,6 +81,8 @@ private void StorePVinTT(Move[] pv, int depth)
7981

8082
Color color = position.SideToMove;
8183
bool isChecked = position.IsChecked(color);
84+
int futilityMargin = (int)color * depth * MAX_GAIN_PER_PLY;
85+
8286
//should we try null move pruning?
8387
if (depth >= 2 && !isChecked)
8488
{
@@ -101,25 +105,30 @@ private void StorePVinTT(Move[] pv, int depth)
101105
expandedNodes++;
102106

103107
//moves after the PV node are unlikely to raise alpha. skip those that appear clearly futile!
104-
int futilityMargin = (int)color * depth * MAX_GAIN_PER_PLY;
105-
if (expandedNodes > 1 && depth <= 4 && !isChecked && window.FailLow(child.Score + futilityMargin, color) && !child.IsChecked(child.SideToMove))
108+
bool reduce = expandedNodes > 1 && !isChecked && (move.Promotion < Piece.Queen) && !child.IsChecked(child.SideToMove);
109+
if (reduce && depth <= 4 && window.FailLow(child.Score + futilityMargin, color))
106110
continue;
107111

108112
//moves after the PV node are unlikely to raise alpha. searching with a null-sized window can save a lot of nodes
109113
if (expandedNodes > 1 && depth >= 3)
110114
{
111115
//we can save a lot of nodes by searching with "null window" first, proving cheaply that the score is below alpha...
116+
int R = reduce && expandedNodes >= 4 ? 1 : 0;
112117
SearchWindow nullWindow = window.GetLowerBound(color);
113-
var nullResult = EvalPositionTT(child, depth - 1, nullWindow);
118+
var nullResult = EvalPositionTT(child, depth - 1 - R, nullWindow);
114119
if (nullWindow.FailLow(nullResult.Score, color))
115120
continue;
116121
}
117122

118123
//this node may raise alpha!
119124
var eval = EvalPositionTT(child, depth - 1, window);
120125
if (window.FailLow(eval.Score, color))
126+
{
127+
History.Bad(position[move.FromSquare], move.ToSquare, depth);
121128
continue;
129+
}
122130

131+
History.Good(position[move.FromSquare], move.ToSquare, depth);
123132
Transpositions.Store(position.ZobristHash, depth, window, eval.Score, move);
124133
//store the PV beginning with move, followed by the PV of the childnode
125134
pv = Merge(move, eval.PV);

MinimalChess/MoveCollection.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,14 @@ int Score(Move move)
6363
}
6464
Sort((a, b) => Score(b).CompareTo(Score(a)));
6565
}
66+
67+
public void SortHistory(Board context)
68+
{
69+
float Score(Move move)
70+
{
71+
return History.Value(context[move.FromSquare], move.ToSquare);
72+
}
73+
Sort((a, b) => Score(b).CompareTo(Score(a)));
74+
}
6675
}
6776
}

MinimalChess/Playmaker.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23

34
namespace MinimalChess
45
{
@@ -32,7 +33,10 @@ public static class Playmaker
3233
}
3334

3435
//4. Play quiet moves that aren't known killers
35-
foreach (var move in MoveList.Quiets(position))
36+
var quiets = MoveList.Quiets(position);
37+
if(depth >= 3)
38+
quiets.SortHistory(position);
39+
foreach (var move in quiets)
3640
if (!killers.Contains(depth, move))
3741
{
3842
var nextPosition = new Board(position, move);

MinimalChess/SearchWindow.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
{
33
public struct SearchWindow
44
{
5-
public static SearchWindow Infinite = new SearchWindow(short.MinValue, short.MaxValue);
5+
public static SearchWindow Infinite = new(short.MinValue, short.MaxValue);
66

77
public int Floor;//Alpha
88
public int Ceiling;//Beta
99

10-
public SearchWindow UpperBound => new SearchWindow(Ceiling - 1, Ceiling);
11-
public SearchWindow LowerBound => new SearchWindow(Floor, Floor + 1);
10+
public SearchWindow UpperBound => new(Ceiling - 1, Ceiling);
11+
public SearchWindow LowerBound => new(Floor, Floor + 1);
1212
//used to quickly determine that a move is not improving the score for color.
1313
public SearchWindow GetLowerBound(Color color) => color == Color.White ? LowerBound : UpperBound;
1414
//used to quickly determine that a move is too good and will not be allowed by the opponent .

MinimalChessBoard/MinimalChessBoard.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
<None Update="wac-revised.epd">
2828
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2929
</None>
30+
<None Update="wac.epd">
31+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
32+
</None>
3033
<None Update="zugzwang.epd">
3134
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3235
</None>

0 commit comments

Comments
 (0)