More movegen bug fixes and minor improvements to the test suite
This commit is contained in:
parent
0dfd647f4c
commit
e62c78e4cc
|
@ -31,14 +31,20 @@ import misc
|
||||||
export bitboards, magics, pieces, moves, position, rays, misc, board
|
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) =
|
proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
|
||||||
let
|
let
|
||||||
sideToMove = self.position.sideToMove
|
sideToMove = self.position.sideToMove
|
||||||
|
nonSideToMove = sideToMove.opposite()
|
||||||
pawns = self.getBitboard(Pawn, sideToMove)
|
pawns = self.getBitboard(Pawn, sideToMove)
|
||||||
occupancy = self.getOccupancy()
|
occupancy = self.getOccupancy()
|
||||||
# We can only capture enemy pieces (except the king)
|
# 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
|
epTarget = self.position.enPassantSquare
|
||||||
diagonalPins = self.position.diagonalPins
|
diagonalPins = self.position.diagonalPins
|
||||||
orthogonalPins = self.position.orthogonalPins
|
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
|
# If a pawn is pinned diagonally, it cannot push forward
|
||||||
let
|
let
|
||||||
# If a pawn is pinned horizontally, it cannot move either. It can move vertically
|
# 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
|
horizontalPins = Bitboard((0xFF'u64 shl (rankFromSquare(friendlyKing).uint64 * 8))) and orthogonalPins
|
||||||
pushablePawns = pawns and not diagonalPins and not horizontalPins
|
pushablePawns = pawns and not diagonalPins and not horizontalPins
|
||||||
singlePushes = (pushablePawns.forwardRelativeTo(sideToMove) and not occupancy) and destinationMask
|
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
|
let canCapture = pawns and not orthogonalPins
|
||||||
var
|
var
|
||||||
captureLeft = canCapture.forwardLeftRelativeTo(sideToMove) and enemyPieces and destinationMask
|
captureLeft = canCapture.forwardLeftRelativeTo(sideToMove)
|
||||||
captureRight = canCapture.forwardRightRelativeTo(sideToMove) and enemyPieces and destinationMask
|
captureRight = canCapture.forwardRightRelativeTo(sideToMove)
|
||||||
|
|
||||||
# If a piece is pinned on the right, it can only capture on the right and
|
# If a piece is pinned on the right, it can only capture on the right and
|
||||||
# vice versa for the left
|
# vice versa for the left
|
||||||
if (let capture = diagonalPins and captureLeft; capture) != 0:
|
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:
|
if (let capture = diagonalPins and captureRight; capture) != 0:
|
||||||
captureLeft = Bitboard(0)
|
captureLeft = Bitboard(0)
|
||||||
captureRight = capture
|
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:
|
for pawn in captureRight:
|
||||||
let pawnBB = pawn.toBitboard()
|
let pawnBB = pawn.toBitboard()
|
||||||
if promotionRank.contains(pawn):
|
if promotionRank.contains(pawn):
|
||||||
|
@ -121,23 +131,38 @@ proc generatePawnMoves(self: Chessboard, moves: var MoveList, destinationMask: B
|
||||||
epPawn = epBitboard.backwardRelativeTo(sideToMove)
|
epPawn = epBitboard.backwardRelativeTo(sideToMove)
|
||||||
epLeft = pawns.forwardLeftRelativeTo(sideToMove) and epBitboard and destinationMask
|
epLeft = pawns.forwardLeftRelativeTo(sideToMove) and epBitboard and destinationMask
|
||||||
epRight = pawns.forwardRightRelativeTo(sideToMove) and epBitboard and destinationMask
|
epRight = pawns.forwardRightRelativeTo(sideToMove) and epBitboard and destinationMask
|
||||||
var
|
# Note: it's possible for two pawns to both have rights to do an en passant! See
|
||||||
newOccupancy = occupancy and not epPawn
|
# 4k3/8/8/2PpP3/8/8/8/4K3 w - d6 0 1
|
||||||
friendlyPawn: Bitboard = Bitboard(0)
|
|
||||||
if epLeft != 0:
|
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
|
# We basically simulate the en passant and see if the resulting
|
||||||
# occupancy bitboard has the king in check
|
# occupancy bitboard has the king in check
|
||||||
newOccupancy = newOccupancy and not friendlyPawn
|
let
|
||||||
newOccupancy = newOccupancy or epBitboard
|
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):
|
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
|
||||||
# En passant does not create a check on the king: all good
|
# En passant does not create a check on the king: all good
|
||||||
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
|
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) =
|
proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
|
||||||
|
@ -155,7 +180,7 @@ proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: B
|
||||||
let
|
let
|
||||||
blockers = occupancy and Rook.getRelevantBlockers(square)
|
blockers = occupancy and Rook.getRelevantBlockers(square)
|
||||||
moveset = getRookMoves(square, blockers)
|
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))
|
moves.add(createMove(square, target))
|
||||||
for target in moveset and enemyPieces and pinMask and destinationMask:
|
for target in moveset and enemyPieces and pinMask and destinationMask:
|
||||||
moves.add(createMove(square, target, Capture))
|
moves.add(createMove(square, target, Capture))
|
||||||
|
@ -186,7 +211,7 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask:
|
||||||
moveset = getBishopMoves(square, blockers)
|
moveset = getBishopMoves(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))
|
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))
|
moves.add(createMove(square, target, Capture))
|
||||||
for square in unpinnedBishops:
|
for square in unpinnedBishops:
|
||||||
let
|
let
|
||||||
|
@ -236,7 +261,7 @@ proc generateCastling(self: Chessboard, moves: var MoveList) =
|
||||||
sideToMove = self.position.sideToMove
|
sideToMove = self.position.sideToMove
|
||||||
castlingRights = self.canCastle()
|
castlingRights = self.canCastle()
|
||||||
kingSquare = self.getBitboard(King, sideToMove).toSquare()
|
kingSquare = self.getBitboard(King, sideToMove).toSquare()
|
||||||
kingPiece = self.grid[kingSquare]
|
kingPiece = self.getPiece(kingSquare)
|
||||||
if castlingRights.king:
|
if castlingRights.king:
|
||||||
moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle))
|
moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle))
|
||||||
if castlingRights.queen:
|
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
|
# it, meaning the only legal moves are those that either block the
|
||||||
# check or capture the checking piece. For other non-sliding pieces
|
# 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 ray will be empty so the only legal move will be to capture
|
||||||
# the checking piece
|
# the checking piece (or moving the king)
|
||||||
let checker = self.position.checkers.lowestSquare()
|
let
|
||||||
destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard()
|
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.generatePawnMoves(moves, destinationMask)
|
||||||
self.generateKnightMoves(moves, destinationMask)
|
self.generateKnightMoves(moves, destinationMask)
|
||||||
self.generateRookMoves(moves, destinationMask)
|
self.generateRookMoves(moves, destinationMask)
|
||||||
|
@ -290,7 +325,7 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
||||||
proc removePieceFromBitboard(self: Chessboard, square: Square) =
|
proc removePieceFromBitboard(self: Chessboard, square: Square) =
|
||||||
## Removes a piece at the given square in the chessboard from
|
## Removes a piece at the given square in the chessboard from
|
||||||
## its respective bitboard
|
## its respective bitboard
|
||||||
let piece = self.grid[square]
|
let piece = self.getPiece(square)
|
||||||
self.position.pieces[piece.color][piece.kind][].clearBit(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
|
## Internal helper to "spawn" a given piece at the given
|
||||||
## square
|
## square
|
||||||
when not defined(danger):
|
when not defined(danger):
|
||||||
doAssert self.grid[square].kind == Empty
|
doAssert self.getPiece(square).kind == Empty
|
||||||
self.addPieceToBitboard(square, piece)
|
self.addPieceToBitboard(square, piece)
|
||||||
self.grid[square] = piece
|
self.grid[square] = piece
|
||||||
|
|
||||||
|
@ -313,7 +348,7 @@ proc removePiece(self: Chessboard, square: Square) =
|
||||||
## Removes a piece from the board, updating necessary
|
## Removes a piece from the board, updating necessary
|
||||||
## metadata
|
## metadata
|
||||||
when not defined(danger):
|
when not defined(danger):
|
||||||
let Piece = self.grid[square]
|
let Piece = self.getPiece(square)
|
||||||
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
||||||
self.removePieceFromBitboard(square)
|
self.removePieceFromBitboard(square)
|
||||||
self.grid[square] = nullPiece()
|
self.grid[square] = nullPiece()
|
||||||
|
@ -405,13 +440,14 @@ proc doMove*(self: Chessboard, move: Move) =
|
||||||
|
|
||||||
if move.isCapture():
|
if move.isCapture():
|
||||||
# Get rid of captured pieces
|
# Get rid of captured pieces
|
||||||
|
let captured = self.getPiece(move.targetSquare)
|
||||||
self.removePiece(move.targetSquare)
|
self.removePiece(move.targetSquare)
|
||||||
# If a rook has been captured, castling on that side is prohibited
|
# If a rook has been captured, castling on that side is prohibited
|
||||||
if piece.kind == Rook:
|
if captured.kind == Rook:
|
||||||
if move.targetSquare == piece.color.kingSideRook():
|
if move.targetSquare == captured.color.kingSideRook():
|
||||||
self.position.castlingAvailability[piece.color.int].king = false
|
self.position.castlingAvailability[captured.color.int].king = false
|
||||||
elif move.targetSquare == piece.color.queenSideRook():
|
elif move.targetSquare == captured.color.queenSideRook():
|
||||||
self.position.castlingAvailability[piece.color.int].queen = false
|
self.position.castlingAvailability[captured.color.int].queen = false
|
||||||
|
|
||||||
# Move the piece to its target square
|
# Move the piece to its target square
|
||||||
self.movePiece(move)
|
self.movePiece(move)
|
||||||
|
|
|
@ -360,8 +360,7 @@ const HELP_TEXT = """Nimfish help menu:
|
||||||
- fen: Shorthand for "position fen"
|
- fen: Shorthand for "position fen"
|
||||||
- pos <args>: Shorthand for "position <args>"
|
- pos <args>: Shorthand for "position <args>"
|
||||||
- get <square>: Get the piece on the given square
|
- 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 which pieces are currently attacking the given square
|
||||||
- atk <square>: Print the attack bitboard of the given square for the opponent side
|
|
||||||
- pins: Print the current pin mask
|
- pins: Print the current pin mask
|
||||||
- checks: Print the current checks mask
|
- checks: Print the current checks mask
|
||||||
- skip: Swap the side to move
|
- skip: Swap the side to move
|
||||||
|
@ -415,15 +414,6 @@ proc commandLoop*: int =
|
||||||
board.unmakeMove()
|
board.unmakeMove()
|
||||||
of "stm":
|
of "stm":
|
||||||
echo &"Side to move: {board.position.sideToMove}"
|
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":
|
of "atk":
|
||||||
if len(cmd) != 2:
|
if len(cmd) != 2:
|
||||||
echo "error: atk: invalid number of arguments"
|
echo "error: atk: invalid number of arguments"
|
||||||
|
|
|
@ -51,20 +51,25 @@ def main(args: Namespace) -> int:
|
||||||
else:
|
else:
|
||||||
failed.append(futures[future])
|
failed.append(futures[future])
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
stop = timeit.default_timer()
|
||||||
pool.shutdown(cancel_futures=True)
|
pool.shutdown(cancel_futures=True)
|
||||||
print(f"\r[S] Interrupted\033[K")
|
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:
|
else:
|
||||||
stop = timeit.default_timer()
|
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")
|
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:
|
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))
|
print("\n\t".join(failed))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = ArgumentParser(description="Run a set of tests using compare_positions.py")
|
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("--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("--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("--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'",
|
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("--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("-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("--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:
|
try:
|
||||||
sys.exit(main(parser.parse_args()))
|
sys.exit(main(parser.parse_args()))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|
Loading…
Reference in New Issue