Skip to content

Commit

Permalink
feat: add null move reduction (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
znxftw authored Dec 26, 2024
1 parent 64914b7 commit 24879a7
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 26 deletions.
35 changes: 35 additions & 0 deletions Rudim.Test/UnitTest/Board/BoardStateMovesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Rudim.Test.UnitTest.Board
{
[Collection("StateRace")]
public class BoardStateMovesTest
{
[Fact]
Expand All @@ -23,5 +24,39 @@ public void ShouldGenerateMoves()
Assert.Equal(48, randomPosition.Moves.Count);
Assert.Equal(20, startingPosition.Moves.Count);
}

[Fact]
public void ShouldMakeAndUndoNullMoveCorrectly()
{
BoardState boardState = BoardState.Default();
ulong originalBoardHash = boardState.BoardHash;
Side originalSideToMove = boardState.SideToMove;
Square originalEnPassantSquare = boardState.EnPassantSquare;
Castle originalCastlingRights = boardState.Castle;
int originalMoveCount = boardState.MoveCount;


boardState.MakeNullMove();
Assert.NotEqual(originalBoardHash, boardState.BoardHash);
Assert.NotEqual(originalSideToMove, boardState.SideToMove);
Assert.Equal(Square.NoSquare, boardState.EnPassantSquare);

// Make one legal move for each side
Move blackMove = new(Square.e7, Square.e5, MoveTypes.DoublePush);
boardState.MakeMove(blackMove);
Move whiteMove = new(Square.e2, Square.e4, MoveTypes.DoublePush);
boardState.MakeMove(whiteMove);

boardState.UnmakeMove(whiteMove);
boardState.UnmakeMove(blackMove);

boardState.UndoNullMove();

Assert.Equal(originalBoardHash, boardState.BoardHash);
Assert.Equal(originalSideToMove, boardState.SideToMove);
Assert.Equal(originalEnPassantSquare, boardState.EnPassantSquare);
Assert.Equal(originalCastlingRights, boardState.Castle);
Assert.Equal(originalMoveCount, boardState.MoveCount);
}
}
}
18 changes: 15 additions & 3 deletions Rudim.Test/UnitTest/Board/TacticsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,34 @@ namespace Rudim.Test.UnitTest.Board
[Collection("StateRace")]
public class TacticsTest
{
// TODO : End goal is to have NO Skip here - these should eventually all be solvable once Rudim is strong enough
[Theory]

// Random Puzzle Position
[InlineData("r4r2/pb4kp/1p4p1/1P6/2P1pRp1/P3B3/7P/5RK1 w - - 0 29", "f4f8")]
// [InlineData("8/k7/3p4/p2P1p2/P2P1P2/8/8/K7 w - - ", "a1b1")] // Transposition Table Verification - without TT / wrong TT this would take too long

// Transposition Table Verification - without TT / wrong TT this would take too long
[InlineData("8/k7/3p4/p2P1p2/P2P1P2/8/8/K7 w - - ", "a1b1", Skip = "More Depth Needed")]

// Zugzwang Verification - NMR should not get wrong results for these nodes
[InlineData("8/8/1p1r1k2/p1pPN1p1/P3KnP1/1P6/8/3R4 b - - 0 1", "f4d5", Skip = "More Depth Needed")]
[InlineData("7k/5K2/5P1p/3p4/6P1/3p4/8/8 w - - 0 1", "g4g5", Skip = "Improve NMR")]
[InlineData("8/6B1/p5p1/Pp4kp/1P5r/5P1Q/4q1PK/8 w - - 0 32", "h3h4", Skip = "Improve NMR")]
[InlineData("8/8/p1p5/1p5p/1P5p/8/PPP2K1p/4R1rk w - - 0 1", "e1f1", Skip = "Improve NMR")]
[InlineData("1q1k4/2Rr4/8/2Q3K1/8/8/8/8 w - - 0 1", "g5h6", Skip = "Improve NMR")]
public void ShouldNotMissBestMoveForTactic(string fen, string moveLan)
{
Global.Reset();
BoardState boardState = BoardState.ParseFEN(fen);

CancellationTokenSource cancellationToken = new(2000);
bool debugMode = false;
Move bestMove = boardState.FindBestMove(25, cancellationToken.Token, ref debugMode);

Move expectedMove = Move.ParseLongAlgebraic(moveLan);
boardState.GenerateMoves();
expectedMove = Helpers.FindMoveFromMoveList(boardState, expectedMove);

Assert.Equal(expectedMove, bestMove);
}
}
Expand Down
8 changes: 4 additions & 4 deletions Rudim.Test/UnitTest/Board/TraversalTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ public class TraversalTest
// This helps keep track if certain optimizations are good enough to make up for the extra time spent
// Compare time spent with and without the change before updating the keys
[Theory]
[InlineData(Helpers.StartingFEN, 2938703, 7, 8)]
[InlineData(Helpers.EndgameFEN, 525630, 36, 9)]
[InlineData(Helpers.AdvancedMoveFEN, 8610756, 1750, 8)]
[InlineData(Helpers.KiwiPeteFEN, 25369977, -42, 8)]
[InlineData(Helpers.StartingFEN, 405929, 7, 8)]
[InlineData(Helpers.EndgameFEN, 141090, 36, 9)]
[InlineData(Helpers.AdvancedMoveFEN, 2051222, 1750, 8)]
[InlineData(Helpers.KiwiPeteFEN, 6064990, -42, 8)]
public void ShouldTraverseDeterministically(string position, int expectedNodes, int expectedScore, int depth)
{
Global.Reset();
Expand Down
17 changes: 17 additions & 0 deletions Rudim/Board/BoardState.Moves.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,5 +309,22 @@ private bool IsSquareCapture(int target)
{
return Occupancies[(int)SideToMove.Other()].GetBit(target) == 1;
}

public void MakeNullMove()
{
History.SaveBoardHistory(Piece.None, EnPassantSquare, Castle, BoardHash, LastDrawKiller);
UpdateEnPassant(Move.NoMove);
FlipSideToMove();
}

public void UndoNullMove()
{
History.BoardHistory history = History.RestoreBoardHistory();
FlipSideToMove();
LastDrawKiller = history.LastDrawKiller;
BoardHash = history.BoardHash;
Castle = history.CastlingRights;
EnPassantSquare = history.EnPassantSquare;
}
}
}
9 changes: 3 additions & 6 deletions Rudim/Common/GamePhase.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
using Rudim.Board;
using System;
using System.Collections.Generic;
using System.Numerics;

namespace Rudim.Common
namespace Rudim.Common
{
public static class GamePhase
{
private static readonly int[] PieceConstants;
public static readonly int TotalPhase;
public static readonly double PhaseFactor;
public static readonly int OnlyPawns;

static GamePhase()
{
PieceConstants = [0, 1, 1, 2, 4, 0];
TotalPhase = PieceConstants[(int)Piece.Pawn] * 16 + PieceConstants[(int)Piece.Knight] * 4 + PieceConstants[(int)Piece.Bishop] * 4 + PieceConstants[(int)Piece.Rook] * 4 + PieceConstants[(int)Piece.Queen] * 2;
OnlyPawns = PieceConstants[(int)Piece.Pawn] * 16;
PhaseFactor = 1 / (double)TotalPhase;
}

Expand Down
12 changes: 9 additions & 3 deletions Rudim/Search/Negamax.Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ namespace Rudim.Search
public static partial class Negamax
{
private static int PrincipalVariationSearch(BoardState boardState, int depth, int alpha, int beta,
CancellationToken cancellationToken)
bool allowNullMove, CancellationToken cancellationToken)
{
int score = -Search(boardState, depth - 1, -alpha - 1, -alpha, cancellationToken);
int score = -Search(boardState, depth - 1, -alpha - 1, -alpha, allowNullMove, cancellationToken);
if (score > alpha && score < beta)
score = -Search(boardState, depth - 1, -beta, -alpha, cancellationToken);
score = -Search(boardState, depth - 1, -beta, -alpha, allowNullMove, cancellationToken);
return score;
}

Expand Down Expand Up @@ -50,5 +50,11 @@ private static void PopulateMoveScores(BoardState boardState, int ply)
}
}
}

