Skip to content

Commit

Permalink
⚖ Pawnless endgames evaluation refactoring and tweaking (#693)
Browse files Browse the repository at this point in the history
* General refactoring
* ⚖ Scale down all 2vs1 minor pieces endgame (#689)
* ⚖ Scale down RX vs R endgames (#690)
  • Loading branch information
eduherminio authored Mar 16, 2024
1 parent b7240ec commit 53cf80a
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 9 deletions.
9 changes: 9 additions & 0 deletions src/Lynx/Bench.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,17 @@ public partial class Engine
"5R2/2k3PK/8/5N2/7P/5q2/8/q7 w - - 0 69", // McShane - Aronian 2012 - knight promotion
"rnbqk1nr/ppp2ppp/8/4P3/1BP5/8/PP2KpPP/RN1Q1BNR b kq - 1 7", // Albin Countergambit, Lasker Trap - knight promotion

"8/nRp5/8/8/8/7k/8/7K w - - 0 1", // R vs NP
"8/bRp5/8/8/8/7k/8/7K w - - 0 1", // R vs BP
"8/8/4k3/3n1n2/5P2/8/3K4/8 b - - 0 12", // NN vs P endgame
"8/bQr5/8/8/8/7k/8/7K w - - 0 1", // Q vs RB, leading to Q vs R or Q vs B
"8/nQr5/8/8/8/7k/8/7K w - - 0 1", // Q vs RN, leading to Q vs R or Q vs N
"4kq2/8/n7/8/8/3Q3b/8/3K4 w - - 0 1", // Q vs QBN, leading to Q vs QB or Q vs QN
"8/5R2/1n2RK2/8/8/7k/4r3/8 b - - 0 1", // RR vs RN endgame, where if black takes, they actually loses
"8/n3p3/8/2B5/2b5/7k/P7/7K w - - 0 1", // BP vs BNP endgame, leading to B vs BN
"8/n3p3/8/2B5/1n6/7k/P7/7K w - - 0 1", // BP vs NNP endgame, leading to B vs NN
"8/bRn5/8/7b/8/7k/8/7K w - - 0 1", // R vs BBN, leading to R vs BN or R vs BB
"1b6/1R1r4/8/1n6/7k/8/8/7K w - - 0 1", // R vs RBN, leading to R vs BN or R vs RB or R vs RN
"8/q5rk/8/8/8/8/Q5RK/7N w - - 0 1", // Endgame that can lead to QN vs Q or RN vs R positions
"1kr5/2bp3q/Q7/1K6/6q1/6B1/8/8 w - - 0 1", // Endgame where triple repetition can and needs to be forced by white
"1kr5/2bp3q/R7/1K6/6q1/6B1/8/8 w - - 96 200" // Endgame where 50 moves draw can and needs to be forced by white
Expand Down
54 changes: 45 additions & 9 deletions src/Lynx/Model/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -750,18 +750,54 @@ public bool WasProduceByAValidMove()
gamePhase = maxPhase;
}

// Check if drawn position due to lack of material
if (gamePhase <= 4)
// Pawnless endgames with few pieces
if (gamePhase <= 5 && pieceCount[(int)Piece.P] == 0 && pieceCount[(int)Piece.p] == 0)
{
var offset = Utils.PieceOffset(endGameScore >= 0);
switch (gamePhase)
{
case 5:
{
// RB vs R, RN vs R - escale it down due to the chances of it being a draw
if (pieceCount[(int)Piece.R] == 1 && pieceCount[(int)Piece.r] == 1)
{
endGameScore >>= 1; // /2
}

bool sideCannotWin = pieceCount[(int)Piece.P + offset] == 0 && pieceCount[(int)Piece.Q + offset] == 0 && pieceCount[(int)Piece.R + offset] == 0
&& (pieceCount[(int)Piece.B + offset] + pieceCount[(int)Piece.N + offset] == 1 // B or N
|| (pieceCount[(int)Piece.B + offset] == 0 && pieceCount[(int)Piece.N + offset] == 2)); // N+N
break;
}
case 3:
{
var winningSideOffset = Utils.PieceOffset(endGameScore >= 0);

if (sideCannotWin)
{
return (0, gamePhase);
if (pieceCount[(int)Piece.N + winningSideOffset] == 2) // NN vs N, NN vs B
{
return (0, gamePhase);
}

// Without rooks, only BB vs N is a win and BN vs N can have some chances
// Not taking that into account here though, we would need this to rule them out: `pieceCount[(int)Piece.b - winningSideOffset] == 1 || pieceCount[(int)Piece.B + winningSideOffset] <= 1`
if (pieceCount[(int)Piece.R + winningSideOffset] == 0) // BN vs B, NN vs B, BB vs B, BN vs N, NN vs N
{
endGameScore >>= 1; // /2
}

break;
}
case 2:
{
if (pieceCount[(int)Piece.N] + pieceCount[(int)Piece.n] == 2 // NN vs -, N vs N
|| pieceCount[(int)Piece.N] + pieceCount[(int)Piece.B] == 1) // B vs N, B vs B
{
return (0, gamePhase);
}

break;
}
case 1:
case 0:
{
return (0, gamePhase);
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions tests/Lynx.Test/BestMove/RegressionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,13 @@ public void MaxDepthCrash()

Assert.DoesNotThrow(() => engine.BestMove(new("go wtime 100000 btime 100000 winc 80 binc 80")));
}

[TestCase("8/8/4k3/3n1n2/5P2/8/3K4/8 b - - 0 12", null, new[] { "d5f4" },
Description = "NN vs P, where knights can't take the pawn")]
[TestCase("8/5R2/1n2RK2/8/8/7k/4r3/8 b - - 0 1", null, new[] { "e2e6" },
Description = "RR vs RB, where if the side with the bishop exchanges the rooks, they lose")]
public void PawnlessEndgames(string fen, string[]? allowedUCIMoveString, string[]? excludedUCIMoveString = null)
{
TestBestMove(fen, allowedUCIMoveString, excludedUCIMoveString);
}
}
61 changes: 61 additions & 0 deletions tests/Lynx.Test/Model/PositionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,50 @@ public void StaticEvaluation_Clamp(string fen, int expectedStaticEvaluation)
Assert.AreEqual(expectedStaticEvaluation, position.StaticEvaluation().Score);
}

[TestCase("k7/8/8/3K4/8/8/8/8 w - - 0 1", true, "K vs k")]
[TestCase("8/8/8/8/3k4/8/8/K7 w - - 0 1", true, "K vs k")]
public void StaticEvaluation_PawnlessEndgames_KingVsKing(string fen, bool isDrawExpected, string _)
{
EvaluateDrawOrNotDraw(fen, isDrawExpected, 0);
}

[TestCase("1k6/8/8/8/8/8/1B6/1K6 w - - 0 1", true, 1, "B")]
[TestCase("1k6/1b6/8/8/8/8/8/1K6 w - - 0 1", true, 1, "b")]
[TestCase("1k6/8/8/8/8/8/1N6/1K6 w - - 0 1", true, 1, "N")]
[TestCase("1k6/1n6/8/8/8/8/8/1K6 w - - 0 1", true, 1, "n")]
[TestCase("1k6/8/8/8/8/8/1R6/1K6 w - - 0 1", false, 2, "R")]
[TestCase("rk6/8/8/8/8/8/8/1K6 w - - 0 1", false, 2, "r")]
[TestCase("1k6/8/8/8/8/8/1Q6/1K6 w - - 0 1", false, 4, "Q")]
[TestCase("qk6/8/8/8/8/8/8/1K6 w - - 0 1", false, 4, "q")]
public void StaticEvaluation_PawnlessEndgames_SinglePiece(string fen, bool isDrawExpected, int expectedPhase, string _)
{
EvaluateDrawOrNotDraw(fen, isDrawExpected, expectedPhase);
}

[TestCase("1k6/n7/8/8/8/8/N7/1K6 w - - 0 1", true, "N vs n")]
[TestCase("1k6/b7/8/8/8/8/B7/1K6 w - - 0 1", true, "B vs b")]
[TestCase("1k6/n7/8/8/8/8/B7/1K6 w - - 0 1", true, "B vs n")]
[TestCase("1k6/b7/8/8/8/8/N7/1K6 w - - 0 1", true, "N vs b")]
[TestCase("1k6/8/8/8/8/8/NB6/1K6 w - - 0 1", false, "BN")]
[TestCase("1k6/8/8/8/8/8/BB6/1K6 w - - 0 1", false, "BB")]
[TestCase("1k6/bb6/8/8/8/8/8/1K6 w - - 0 1", false, "bb")]
[TestCase("1k6/bn6/8/8/8/8/8/1K6 w - - 0 1", false, "bn")]
[TestCase("1k6/8/8/8/8/8/NN6/1K6 w - - 0 1", true, "NN")]
[TestCase("1k6/nn6/8/8/8/8/8/1K6 w - - 0 1", true, "nn")]
public void StaticEvaluation_PawnlessEndgames_TwoMinorPieces(string fen, bool isDrawExpected, string _)
{
EvaluateDrawOrNotDraw(fen, isDrawExpected, 2);
}

[TestCase("8/8/3kb3/8/8/2NN4/3K4/8 w - - 0 1", true, "NN vs b")]
[TestCase("8/8/3knn2/8/8/3B4/3K4/8 w - - 0 1", true, "B vs nn")]
[TestCase("8/8/3kn3/8/8/2NN4/3K4/8 w - - 0 1", true, "NN vs n")]
[TestCase("8/8/3knn2/8/8/3N4/3K4/8 w - - 0 1", true, "N vs nn")]
public void StaticEvaluation_PawnlessEndgames_TwoMinorPiecesVsOne(string fen, bool isDrawExpected, string _)
{
EvaluateDrawOrNotDraw(fen, isDrawExpected, 3);
}

[TestCase(0, 0)]
[TestCase(0, 100)]
[TestCase(100, 100)]
Expand Down Expand Up @@ -972,4 +1016,21 @@ private static int AdditionalKingEvaluation(Position position, Piece piece)
? position.KingAdditionalEvaluation(bitBoard, Side.White, pieceCount).EndGameScore
: position.KingAdditionalEvaluation(bitBoard, Side.Black, pieceCount).EndGameScore;
}

private static void EvaluateDrawOrNotDraw(string fen, bool isDrawExpected, int expectedPhase)
{
var position = new Position(fen);
var (score, phase) = position.StaticEvaluation();

Assert.AreEqual(expectedPhase, phase);

if (isDrawExpected)
{
Assert.AreEqual(0, score);
}
else
{
Assert.AreNotEqual(0, score);
}
}
}

0 comments on commit 53cf80a

Please sign in to comment.