Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃К Implement SEE and order bad captures after killers but before quiet moves #554

Merged
merged 6 commits into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@
锘縰sing 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;
}
}