private static bool CanPruneNullMove(bool isPvNode, BoardState boardState, bool allowNullMove, int depth)
{
return allowNullMove && !isPvNode && !boardState.IsInCheck(boardState.SideToMove) && depth >= 2 &&
boardState.Phase > GamePhase.OnlyPawns;
}
}
}
30 changes: 20 additions & 10 deletions Rudim/Search/Negamax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static int Search(BoardState boardState, int depth, CancellationToken can
Nodes = 0;
BestMove = Move.NoMove;
Quiescence.ResetNodes();
int score = Search(boardState, depth, int.MinValue + 1, int.MaxValue - 1, cancellationToken);
int score = Search(boardState, depth, int.MinValue + 1, int.MaxValue - 1, true, cancellationToken);
if (BestMove == Move.NoMove)
{
boardState.GenerateMoves();
Expand All @@ -26,23 +26,33 @@ public static int Search(BoardState boardState, int depth, CancellationToken can
return score;
}

private static int Search(BoardState boardState, int depth, int alpha, int beta, CancellationToken cancellationToken)
private static int Search(BoardState boardState, int depth, int alpha, int beta, bool allowNullMove,CancellationToken cancellationToken)
{
int ply = _searchDepth - depth;
bool isPvNode = beta - alpha > 1;
Nodes++;

if (boardState.IsDraw())
return 0;

(bool hasValue, int ttScore, Move bestEvaluation) = TranspositionTable.GetEntry(boardState.BoardHash, alpha, beta, depth);
if (hasValue)
{
BestMove = bestEvaluation;
return TranspositionTable.RetrieveScore(ttScore, ply);
}

if (boardState.IsDraw())
return 0;

if (depth == 0)
if (depth <= 0)
return Quiescence.Search(boardState, alpha, beta, cancellationToken);

if (CanPruneNullMove(isPvNode, boardState, allowNullMove, depth))
{
boardState.MakeNullMove();
int score = -Search(boardState, depth - 1 - 2, -beta, -beta + 1, false, cancellationToken);
boardState.UndoNullMove();
if (score >= beta)
return score;
}

int originalAlpha = alpha;
bool foundPv = false;
Expand All @@ -64,7 +74,7 @@ private static int Search(BoardState boardState, int depth, int alpha, int beta,
continue;
}

int score = SearchDeeper(boardState, depth, alpha, beta, cancellationToken, foundPv);
int score = SearchDeeper(boardState, depth, alpha, beta, cancellationToken, foundPv, allowNullMove);

numberOfLegalMoves++;

Expand Down Expand Up @@ -95,16 +105,16 @@ private static int Search(BoardState boardState, int depth, int alpha, int beta,
}

private static int SearchDeeper(BoardState boardState, int depth, int alpha, int beta,
CancellationToken cancellationToken, bool foundPv)
CancellationToken cancellationToken, bool foundPv, bool allowNullMove)
{
int score;
if (foundPv)
{
score = PrincipalVariationSearch(boardState, depth, alpha, beta, cancellationToken);
score = PrincipalVariationSearch(boardState, depth, alpha, beta, allowNullMove, cancellationToken);
}
else
{
score = -Search(boardState, depth - 1, -beta, -alpha, cancellationToken);
score = -Search(boardState, depth - 1, -beta, -alpha, allowNullMove, cancellationToken);
}
return score;
}
Expand Down

1 comment on commit 24879a7

@znxftw
Copy link
Owner Author

@znxftw znxftw commented on 24879a7 Dec 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction in commit message : this is Null Move Pruning, not Null Move Reductions

Please sign in to comment.