From fe987576c3621382be8c3ef7d07c574777f22524 Mon Sep 17 00:00:00 2001 From: Mattia Giambirtone Date: Sat, 20 Apr 2024 23:47:57 +0200 Subject: [PATCH] Bug fixes to checks, pins and more. Reworking pawn movegen --- Chess/nimfish/nimfish.nim | 293 ++++++++++++++----------- Chess/nimfish/nimfishpkg/bitboards.nim | 6 + Chess/nimfish/nimfishpkg/position.nim | 5 + Chess/nimfish/nimfishpkg/tui.nim | 13 +- Chess/tests/compare_positions.py | 2 + 5 files changed, 185 insertions(+), 134 deletions(-) diff --git a/Chess/nimfish/nimfish.nim b/Chess/nimfish/nimfish.nim index 3685e67..d97d335 100644 --- a/Chess/nimfish/nimfish.nim +++ b/Chess/nimfish/nimfish.nim @@ -37,7 +37,7 @@ type # The current position position*: Position # List of all previously reached positions - positions: seq[Position] + positions*: seq[Position] # A bunch of simple utility functions and forward declarations @@ -53,7 +53,7 @@ proc removePiece(self: Chessboard, square: Square) proc update*(self: Chessboard) func inCheck*(self: Chessboard): bool {.inline.} proc fromChar*(c: char): Piece - +proc updateChecksAndPins*(self: Chessboard) func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) @@ -148,13 +148,13 @@ proc newChessboardFromFEN*(fen: string): Chessboard = of '-': discard of 'K': - discard + result.position.castlingAvailability.white.king = true of 'Q': - discard + result.position.castlingAvailability.white.queen = true of 'k': - discard + result.position.castlingAvailability.black.king = true of 'q': - discard + result.position.castlingAvailability.black.queen = true else: raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section") of 3: @@ -187,6 +187,7 @@ proc newChessboardFromFEN*(fen: string): Chessboard = else: raise newException(ValueError, "invalid FEN: too many fields in FEN string") inc(index) + result.updateChecksAndPins() proc newDefaultChessboard*: Chessboard {.inline.} = @@ -233,8 +234,7 @@ func getOccupancy(self: Chessboard): Bitboard {.inline.} = func getPawnAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = - ## Returns the attack bitboard for the given square from - ## the pawns of the given side + ## Returns the locations of the pawns attacking the given square let sq = square.toBitboard() pawns = self.getBitboard(Pawn, attacker) @@ -244,8 +244,7 @@ func getPawnAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bit func getKingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = - ## Returns the attack bitboard for the given square from - ## the king of the given side + ## Returns the location of the king if it is attacking the given square result = Bitboard(0) let king = self.getBitboard(King, attacker) @@ -254,8 +253,7 @@ func getKingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bit func getKnightAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = - ## Returns the attack bitboard for the given square from - ## the knights of the given side + ## Returns the locations of the knights attacking the given square let knights = self.getBitboard(Knight, attacker) result = Bitboard(0) @@ -266,24 +264,27 @@ func getKnightAttacks(self: Chessboard, square: Square, attacker: PieceColor): B proc getSlidingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = - ## Returns the attack bitboard for the given square from - ## the sliding pieces of the given side + ## Returns the locations of the sliding pieces attacking the given square let queens = self.getBitboard(Queen, attacker) rooks = self.getBitboard(Rook, attacker) or queens bishops = self.getBitboard(Bishop, attacker) or queens + occupancy = self.getOccupancy() + squareBB = square.toBitboard() result = Bitboard(0) for rook in rooks: - let blockers = Rook.getRelevantBlockers(square) - let rookBB = rook.toBitboard() - if (getRookMoves(square, blockers) and rookBB) != 0: - result = result or rookBB + let + blockers = occupancy and Rook.getRelevantBlockers(rook) + moves = getRookMoves(rook, blockers) + # Attack set intersects our chosen square + if (moves and squareBB) != 0: + result = result or rook.toBitboard() for bishop in bishops: let - blockers = Bishop.getRelevantBlockers(square) - bishopBB = bishop.toBitboard() - if (getBishopMoves(square, blockers) and bishopBB) != 0: - result = result or bishopBB + blockers = occupancy and Bishop.getRelevantBlockers(bishop) + moves = getBishopMoves(bishop, blockers) + if (moves and squareBB) != 0: + result = result or bishop.toBitboard() proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = @@ -296,7 +297,53 @@ proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitb result = result or self.getSlidingAttacks(square, attacker) -proc updateChecksAndPins(self: Chessboard) = +proc isOccupancyAttacked*(self: Chessboard, square: Square, occupancy: Bitboard): bool = + ## Returns whether the given square would be attacked by the + ## enemy side if the board had the given occupancy. This function + ## is necessary mostly to make sure sliding attacks can check the + ## king properly: due to how we generate our attack bitboards, if + ## the king moved backwards along a ray from a slider we would not + ## consider it to be in check (because the ray stops at the first + ## blocker). In order to fix that, in generateKingMoves() we use this + ## function and pass in the board's occupancy without the moving king so + ## that we can pick the correct magic bitboard and ray. Also, since this + ## function doesn't need to generate all the attacks to know whether a + ## given square is unsafe, it can short circuit at the first attack and + ## exit early, unlike getAttacksTo + let + sideToMove = self.position.sideToMove + nonSideToMove = sideToMove.opposite() + knights = self.getBitboard(Knight, nonSideToMove) + + # Let's do the cheap ones first (the ones which are precomputed) + if (getKnightAttacks(square) and knights) != 0: + return true + + let king = self.getBitboard(King, nonSideToMove) + + if (getKingAttacks(square) and king) != 0: + return true + + let + queens = self.getBitboard(Queen, nonSideToMove) + bishops = self.getBitboard(Bishop, nonSideToMove) or queens + + if (getBishopMoves(square, occupancy) and bishops) != 0: + return true + + let rooks = self.getBitboard(Rook, nonSideToMove) or queens + + if (getRookMoves(square, occupancy) and rooks) != 0: + return true + + # TODO: Precompute pawn moves as well? + let pawns = self.getBitboard(Pawn, nonSideToMove) + + if (self.getPawnAttacks(square, nonSideToMove) and pawns) != 0: + return true + + +proc updateChecksAndPins*(self: Chessboard) = ## Updates internal metadata about checks and ## pinned pieces @@ -305,8 +352,7 @@ proc updateChecksAndPins(self: Chessboard) = let sideToMove = self.position.sideToMove nonSideToMove = sideToMove.opposite() - friendlyKingBB = self.getBitboard(King, sideToMove) - friendlyKing = friendlyKingBB.toSquare() + friendlyKing = self.getBitboard(King, sideToMove).toSquare() friendlyPieces = self.getOccupancyFor(sideToMove) enemyPieces = self.getOccupancyFor(nonSideToMove) @@ -325,14 +371,15 @@ proc updateChecksAndPins(self: Chessboard) = for piece in canPinDiagonally: let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() - if (pinningRay and friendlyKingBB).countSquares() > 0: + # Is the pinning ray obstructed by any of our friendly pieces? If so, the + # piece is pinned + if (pinningRay and friendlyPieces).countSquares() > 0: self.position.diagonalPins = self.position.diagonalPins or pinningRay for piece in canPinOrthogonally: let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() - - if (pinningRay and friendlyKingBB).countSquares() > 0: - self.position.diagonalPins = self.position.diagonalPins or pinningRay + if (pinningRay and friendlyPieces).countSquares() > 0: + self.position.orthogonalPins = self.position.orthogonalPins or pinningRay func inCheck(self: Chessboard): bool {.inline.} = @@ -502,85 +549,49 @@ proc unmakeMove*(self: Chessboard) = self.update() -proc generatePawnMovements(self: Chessboard, moves: var MoveList) = - ## Helper of generatePawnMoves for generating all non-capture - ## and non-promotion pawn moves +proc generatePawnMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = let sideToMove = self.position.sideToMove pawns = self.getBitboard(Pawn, sideToMove) - # We can only move to squares that are *not* occupied by another piece. - # We also cannot move to the last rank, as that will result in a promotion - # and is handled elsewhere - allowedSquares = not (self.getOccupancy() or sideToMove.getLastRank()) - # Single push - for square in pawns.forwardRelativeTo(sideToMove) and allowedSquares: - moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square)) - # Double push - let rank = if sideToMove == White: getRankMask(6) else: getRankMask(1) # Only pawns on their starting rank can double push - for square in (pawns and rank).doubleForwardRelativeTo(sideToMove) and allowedSquares: - moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush)) - - -proc generatePawnCaptures(self: Chessboard, moves: var MoveList) = - ## Helper of generatePawnMoves for generating all capturing - ## moves - 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(nonSideToMove) and not self.getBitboard(King, nonSideToMove) - enemyPawns = self.getBitboard(Pawn, nonSideToMove) + enemyPieces = self.getOccupancyFor(sideToMove.opposite()) + # We can only capture diagonally and forward rightMovement = pawns.forwardRightRelativeTo(sideToMove) leftMovement = pawns.forwardLeftRelativeTo(sideToMove) epTarget = self.position.enPassantSquare - var epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0) - # TODO: Remove this. Seems like we're not keeping track of en passant targets properly and - # trying to do en passant on top of a piece - epBitboard = epBitboard and not occupancy - # Top right attacks - for square in rightMovement and enemyPieces: - moves.add(createMove(square.toBitboard().backwardLeftRelativeTo(sideToMove), square, Capture)) - # Top left attacks - for square in leftMovement and enemyPieces: - moves.add(createMove(square.toBitboard().backwardRightRelativeTo(sideToMove), square, Capture)) - # Special case for en passant + checkers = self.position.checkers + diagonalPins = self.position.diagonalPins + orthogonalPins = self.position.orthogonalPins + promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7) + # The rank where each color's side starts + # TODO: Give names to ranks and files so we don't have to assume a + # specific board layout when calling get(Rank|File)Mask + startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1) + + var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0) + let epPawn = if epBitboard == 0: Bitboard(0) else: epBitboard.forwardRelativeTo(sideToMove) + # If we are in check, en passant is only possible if we'd capture the (only) + # checking pawn with it + if epBitboard != 0 and self.inCheck() and (epPawn and checkers).countSquares() == 0: + epBitboard = Bitboard(0) + + # Single and double pushes let - epLeft = epBitboard and leftMovement - epRight = epBitboard and rightMovement - if epLeft != 0: - moves.add(createMove(epBitboard.forwardLeftRelativeTo(nonSideToMove), epBitboard, EnPassant)) - elif epRight != 0: - moves.add(createMove(epBitboard.forwardRightRelativeTo(nonSideToMove), epBitboard, EnPassant)) + # If a pawn is pinned diagonally, it cannot move + pushablePawns = pawns and not diagonalPins + # Neither can it move if it's pinned orthogonally + singlePushes = pushablePawns.forwardRelativeTo(sideToMove) and not occupancy and not orthogonalPins + # Only pawns on their starting rank can double push + doublePushes = (pushablePawns and startingRank).doubleForwardRelativeTo(sideToMove) and not occupancy and orthogonalPins -proc generatePawnPromotions(self: Chessboard, moves: var MoveList) = - ## Helper of generatePawnMoves for generating all promotion - ## moves - let - sideToMove = self.position.sideToMove - pawns = self.getBitboard(Pawn, sideToMove) - occupancy = self.getOccupancy() - for square in pawns.forwardRelativeTo(sideToMove) and not occupancy and sideToMove.getLastRank(): - for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: - moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square, promotion)) - - -proc generatePawnMoves(self: Chessboard, moves: var MoveList) = - ## Generates all the legal pawn moves for the side to move - self.generatePawnMovements(moves) - self.generatePawnCaptures(moves) - self.generatePawnPromotions(moves) - - -proc generateRookMoves(self: Chessboard, moves: var MoveList) = +proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = ## Helper of generateSlidingMoves to generate rook moves let sideToMove = self.position.sideToMove occupancy = self.getOccupancy() - nonSideToMove = sideToMove.opposite() - enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, nonSideToMove) + enemyPieces = self.getOccupancyFor(sideToMove.opposite()) rooks = self.getBitboard(Rook, sideToMove) queens = self.getBitboard(Queen, sideToMove) movableRooks = not self.position.diagonalPins and (queens or rooks) @@ -591,27 +602,26 @@ proc generateRookMoves(self: Chessboard, moves: var MoveList) = let blockers = occupancy and Rook.getRelevantBlockers(square) moveset = getRookMoves(square, blockers) - for target in moveset and not occupancy and pinMask: + for target in moveset and not occupancy and pinMask and mask: moves.add(createMove(square, target)) - for target in moveset and enemyPieces and pinMask: + for target in moveset and enemyPieces and pinMask and mask: moves.add(createMove(square, target, Capture)) for square in unpinnedRooks: let blockers = occupancy and Rook.getRelevantBlockers(square) moveset = getRookMoves(square, blockers) - for target in moveset and not occupancy: + for target in moveset and not occupancy and mask: moves.add(createMove(square, target)) - for target in moveset and enemyPieces: + for target in moveset and enemyPieces and mask: moves.add(createMove(square, target, Capture)) -proc generateBishopMoves(self: Chessboard, moves: var MoveList) = +proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = ## Helper of generateSlidingMoves to generate bishop moves let sideToMove = self.position.sideToMove occupancy = self.getOccupancy() - nonSideToMove = sideToMove.opposite() - enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, nonSideToMove) + enemyPieces = self.getOccupancyFor(sideToMove.opposite()) bishops = self.getBitboard(Bishop, sideToMove) queens = self.getBitboard(Queen, sideToMove) movableBishops = not self.position.orthogonalPins and (queens or bishops) @@ -622,27 +632,20 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList) = let blockers = occupancy and Bishop.getRelevantBlockers(square) moveset = getBishopMoves(square, blockers) - for target in moveset and pinMask and not occupancy: + for target in moveset and pinMask and mask: moves.add(createMove(square, target)) - for target in moveset and enemyPieces and pinMask: + for target in moveset and enemyPieces and pinMask and mask: moves.add(createMove(square, target, Capture)) for square in unpinnedBishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) moveset = getBishopMoves(square, blockers) - for target in moveset and not occupancy: + for target in moveset and mask: moves.add(createMove(square, target)) - for target in moveset and enemyPieces: + for target in moveset and enemyPieces and mask: moves.add(createMove(square, target, Capture)) -proc generateSlidingMoves(self: Chessboard, moves: var MoveList) = - ## Generates all legal sliding moves for the side to move - self.generateRookMoves(moves) - self.generateBishopMoves(moves) - # Queens are just handled rooks + bishops - - proc generateKingMoves(self: Chessboard, moves: var MoveList) = ## Generates all legal king moves for the side to move let @@ -652,25 +655,27 @@ proc generateKingMoves(self: Chessboard, moves: var MoveList) = nonSideToMove = sideToMove.opposite() enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) bitboard = getKingAttacks(king.toSquare()) + noKingOccupancy = occupancy and not king for square in bitboard and not occupancy: - moves.add(createMove(king, square)) + if not self.isOccupancyAttacked(square, noKingOccupancy): + moves.add(createMove(king, square)) for square in bitboard and enemyPieces: - moves.add(createMove(king, square, Capture)) + if not self.isOccupancyAttacked(square, noKingOccupancy): + moves.add(createMove(king, square, Capture)) -proc generateKnightMoves(self: Chessboard, moves: var MoveList)= +proc generateKnightMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = ## Generates all the legal knight moves for the side to move let sideToMove = self.position.sideToMove knights = self.getBitboard(Knight, sideToMove) - occupancy = self.getOccupancy() nonSideToMove = sideToMove.opposite() pinned = self.position.diagonalPins or self.position.orthogonalPins unpinnedKnights = knights and not pinned enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) for square in unpinnedKnights: let bitboard = getKnightAttacks(square) - for target in bitboard and not occupancy: + for target in bitboard and mask: moves.add(createMove(square, target)) for target in bitboard and enemyPieces: moves.add(createMove(square, target, Capture)) @@ -682,6 +687,7 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = if self.position.halfMoveClock >= 100: # Draw by 50-move rule return + let sideToMove = self.position.sideToMove # TODO: Check for draw by insufficient material # TODO: Check for repetitions (requires zobrist hashing + table) self.generateKingMoves(moves) @@ -692,10 +698,32 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = if not self.inCheck(): # TODO: Castling discard + + # We pass a mask to our move generators to remove stuff + # like our friendly pieces from the set of possible + # target squares, as well as to ensure checks are not + # ignored - self.generatePawnMoves(moves) - self.generateKnightMoves(moves) - self.generateSlidingMoves(moves) + var mask: Bitboard + if not self.inCheck(): + # Not in check: cannot move over friendly pieces + mask = not self.getOccupancyFor(sideToMove) + else: + # We *are* in check (from a single piece, because the two checks + # case was handled above already). If the piece is a slider, we'll + # extract the ray from it to our king and add the checking piece to + # 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() + mask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard() + + self.generatePawnMoves(moves, mask) + self.generateKnightMoves(moves, mask) + self.generateRookMoves(moves, mask) + self.generateBishopMoves(moves, mask) + # Queens are just handled rooks + bishops proc isLegal(self: Chessboard, move: Move): bool {.inline.} = @@ -863,20 +891,19 @@ proc toFEN*(self: Chessboard): string = result &= (if self.position.sideToMove == White: "w" else: "b") result &= " " # Castling availability - result &= "-" - # let castleWhite = self.position.castlingRightsAvailable.white - # let castleBlack = self.position.castlingRightsAvailable.black - # if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): - # result &= "-" - # else: - # if castleWhite.king: - # result &= "K" - # if castleWhite.queen: - # result &= "Q" - # if castleBlack.king: - # result &= "k" - # if castleBlack.queen: - # result &= "q" + let castleWhite = self.position.castlingAvailability.white + let castleBlack = self.position.castlingAvailability.black + if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): + result &= "-" + else: + if castleWhite.king: + result &= "K" + if castleWhite.queen: + result &= "Q" + if castleBlack.king: + result &= "k" + if castleBlack.queen: + result &= "q" result &= " " # En passant target if self.position.enPassantSquare == nullSquare(): diff --git a/Chess/nimfish/nimfishpkg/bitboards.nim b/Chess/nimfish/nimfishpkg/bitboards.nim index 6fd7f89..21e45cf 100644 --- a/Chess/nimfish/nimfishpkg/bitboards.nim +++ b/Chess/nimfish/nimfishpkg/bitboards.nim @@ -61,6 +61,12 @@ func countSquares*(self: Bitboard): int {.inline.} = result = self.countSetBits() +func lowestSquare*(self: Bitboard): Square {.inline.} = + ## Returns the index of the lowest one bit + ## in the given bitboard as a square + result = Square(self.countTrailingZeroBits().uint8) + + func getFileMask*(file: int): Bitboard = Bitboard(0x101010101010101'u64) shl file.uint64 func getRankMask*(rank: int): Bitboard = Bitboard(0xff) shl uint64(8 * rank) func toBitboard*(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64 diff --git a/Chess/nimfish/nimfishpkg/position.nim b/Chess/nimfish/nimfishpkg/position.nim index 45fd891..00aa008 100644 --- a/Chess/nimfish/nimfishpkg/position.nim +++ b/Chess/nimfish/nimfishpkg/position.nim @@ -11,6 +11,11 @@ type # Castling metadata. Updated on every move castlingRights*: array[64, uint8] + # Castling availability. This just keeps track + # of whether the king or the rooks on either side + # moved and is only useful for printing the correct + # FEN for a given position + castlingAvailability*: tuple[white, black: tuple[queen, king: bool]] # Number of half-moves that were performed # to reach this position starting from the # root of the tree diff --git a/Chess/nimfish/nimfishpkg/tui.nim b/Chess/nimfish/nimfishpkg/tui.nim index a70f885..e373f81 100644 --- a/Chess/nimfish/nimfishpkg/tui.nim +++ b/Chess/nimfish/nimfishpkg/tui.nim @@ -345,6 +345,8 @@ const HELP_TEXT = """Nimfish help menu: - pos : Shorthand for "position " - get : Get the piece on the given square - atk : Print the attack bitboard of the given square for the side to move + - pins: Print the current pin mask + - checks: Print the current checks mask - skip: Swap the side to move - uci: enter UCI mode (WIP) - quit: exit @@ -380,6 +382,7 @@ proc commandLoop*: int = echo HELP_TEXT of "skip": board.position.sideToMove = board.position.sideToMove.opposite() + board.updateChecksAndPins() of "go": handleGoCommand(board, cmd) of "position", "pos": @@ -389,7 +392,10 @@ proc commandLoop*: int = of "pretty", "print", "fen": handlePositionCommand(board, @["position", cmd[0]]) of "unmove", "u": - board.unmakeMove() + if board.positions.len() == 0: + echo "No previous move to undo" + else: + board.unmakeMove() of "stm": echo &"Side to move: {board.position.sideToMove}" of "atk": @@ -421,6 +427,11 @@ proc commandLoop*: int = echo &"Castling rights for {($board.position.sideToMove).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" of "check": echo &"{board.position.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}" + of "pins": + echo &"Ortogonal pins:\n{board.position.orthogonalPins}" + echo &"Diagonal pins:\n{board.position.diagonalPins}" + of "checks": + echo board.position.checkers of "quit": return 0 else: diff --git a/Chess/tests/compare_positions.py b/Chess/tests/compare_positions.py index 274351f..59e4d7c 100644 --- a/Chess/tests/compare_positions.py +++ b/Chess/tests/compare_positions.py @@ -10,6 +10,8 @@ from argparse import ArgumentParser, Namespace def main(args: Namespace) -> int: if args.silent: print = lambda *_: ... + else: + print = __builtins__.print print("Nimfish move validator v0.0.1 by nocturn9x") try: STOCKFISH = (args.stockfish or Path(which("stockfish"))).resolve(strict=True)