Skip to content

Commit

Permalink
🧬 Implement SEE and order bad captures after killers but before quiet…
Browse files Browse the repository at this point in the history
… moves (#554)
  • Loading branch information
eduherminio committed Dec 31, 2023
1 parent 12ece97 commit 514d63d
Show file tree
Hide file tree
Showing 11 changed files with 555 additions and 21 deletions.
2 changes: 1 addition & 1 deletion src/Lynx.Benchmark/LocalVariableIn_vs_NoIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public static int OldScore(this Move move, in Position position, int[,]? killerM
}
}

return EvaluationConstants.CaptureMoveBaseScoreValue + EvaluationConstants.MostValueableVictimLeastValuableAttacker[sourcePiece, targetPiece];
return EvaluationConstants.BadCaptureMoveBaseScoreValue + EvaluationConstants.MostValueableVictimLeastValuableAttacker[sourcePiece, targetPiece];
}
else if (killerMoves is not null && plies is not null)
{
Expand Down
11 changes: 7 additions & 4 deletions src/Lynx/EvaluationConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,9 @@ static EvaluationConstants()

public const int TTMoveScoreValue = 2_097_152;

/// <summary>
/// For MVVLVA
/// </summary>
public const int CaptureMoveBaseScoreValue = 1_048_576;
#region Move ordering

public const int GoodCaptureMoveBaseScoreValue = 1_048_576;

public const int FirstKillerMoveValue = 524_288;

Expand All @@ -339,13 +338,17 @@ static EvaluationConstants()

public const int PromotionMoveScoreValue = 65_536;

public const int BadCaptureMoveBaseScoreValue = 32_768;

//public const int MaxHistoryMoveValue => Configuration.EngineSettings.MaxHistoryMoveValue;

/// <summary>
/// Negative offset to ensure history move scores don't reach other move ordering values
/// </summary>
public const int BaseMoveScore = int.MinValue / 2;

#endregion

/// <summary>
/// Outside of the evaluation ranges (higher than any sensible evaluation, lower than <see cref="PositiveCheckmateDetectionLimit"/>)
/// </summary>
Expand Down
39 changes: 39 additions & 0 deletions src/Lynx/Model/BitBoard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ namespace Lynx.Model;

public static class BitBoardExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Empty(this BitBoard board) => board == default;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool NotEmpty(this BitBoard board) => board != default;

public static BitBoard Initialize(params BoardSquare[] occupiedSquares)
{
BitBoard board = default;
Expand Down Expand Up @@ -101,6 +105,17 @@ public static void ToggleBits(this ref BitBoard bitboard, int squareA, int squar
bitboard ^= (1ul << squareA | 1ul << squareB);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong LSB(this BitBoard board)
{
if (System.Runtime.Intrinsics.X86.Bmi1.IsSupported)
{
return System.Runtime.Intrinsics.X86.Bmi1.X64.ExtractLowestSetBit(board);
}

return board & (~board + 1);
}

#region Static methods

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -152,6 +167,30 @@ public static int CountBits(this BitBoard board)
return BitOperations.PopCount(board);
}

/// <summary>
/// Extracts the bit that represents each square on a bitboard
/// </summary>
/// <param name="boardSquare"></param>
/// <returns></returns>
public static ulong SquareBit(int boardSquare)
{
return 1UL << boardSquare;
}

public static bool Contains(this BitBoard board, int boardSquare)
{
var bit = SquareBit(boardSquare);

return (board & bit) != default;
}

public static bool DoesNotContain(this BitBoard board, int boardSquare)
{
var bit = SquareBit(boardSquare);

return (board & bit) == default;
}

#endregion

#region Methods accepting BoardSquares
Expand Down
63 changes: 63 additions & 0 deletions src/Lynx/Model/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ public class Position
/// </summary>
public BitBoard[] PieceBitBoards { get; }

public BitBoard Queens => PieceBitBoards[(int)Piece.Q] | PieceBitBoards[(int)Piece.q];
public BitBoard Rooks => PieceBitBoards[(int)Piece.R] | PieceBitBoards[(int)Piece.r];
public BitBoard Bishops => PieceBitBoards[(int)Piece.B] | PieceBitBoards[(int)Piece.b];
public BitBoard Knights => PieceBitBoards[(int)Piece.N] | PieceBitBoards[(int)Piece.n];
public BitBoard Kings => PieceBitBoards[(int)Piece.K] | PieceBitBoards[(int)Piece.k];

/// <summary>
/// Black, White, Both
/// </summary>
Expand Down Expand Up @@ -583,6 +589,63 @@ public string FEN(int halfMovesWithoutCaptureOrPawnMove = 0, int fullMoveClock =
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int CountPieces() => PieceBitBoards.Sum(b => b.CountBits());

/// <summary>
/// Based on Stormphrax
/// </summary>
/// <param name="square"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int PieceAt(int square)
{
var bit = BitBoardExtensions.SquareBit(square);

Side color;

if ((OccupancyBitBoards[(int)Side.Black] & bit) != default)
{
color = Side.Black;
}
else if ((OccupancyBitBoards[(int)Side.White] & bit) != default)
{
color = Side.White;
}
else
{
return (int)Piece.Unknown;
}

var offset = Utils.PieceOffset(color);

for (int pieceIndex = offset; pieceIndex < 6 + offset; ++pieceIndex)
{
if (!(PieceBitBoards[pieceIndex] & bit).Empty())
{
return pieceIndex;
}
}

System.Diagnostics.Debug.Fail($"Bit set in {Side} occupancy bitboard, but not piece found");

return (int)Piece.Unknown;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong AllAttackersTo(int square, BitBoard occupancy)
{
System.Diagnostics.Debug.Assert(square != (int)BoardSquare.noSquare);

var queens = Queens;
var rooks = queens | Rooks;
var bishops = queens | Bishops;

return (rooks & Attacks.RookAttacks(square, occupancy))
| (bishops & Attacks.BishopAttacks(square, occupancy))
| (PieceBitBoards[(int)Piece.p] & Attacks.PawnAttacks[(int)Side.White, square])
| (PieceBitBoards[(int)Piece.P] & Attacks.PawnAttacks[(int)Side.Black, square])
| (Knights & Attacks.KnightAttacks[square])
| (Kings & Attacks.KingAttacks[square]);
}

/// <summary>
/// Evaluates material and position in a NegaMax style.
/// That is, positive scores always favour playing <see cref="Side"/>.
Expand Down
169 changes: 169 additions & 0 deletions src/Lynx/SEE.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using Lynx.Model;
using System.Runtime.CompilerServices;

namespace Lynx;

/// <summary>
/// Implementation based on Stormprhax, some comments and clarifications from Altair
/// </summary>
public static class SEE
{
#pragma warning disable IDE0055 // Discard formatting in this region

private static readonly int[] _pieceValues =
[
100, 450, 450, 650, 1250, 0,
100, 450, 450, 650, 1250, 0
];

#pragma warning restore IDE0055

/// <summary>
/// Doesn't handle non-captures, promotions and en-passants
/// </summary>
/// <param name="position"></param>
/// <param name="move"></param>
/// <param name="threshold"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsGoodCapture(Position position, Move move, short threshold = 0)
{
System.Diagnostics.Debug.Assert(move.IsCapture(), $"{nameof(IsGoodCapture)} doesn't handle non-capture moves");
System.Diagnostics.Debug.Assert(move.PromotedPiece() == default, $"{nameof(IsGoodCapture)} doesn't handle promotion moves");
System.Diagnostics.Debug.Assert(!move.IsEnPassant(), $"{nameof(IsGoodCapture)} potentially doesn't handle en-passant moves");

var sideToMove = position.Side;

var score = Gain(position, move) - threshold;

// If taking the opponent's piece without any risk is still negative
if (score < 0)
{
return false;
}

var next = move.Piece();
score -= _pieceValues[next];

// If risking our piece being fully lost and the exchange value is still >= 0
if (score >= 0)
{
return true;
}

var targetSquare = move.TargetSquare();

var occupancy = position.OccupancyBitBoards[(int)Side.Both]
^ BitBoardExtensions.SquareBit(move.SourceSquare())
^ BitBoardExtensions.SquareBit(targetSquare);

var queens = position.Queens;
var bishops = queens | position.Bishops;
var rooks = queens | position.Rooks;

var attackers = position.AllAttackersTo(targetSquare, occupancy);

var us = Utils.OppositeSide(sideToMove);

while (true)
{
var ourAttackers = attackers & position.OccupancyBitBoards[us];

if (ourAttackers.Empty())
{
break;
}

var nextPiece = PopLeastValuableAttacker(position, ref occupancy, ourAttackers, us);

// After removing an attacker, there could be a sliding piece attack
if (nextPiece == Piece.P || nextPiece == Piece.p
|| nextPiece == Piece.B || nextPiece == Piece.b
|| nextPiece == Piece.Q || nextPiece == Piece.q)
{
attackers |= Attacks.BishopAttacks(targetSquare, occupancy) & bishops;
}

if (nextPiece == Piece.R || nextPiece == Piece.r
|| nextPiece == Piece.Q || nextPiece == Piece.q)
{
attackers |= Attacks.RookAttacks(targetSquare, occupancy) & rooks;
}

// Removing used pieces from attackers
attackers &= occupancy;

score = -score - 1 - _pieceValues[(int)nextPiece];
us = Utils.OppositeSide(us);

if (score >= 0)
{
// Our only attacker is our king, but the opponent still has defenders
if ((nextPiece == Piece.K || nextPiece == Piece.k)
&& (attackers & position.OccupancyBitBoards[us]).NotEmpty())
{
us = Utils.OppositeSide(us);
}

break;
}
}

return (int)sideToMove != us;
}

/// <summary>
/// Doesn't handle non-captures, promotions and en-passants
/// </summary>
/// <param name="position"></param>
/// <param name="move"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Gain(Position position, Move move) => _pieceValues[position.PieceAt(move.TargetSquare())];

/// <summary>
/// Doesn't handle non-captures, promotions and en-passants
/// </summary>
/// <param name="position"></param>
/// <param name="move"></param>
/// <returns></returns>
[Obsolete("Since we're not handling non-captures, promotiosn and en-passants, we don't really need this")]
private static int CompleteGain(Position position, Move move)
{
if (move.IsCastle())
{
return 0;
}
else if (move.IsEnPassant())
{
return _pieceValues[(int)Piece.P];
}

var promotedPiece = move.PromotedPiece();

return promotedPiece == default
? _pieceValues[position.PieceAt(move.TargetSquare())]
: _pieceValues[promotedPiece] - _pieceValues[(int)Piece.P];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Piece PopLeastValuableAttacker(Position position, ref BitBoard occupancy, BitBoard attackers, int color)
{
var start = Utils.PieceOffset(color);

for (int i = start; i < start + 6; ++i)
{
var piece = (Piece)i;
var board = attackers & position.PieceBitBoards[i];

if (!board.Empty())
{
occupancy ^= board.LSB();

return piece;
}
}

return Piece.Unknown;
}
}

0 comments on commit 514d63d

Please sign in to comment.