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
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue