More movegen bug fixes and minor improvements to the test suite

This commit is contained in:
Mattia Giambirtone 2024-04-23 11:48:11 +02:00
parent 0dfd647f4c
commit e62c78e4cc
3 changed files with 75 additions and 44 deletions

View File

@ -31,14 +31,20 @@ import misc
export bitboards, magics, pieces, moves, position, rays, misc, board
proc removePieceFromBitboard(self: Chessboard, square: Square)
proc addPieceToBitboard(self: Chessboard, square: Square, piece: Piece)
proc removePiece(self: Chessboard, square: Square)
proc spawnPiece(self: Chessboard, square: Square, piece: Piece)
proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
let
sideToMove = self.position.sideToMove
nonSideToMove = sideToMove.opposite()
pawns = self.getBitboard(Pawn, sideToMove)
occupancy = self.getOccupancy()
# We can only capture enemy pieces (except the king)
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
enemyPieces = self.getOccupancyFor(nonSideToMove)
epTarget = self.position.enPassantSquare
diagonalPins = self.position.diagonalPins
orthogonalPins = self.position.orthogonalPins
@ -54,7 +60,8 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
# If a pawn is pinned diagonally, it cannot push forward
let
# If a pawn is pinned horizontally, it cannot move either. It can move vertically
# though
# though. Thanks to Twipply for the tip on how to get a horizontal pin mask out of
# our orthogonal bitboard :)
horizontalPins = Bitboard((0xFF'u64 shl (rankFromSquare(friendlyKing).uint64 * 8))) and orthogonalPins
pushablePawns = pawns and not diagonalPins and not horizontalPins
singlePushes = (pushablePawns.forwardRelativeTo(sideToMove) and not occupancy) and destinationMask
@ -88,9 +95,8 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
let canCapture = pawns and not orthogonalPins
var
captureLeft = canCapture.forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask
captureRight = canCapture.forwardRightRelativeTo(sideToMove) and enemyPieces and destinationMask
captureLeft = canCapture.forwardLeftRelativeTo(sideToMove)
captureRight = canCapture.forwardRightRelativeTo(sideToMove)
# If a piece is pinned on the right, it can only capture on the right and
# vice versa for the left
if (let capture = diagonalPins and captureLeft; capture) != 0:
@ -99,6 +105,10 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
if (let capture = diagonalPins and captureRight; capture) != 0:
captureLeft = Bitboard(0)
captureRight = capture
# We mask off the non-enemy pieces and destination mask now because we need the unobstructed movement
# mask to check for pins correctly
captureLeft = captureLeft and enemyPieces and destinationMask
captureRight = captureRight and enemyPieces and destinationMask
for pawn in captureRight:
let pawnBB = pawn.toBitboard()
if promotionRank.contains(pawn):
@ -121,23 +131,38 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
epPawn = epBitboard.backwardRelativeTo(sideToMove)
epLeft = pawns.forwardLeftRelativeTo(sideToMove) and epBitboard and destinationMask
epRight = pawns.forwardRightRelativeTo(sideToMove) and epBitboard and destinationMask
var
newOccupancy = occupancy and not epPawn
friendlyPawn: Bitboard = Bitboard(0)
# Note: it's possible for two pawns to both have rights to do an en passant! See
# 4k3/8/8/2PpP3/8/8/8/4K3 w - d6 0 1
if epLeft != 0:
friendlyPawn = epBitboard.backwardRightRelativeTo(sideToMove)
elif epRight != 0:
friendlyPawn = epBitboard.backwardLeftRelativeTo(sideToMove)
if friendlyPawn != 0:
# We basically simulate the en passant and see if the resulting
# occupancy bitboard has the king in check
newOccupancy = newOccupancy and not friendlyPawn
newOccupancy = newOccupancy or epBitboard
let
friendlyPawn = epBitboard.backwardRightRelativeTo(sideToMove)
newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard
# We also need to temporarily remove the en passant pawn from
# our bitboards, or else functions like getPawnAttacks won't
# get the news that the pawn is gone and will still think the
# king is in check after en passant when it actually isn't
# (see pos fen rnbqkbnr/pppp1ppp/8/2P5/K7/8/PPPP1PPP/RNBQ1BNR b kq - 0 1 moves b7b5 c5b6)
let epPawnSquare = epPawn.toSquare()
let epPiece = self.getPiece(epPawnSquare)
self.removePiece(epPawnSquare)
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
# En passant does not create a check on the king: all good
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
self.addPieceToBitboard(epPawnSquare, Piece(kind: Pawn, color: nonSideToMove))
if epRight != 0:
# Note that this isn't going to be the same pawn from the previous if block!
let
friendlyPawn = epBitboard.backwardLeftRelativeTo(sideToMove)
newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard
let epPawnSquare = epPawn.toSquare()
let epPiece = self.getPiece(epPawnSquare)
self.removePiece(epPawnSquare)
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
# En passant does not create a check on the king: all good
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
self.addPieceToBitboard(epPawnSquare, Piece(kind: Pawn, color: nonSideToMove))
proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
@ -155,7 +180,7 @@ proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: B
let
blockers = occupancy and Rook.getRelevantBlockers(square)
moveset = getRookMoves(square, blockers)
for target in moveset and pinMask and destinationMask and not enemyPieces:
for target in moveset and pinMask and destinationMask and not enemyPieces:
moves.add(createMove(square, target))
for target in moveset and enemyPieces and pinMask and destinationMask:
moves.add(createMove(square, target, Capture))
@ -186,7 +211,7 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask:
moveset = getBishopMoves(square, blockers)
for target in moveset and pinMask and destinationMask and not enemyPieces:
moves.add(createMove(square, target))
for target in moveset and enemyPieces and destinationMask:
for target in moveset and pinMask and enemyPieces and destinationMask:
moves.add(createMove(square, target, Capture))
for square in unpinnedBishops:
let
@ -236,7 +261,7 @@ proc generateCastling(self: Chessboard, moves: var MoveList) =
sideToMove = self.position.sideToMove
castlingRights = self.canCastle()
kingSquare = self.getBitboard(King, sideToMove).toSquare()
kingPiece = self.grid[kingSquare]
kingPiece = self.getPiece(kingSquare)
if castlingRights.king:
moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle))
if castlingRights.queen:
@ -276,9 +301,19 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
# it, meaning the only legal moves are those that either block the
# check or capture the checking piece. For other non-sliding pieces
# the ray will be empty so the only legal move will be to capture
# the checking piece
let checker = self.position.checkers.lowestSquare()
destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard()
# the checking piece (or moving the king)
let
checker = self.position.checkers.lowestSquare()
checkerBB = checker.toBitboard()
epTarget = self.position.enPassantSquare
checkerPiece = self.getPiece(checker)
destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB
if checkerPiece.kind == Pawn and checkerBB.backwardRelativeTo(checkerPiece.color).toSquare() == epTarget:
# We are in check by a pawn that pushed two squares: add the ep target square to the set of
# squares that our friendly pieces can move to in order to resolve it. This will do nothing
# for most pieces, because the move generators won't allow them to move there, but it does matter
# for pawns
destinationMask = destinationMask or epTarget.toBitboard()
self.generatePawnMoves(moves, destinationMask)
self.generateKnightMoves(moves, destinationMask)
self.generateRookMoves(moves, destinationMask)
@ -290,7 +325,7 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
proc removePieceFromBitboard(self: Chessboard, square: Square) =
## Removes a piece at the given square in the chessboard from
## its respective bitboard
let piece = self.grid[square]
let piece = self.getPiece(square)
self.position.pieces[piece.color][piece.kind][].clearBit(square)
@ -304,7 +339,7 @@ proc spawnPiece(self: Chessboard, square: Square, piece: Piece) =
## Internal helper to "spawn" a given piece at the given
## square
when not defined(danger):
doAssert self.grid[square].kind == Empty
doAssert self.getPiece(square).kind == Empty
self.addPieceToBitboard(square, piece)
self.grid[square] = piece
@ -313,7 +348,7 @@ proc removePiece(self: Chessboard, square: Square) =
## Removes a piece from the board, updating necessary
## metadata
when not defined(danger):
let Piece = self.grid[square]
let Piece = self.getPiece(square)
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
self.removePieceFromBitboard(square)
self.grid[square] = nullPiece()
@ -405,13 +440,14 @@ proc doMove*(self: Chessboard, move: Move) =
if move.isCapture():
# Get rid of captured pieces
let captured = self.getPiece(move.targetSquare)
self.removePiece(move.targetSquare)
# If a rook has been captured, castling on that side is prohibited
if piece.kind == Rook:
if move.targetSquare == piece.color.kingSideRook():
self.position.castlingAvailability[piece.color.int].king = false
elif move.targetSquare == piece.color.queenSideRook():
self.position.castlingAvailability[piece.color.int].queen = false
if captured.kind == Rook:
if move.targetSquare == captured.color.kingSideRook():
self.position.castlingAvailability[captured.color.int].king = false
elif move.targetSquare == captured.color.queenSideRook():
self.position.castlingAvailability[captured.color.int].queen = false
# Move the piece to its target square
self.movePiece(move)

View File

@ -360,8 +360,7 @@ const HELP_TEXT = """Nimfish help menu:
- fen: Shorthand for "position fen"
- pos <args>: Shorthand for "position <args>"
- get <square>: Get the piece on the given square
- def <square>: Print the attack bitboard of the given square for the side to move
- atk <square>: Print the attack bitboard of the given square for the opponent side
- atk <square>: Print which pieces are currently attacking the given square
- pins: Print the current pin mask
- checks: Print the current checks mask
- skip: Swap the side to move
@ -415,15 +414,6 @@ proc commandLoop*: int =
board.unmakeMove()
of "stm":
echo &"Side to move: {board.position.sideToMove}"
of "def":
if len(cmd) != 2:
echo "error: def: invalid number of arguments"
continue
try:
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove)
except ValueError:
echo "error: def: invalid square"
continue
of "atk":
if len(cmd) != 2:
echo "error: atk: invalid number of arguments"

View File

@ -51,20 +51,25 @@ def main(args: Namespace) -> int:
else:
failed.append(futures[future])
except KeyboardInterrupt:
stop = timeit.default_timer()
pool.shutdown(cancel_futures=True)
print(f"\r[S] Interrupted\033[K")
print(f"[S] Ran {i} tests at depth {args.ply} in {stop - start:.2f} seconds ({len(successful)} successful, {len(failed)} failed)")
if failed and args.show_failures:
print("[S] The following FENs failed to pass the test:\n\t", end="")
print("\n\t".join(failed))
else:
stop = timeit.default_timer()
print(f"\r[S] Ran {len(positions)} tests at depth {args.ply} in {stop - start:.2f} seconds ({len(successful)} successful, {len(failed)} failed)\033[K")
if failed and args.show_failures:
print("[S] The following FENs failed to pass the test:", end="")
print("[S] The following FENs failed to pass the test:\n\t", end="")
print("\n\t".join(failed))
if __name__ == "__main__":
parser = ArgumentParser(description="Run a set of tests using compare_positions.py")
parser.add_argument("--ply", "-d", type=int, required=True, help="The depth to stop at, expressed in plys (half-moves)")
parser.add_argument("--bulk", action="store_true", help="Enable bulk-counting for Nimfish (much faster)", default=False)
parser.add_argument("-b", "--bulk", action="store_true", help="Enable bulk-counting for Nimfish (much faster)", default=False)
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--positions", type=Path, help="Location of the file containing FENs to test, one per line. Defaults to 'tests/positions.txt'",
@ -72,7 +77,7 @@ if __name__ == "__main__":
parser.add_argument("--no-silent", action="store_true", help="Do not suppress output from compare_positions.py (defaults to False)", default=False)
parser.add_argument("-p", "--parallel", action="store_true", help="Run multiple tests in parallel", default=False)
parser.add_argument("--workers", "-w", type=int, required=False, help="How many workers to use in parallel mode (defaults to cpu_count() / 2)", default=cpu_count() // 2)
parser.add_argument("--show-failures", action="store_true", help="Show which FENs failed to pass the test", default=False)
parser.add_argument("-s", "--show-failures", action="store_true", help="Show which FENs failed to pass the test", default=False)
try:
sys.exit(main(parser.parse_args()))
except KeyboardInterrupt: