diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 4286e4d..6ddd322 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -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) diff --git a/Chess/nimfish/nimfishpkg/tui.nim b/Chess/nimfish/nimfishpkg/tui.nim index 40feadb..f4cdbf8 100644 --- a/Chess/nimfish/nimfishpkg/tui.nim +++ b/Chess/nimfish/nimfishpkg/tui.nim @@ -360,8 +360,7 @@ const HELP_TEXT = """Nimfish help menu: - fen: Shorthand for "position fen" - pos : Shorthand for "position " - get : Get the piece on the given square - - def : Print the attack bitboard of the given square for the side to move - - atk : Print the attack bitboard of the given square for the opponent side + - atk : 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" diff --git a/Chess/tests/suite.py b/Chess/tests/suite.py index f1a5650..6d53f9f 100644 --- a/Chess/tests/suite.py +++ b/Chess/tests/suite.py @@ -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: