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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use non-alloc version of get moves instead of game move pool #301

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/Lynx.Dev/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
//FileAndRankMasks();
//EnhancedPawnEvaluation();
//RookEvaluation();
TranspositionTable();
_42_Perft();
//TranspositionTable();

#pragma warning disable IDE0059 // Unnecessary assignment of a value
const string TrickyPosition = Constants.TrickyTestPositionFEN;
Expand Down Expand Up @@ -520,14 +521,20 @@ static void _42_Perft()

var pos = new Position(TrickyPosition);

for (int depth = 0; depth < 7; ++depth)
for (int depth = 0; depth < 6; ++depth)
{
var sw = new Stopwatch();
sw.Start();
var nodes = Perft.Results(in pos, depth);
var nodes = Perft.Results(in pos, depth, false);
sw.Stop();
}

Console.WriteLine($"Depth {depth}\tNodes: {nodes}\tTime: {sw.ElapsedMilliseconds}ms");
for (int depth = 0; depth < 6; ++depth)
{
var sw = new Stopwatch();
sw.Start();
var nodes = Perft.Results(in pos, depth, true);
sw.Stop();
}
}

Expand Down
7 changes: 3 additions & 4 deletions src/Lynx/Model/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ public sealed class Game
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

public Move[] MovePool { get; } = new Move[Constants.MaxNumberOfPossibleMovesInAPosition];

public List<Move> MoveHistory { get; }
public List<Position> PositionHistory { get; }
public HashSet<long> PositionHashHistory { get; }
Expand Down Expand Up @@ -46,9 +44,10 @@ public Game(string fen, List<string> movesUCIString) : this(fen)
{
foreach (var moveString in movesUCIString)
{
var moveList = MoveGenerator.GenerateAllMoves(CurrentPosition, MovePool);
Span<Move> moveList = stackalloc Move[Constants.MaxNumberOfPossibleMovesInAPosition];
MoveGenerator.GenerateAllMoves(CurrentPosition, ref moveList);

if (!MoveExtensions.TryParseFromUCIString(moveString, moveList, out var parsedMove))
if (!MoveExtensions.TryParseFromUCIString(moveString, ref moveList, out var parsedMove))
{
_logger.Error("Error parsing game with fen {0} and moves {1}: error detected in {2}", fen, string.Join(' ', movesUCIString), moveString);
break;
Expand Down
60 changes: 60 additions & 0 deletions src/Lynx/Model/Move.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NLog;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
Expand Down Expand Up @@ -112,6 +113,65 @@ bool predicate(Move m)
}
}

/// <summary>
/// Returns the move from <paramref name="moveList"/> indicated by <paramref name="UCIString"/>
/// </summary>
/// <param name="UCIString"></param>
/// <param name="moveList"></param>
/// <param name="move"></param>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="IndexOutOfRangeException"></exception>
/// <returns></returns>
public static bool TryParseFromUCIString(string UCIString, ref Span<Move> moveList, [NotNullWhen(true)] out Move? move)
{
Utils.Assert(UCIString.Length == 4 || UCIString.Length == 5);

var sourceSquare = (UCIString[0] - 'a') + ((8 - (UCIString[1] - '0')) * 8);
var targetSquare = (UCIString[2] - 'a') + ((8 - (UCIString[3] - '0')) * 8);

var candidateMoves = moveList.ToImmutableArray().Where(move => move.SourceSquare() == sourceSquare && move.TargetSquare() == targetSquare);

if (UCIString.Length == 4)
{
move = candidateMoves.FirstOrDefault();

if (move.Equals(default(Move)))
{
_logger.Warn("Unable to link last move string {0} to a valid move in the current position. That move may have already been played", UCIString);
move = null;
return false;
}

Utils.Assert(move.Value.PromotedPiece() == default);
return true;
}
else
{
var promotedPiece = (int)Enum.Parse<Piece>(UCIString[4].ToString());

bool predicate(Move m)
{
var actualPromotedPiece = m.PromotedPiece();

return actualPromotedPiece == promotedPiece
|| actualPromotedPiece == promotedPiece - 6;
}

move = candidateMoves.FirstOrDefault(predicate);
if (move.Equals(default(Move)))
{
_logger.Warn("Unable to link move {0} to a valid move in the current position. That move may have already been played", UCIString);
move = null;
return false;
}

Utils.Assert(candidateMoves.Count() == 4);
Utils.Assert(candidateMoves.Count(predicate) == 1);

return true;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SourceSquare(this Move move) => move & 0x3F;

Expand Down
7 changes: 7 additions & 0 deletions src/Lynx/Model/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,13 @@ private string CalculateFEN()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IEnumerable<Move> AllCapturesMoves(Move[]? movePool = null) => MoveGenerator.GenerateAllMoves(this, movePool, capturesOnly: true);


[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AllPossibleMoves(ref Span<Move> movePool) => MoveGenerator.GenerateAllMoves(this, ref movePool);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AllCapturesMoves(ref Span<Move> movePool) => MoveGenerator.GenerateAllMoves(this, ref movePool, capturesOnly: true);

public int CountPieces() => PieceBitBoards.Sum(b => b.CountBits());

/// <summary>
Expand Down
226 changes: 226 additions & 0 deletions src/Lynx/MoveGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,232 @@ internal static void GeneratePieceMoves(ref int localIndex, Move[] movePool, int
}
}

/// <summary>
/// Generates all psuedo-legal moves from <paramref name="position"/>, ordered by <see cref="Move.Score(Position)"/>
/// </summary>
/// <param name="position"></param>
/// <param name="capturesOnly">Filters out all moves but captures</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void GenerateAllMoves(in Position position, ref Span<Move> movePool, bool capturesOnly = false)
{
#if DEBUG
if (position.Side == Side.Both)
{
return;
}
#endif

int localIndex = 0;

var offset = Utils.PieceOffset(position.Side);

GeneratePawnMoves(ref localIndex, ref movePool, in position, offset, capturesOnly);
GenerateCastlingMoves(ref localIndex, ref movePool, in position, offset);
GeneratePieceMoves(ref localIndex, ref movePool, (int)Piece.K + offset, in position, capturesOnly);
GeneratePieceMoves(ref localIndex, ref movePool, (int)Piece.N + offset, in position, capturesOnly);
GeneratePieceMoves(ref localIndex, ref movePool, (int)Piece.B + offset, in position, capturesOnly);
GeneratePieceMoves(ref localIndex, ref movePool, (int)Piece.R + offset, in position, capturesOnly);
GeneratePieceMoves(ref localIndex, ref movePool, (int)Piece.Q + offset, in position, capturesOnly);

movePool = movePool.Slice(0, localIndex);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void GeneratePawnMoves(ref int localIndex, ref Span<Move> movePool, in Position position, int offset, bool capturesOnly = false)
{
int sourceSquare, targetSquare;

var piece = (int)Piece.P + offset;
var pawnPush = +8 - ((int)position.Side * 16); // position.Side == Side.White ? -8 : +8
int oppositeSide = Utils.OppositeSide(position.Side); // position.Side == Side.White ? (int)Side.Black : (int)Side.White
var bitboard = position.PieceBitBoards[piece];

while (bitboard != default)
{
sourceSquare = bitboard.GetLS1BIndex();
bitboard.ResetLS1B();

var sourceRank = (sourceSquare / 8) + 1;

#if DEBUG
if (sourceRank == 1 || sourceRank == 8)
{
_logger.Warn("There's a non-promoted {0} pawn in rank {1}", position.Side, sourceRank);
continue;
}
#endif

// Pawn pushes
var singlePushSquare = sourceSquare + pawnPush;
if (!position.OccupancyBitBoards[2].GetBit(singlePushSquare))
{
// Single pawn push
var targetRank = (singlePushSquare / 8) + 1;
if (targetRank == 1 || targetRank == 8) // Promotion
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.Q + offset);
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.R + offset);
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.N + offset);
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.B + offset);
}
else if (!capturesOnly)
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, singlePushSquare, piece);
}

// Double pawn push
// Inside of the if because singlePush square cannot be occupied either
if (!capturesOnly)
{
var doublePushSquare = sourceSquare + (2 * pawnPush);
if (!position.OccupancyBitBoards[2].GetBit(doublePushSquare)
&& ((sourceRank == 2 && position.Side == Side.Black) || (sourceRank == 7 && position.Side == Side.White)))
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, doublePushSquare, piece, isDoublePawnPush: TRUE);
}
}
}

var attacks = Attacks.PawnAttacks[(int)position.Side, sourceSquare];

// En passant
if (position.EnPassant != BoardSquare.noSquare && attacks.GetBit(position.EnPassant))
// We assume that position.OccupancyBitBoards[oppositeOccupancy].GetBit(targetSquare + singlePush) == true
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, (int)position.EnPassant, piece, isCapture: TRUE, isEnPassant: TRUE);
}

// Captures
var attackedSquares = attacks & position.OccupancyBitBoards[oppositeSide];
while (attackedSquares != default)
{
targetSquare = attackedSquares.GetLS1BIndex();
attackedSquares.ResetLS1B();

var targetRank = (targetSquare / 8) + 1;
if (targetRank == 1 || targetRank == 8) // Capture with promotion
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.Q + offset, isCapture: TRUE);
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.R + offset, isCapture: TRUE);
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.N + offset, isCapture: TRUE);
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.B + offset, isCapture: TRUE);
}
else
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece, isCapture: TRUE);
}
}
}
}

/// <summary>
/// Obvious moves that put the king in check have been discarded, but the rest still need to be discarded
/// see FEN position "8/8/8/2bbb3/2bKb3/2bbb3/8/8 w - - 0 1", where 4 legal moves (corners) are found
/// </summary>
/// <param name="position"></param>
/// <param name="offset"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void GenerateCastlingMoves(ref int localIndex, ref Span<Move> movePool, in Position position, int offset)
{
var piece = (int)Piece.K + offset;
var oppositeSide = (Side)Utils.OppositeSide(position.Side);

int sourceSquare = position.PieceBitBoards[piece].GetLS1BIndex(); // There's for sure only one

// Castles
if (position.Castle != default)
{
if (position.Side == Side.White)
{
bool ise1Attacked = Attacks.IsSquaredAttackedBySide((int)BoardSquare.e1, in position, oppositeSide);
if (((position.Castle & (int)CastlingRights.WK) != default)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.f1)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.g1)
&& !ise1Attacked
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.f1, in position, oppositeSide)
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.g1, in position, oppositeSide))
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, Constants.WhiteShortCastleKingSquare, piece, isShortCastle: TRUE);
}

if (((position.Castle & (int)CastlingRights.WQ) != default)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.d1)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.c1)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.b1)
&& !ise1Attacked
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.d1, in position, oppositeSide)
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.c1, in position, oppositeSide))
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, Constants.WhiteLongCastleKingSquare, piece, isLongCastle: TRUE);
}
}
else
{
bool ise8Attacked = Attacks.IsSquaredAttackedBySide((int)BoardSquare.e8, in position, oppositeSide);
if (((position.Castle & (int)CastlingRights.BK) != default)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.f8)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.g8)
&& !ise8Attacked
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.f8, in position, oppositeSide)
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.g8, in position, oppositeSide))
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, Constants.BlackShortCastleKingSquare, piece, isShortCastle: TRUE);
}

if (((position.Castle & (int)CastlingRights.BQ) != default)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.d8)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.c8)
&& !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.b8)
&& !ise8Attacked
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.d8, in position, oppositeSide)
&& !Attacks.IsSquaredAttackedBySide((int)BoardSquare.c8, in position, oppositeSide))
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, Constants.BlackLongCastleKingSquare, piece, isLongCastle: TRUE);
}
}
}
}

/// <summary>
/// Generate Knight, Bishop, Rook and Queen moves
/// </summary>
/// <param name="piece"><see cref="Piece"/></param>
/// <param name="position"></param>
/// <param name="capturesOnly"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void GeneratePieceMoves(ref int localIndex, ref Span<Move> movePool, int piece, in Position position, bool capturesOnly = false)
{
var bitboard = position.PieceBitBoards[piece];
int sourceSquare, targetSquare;

while (bitboard != default)
{
sourceSquare = bitboard.GetLS1BIndex();
bitboard.ResetLS1B();

var attacks = _pieceAttacks[piece](sourceSquare, position.OccupancyBitBoards[(int)Side.Both])
& ~position.OccupancyBitBoards[(int)position.Side];

while (attacks != default)
{
targetSquare = attacks.GetLS1BIndex();
attacks.ResetLS1B();

if (position.OccupancyBitBoards[(int)Side.Both].GetBit(targetSquare))
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece, isCapture: TRUE);
}
else if (!capturesOnly)
{
movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece);
}
}
}
}

#region Only for reference, but unused

/// <summary>
Expand Down
Loading