From 6e10cbe925cc6133251280a2e931e7ef17cb7a3b Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Wed, 1 Nov 2023 19:07:09 +0100 Subject: [PATCH] Bug fixes(?) --- src/Chess/board.nim | 216 +++++++++++++++------------------ src/Chess/compare_positions.py | 7 +- 2 files changed, 102 insertions(+), 121 deletions(-) diff --git a/src/Chess/board.nim b/src/Chess/board.nim index cde708d..0dbc326 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -99,30 +99,19 @@ type attacked: tuple[white: Attacked, black: Attacked] # Pieces pinned by both sides pinned: tuple[white: Attacked, black: Attacked] - # The original piece captured to reach this position (may be empty) - #captured: Piece - # The piece that moved to reach this position (needed to undo moves) - #moved: Piece # Active color turn: PieceColor - CacheEntry[T] = ref object - valid: bool - data: T - - Cache = object - canCastle: tuple[white, black: CacheEntry[tuple[queen, king: bool]]] - inCheck: tuple[white, black: CacheEntry[bool]] - ChessBoard* = ref object ## A chess board object + + # An 8x8 matrix we use for constant + # time lookup of pieces by their location grid: Matrix[Piece] + # The current position position: Position - # List of reached positions + # List of all reached positions positions: seq[Position] - # Cached results of expensive - # functions in the current position - #cache: Cache # Initialized only once, copied every time @@ -131,6 +120,8 @@ for _ in countup(0, 63): empty.add(Piece(kind: Empty, color: None)) +# A bunch of simple utility functions + func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) func emptyLocation*: Location {.inline.} = (-1 , -1) func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) @@ -153,7 +144,6 @@ proc updateAttackedSquares(self: ChessBoard) proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] proc getAttacks*(self: ChessBoard, loc: Location): Attacked proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] -#[func invalidateCache(self: ChessBoard) {.inline.}]# proc inCheck*(self: ChessBoard, color: PieceColor = None): bool @@ -162,9 +152,8 @@ proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = self.add(x) -# Due to our board layout, directions of movement are reversed for white/black so -# we need these helpers to avoid going mad with integer tuples and minus signs -# everywhere +# Due to our board layout, directions of movement are reversed for white and black, so +# we need these helpers to avoid going mad with integer tuples and minus signs everywhere func topLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, -1) else: (1, 1)) func topRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (-1, 1) else: (1, -1)) func bottomLeftDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, -1) else: (-1, 1)) @@ -173,16 +162,11 @@ func leftSide(color: PieceColor): Location {.inline.} = (if color == White: (0, func rightSide(color: PieceColor): Location {.inline.} = (if color == White: (0, 1) else: (0, -1)) func topSide(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0)) func bottomSide(color: PieceColor): Location {.inline.} = (if color == White: (1, 0) else: (-1, 0)) -func forward(color: PieceColor): Location {.inline.} = (if color == White: (-1, 0) else: (1, 0)) func doublePush(color: PieceColor): Location {.inline.} = (if color == White: (-2, 0) else: (2, 0)) func longCastleKing: Location {.inline.} = (0, -2) func shortCastleKing: Location {.inline.} = (0, 2) func longCastleRook: Location {.inline.} = (0, 3) func shortCastleRook: Location {.inline.} = (0, -2) -func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7)) -func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0)) - - func bottomLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = if color == White: if long: @@ -234,7 +218,12 @@ func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline else: return (-1, 2) +# These return absolute locations rather than relative direction offsets +func kingSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 7) else: (0, 7)) +func queenSideRook(color: PieceColor): Location {.inline.} = (if color == White: (7, 0) else: (0, 0)) + +# A bunch of getters func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = ## Returns the currently active color ## (turn of who has to move) @@ -296,8 +285,6 @@ proc newChessboard: ChessBoard = new(result) # Turns our flat sequence into an 8x8 grid result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) - #result.cache = Cache(canCastle: (white: CacheEntry[tuple[queen, king: bool]](), black: CacheEntry[tuple[queen, king: bool]]()), - # inCheck: (white: CacheEntry[bool](), black: CacheEntry[bool]())) result.position = Position(attacked: (@[], @[]), enPassantSquare: emptyLocation(), move: emptyMove(), @@ -318,9 +305,9 @@ proc newChessboard: ChessBoard = -proc newChessboardFromFEN*(state: string): ChessBoard = +proc newChessboardFromFEN*(fen: string): ChessBoard = ## Initializes a chessboard with the - ## state encoded by the given FEN string + ## position encoded by the given FEN string result = newChessboard() var # Current location in the grid @@ -330,11 +317,11 @@ proc newChessboardFromFEN*(state: string): ChessBoard = section = 0 # Current index into the FEN string index = 0 - # Temporary variable to store the piece + # Temporary variable to store a piece piece: Piece # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation - while index <= state.high(): - var c = state[index] + while index <= fen.high(): + var c = fen[index] if c == ' ': # Next section inc(section) @@ -366,6 +353,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard = of Queen: result.position.pieces.black.queens.add((row, column)) of King: + if result.position.pieces.black.king != emptyLocation(): + raise newException(ValueError, "invalid position: exactly one king of each color must be present") result.position.pieces.black.king = (row, column) else: discard @@ -382,6 +371,8 @@ proc newChessboardFromFEN*(state: string): ChessBoard = of Queen: result.position.pieces.white.queens.add((row, column)) of King: + if result.position.pieces.white.king != emptyLocation(): + raise newException(ValueError, "invalid position: exactly one king of each color must be present") result.position.pieces.white.king = (row, column) else: discard @@ -397,10 +388,10 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # Skip x columns let x = int(uint8(c) - uint8('0')) if x > 8: - raise newException(ValueError, "invalid skip value (> 8) in FEN string") + raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)") column += int8(x) else: - raise newException(ValueError, "invalid piece identifier in FEN string") + raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'") of 1: # Active color case c: @@ -409,7 +400,7 @@ proc newChessboardFromFEN*(state: string): ChessBoard = of 'b': result.position.turn = Black else: - raise newException(ValueError, "invalid active color identifier in FEN string") + raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'") of 2: # Castling availability case c: @@ -427,7 +418,7 @@ proc newChessboardFromFEN*(state: string): ChessBoard = of 'q': result.position.castlingAvailable.black.queen = true else: - raise newException(ValueError, "invalid castling availability in FEN string") + raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castling availability section") of 3: # En passant target square case c: @@ -435,14 +426,14 @@ proc newChessboardFromFEN*(state: string): ChessBoard = # Field is already uninitialized to the correct state discard else: - result.position.enPassantSquare = state[index..index+1].algebraicToLocation() + result.position.enPassantSquare = fen[index..index+1].algebraicToLocation() # Square metadata is 2 bytes long inc(index) of 4: # Halfmove clock var s = "" - while not state[index].isSpaceAscii(): - s.add(state[index]) + while not fen[index].isSpaceAscii(): + s.add(fen[index]) inc(index) # Backtrack so the space is seen by the # next iteration of the loop @@ -451,17 +442,20 @@ proc newChessboardFromFEN*(state: string): ChessBoard = of 5: # Fullmove number var s = "" - while index <= state.high(): - s.add(state[index]) + while index <= fen.high(): + s.add(fen[index]) inc(index) result.position.fullMoveCount = parseInt(s).int8 else: - raise newException(ValueError, "too many fields in FEN string") + raise newException(ValueError, "invalid FEN: too many fields in FEN string") inc(index) result.updateAttackedSquares() if result.inCheck(result.getActiveColor().opposite): - # Opponent king cannot be captured! + # Opponent king cannot be captured on the next move raise newException(ValueError, "invalid position: opponent king can be captured") + if result.position.pieces.white.king == emptyLocation() or result.position.pieces.black.king == emptyLocation(): + # Both kings must be on the board + raise newException(ValueError, "invalid position: exactly one king of each color must be present") proc newDefaultChessboard*: ChessBoard {.inline.} = @@ -588,26 +582,12 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = color = self.getActiveColor() case color: of White: - #[if self.cache.inCheck.white.valid: - return self.cache.inCheck.white.data]# result = self.isAttacked(self.position.pieces.white.king, Black) of Black: - #[if self.cache.inCheck.black.valid: - return self.cache.inCheck.black.data]# result = self.isAttacked(self.position.pieces.black.king, White) else: # Unreachable discard - #[case color: - of White: - self.cache.inCheck.white.valid = true - self.cache.inCheck.white.data = result - of Black: - self.cache.inCheck.black.valid = true - self.cache.inCheck.black.data = result - else: - # Unreachable - discard]# proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} = @@ -617,17 +597,12 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: var color = color if color == None: color = self.getActiveColor() - # If the rooks or king have been moved, castling - # rights have been lost + # Check if castling rights are still available for moving side case color: of White: - #[if self.cache.canCastle.white.valid: - return self.cache.canCastle.white.data]# result.king = self.position.castlingAvailable.white.king result.queen = self.position.castlingAvailable.white.queen of Black: - #[if self.cache.canCastle.black.valid: - return self.cache.canCastle.black.data#]# result.king = self.position.castlingAvailable.black.king result.queen = self.position.castlingAvailable.black.queen of None: @@ -641,9 +616,9 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: loc: Location queenSide: Location kingSide: Location - # If the path between the king and rook on a given side is blocked or any of the - # squares where the king would travel to are attacked by the opponent, - # then castling is (temporarily) prohibited on that side + # If the path between the king and rook on a given side is blocked, or any of the + # squares where the king would move to are attacked by the opponent, then castling + # is temporarily prohibited on that side case color: of White: loc = self.position.pieces.white.king @@ -662,42 +637,53 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: var location = loc otherPiece: Piece - while location != loc + shortCastleKing(): + moveKing: bool = true + while true: location = location + kingSide + if location == color.kingSideRook(): + # No need to do any extra checks: if the piece + # on this square were not a rook of the same color + # as the castling king, then we wouldn't have gotten + # here in the first place (it would've had to be either + # moved or captured, and both of those actions are detected + # and accounted for way before this point) + break + if location == loc + shortCastleKing(): + moveKing = false otherPiece = self.grid[location.row, location.col] - if otherPiece.color != None or self.isAttacked(location, color.opposite()): + if otherPiece.color != None: + result.king = false + break + if moveKing and self.isAttacked(location, color.opposite()): result.king = false break - if result.queen: # Long castle var location = loc otherPiece: Piece - while location != loc + longCastleKing(): + moveKing: bool = true + while true: location = location + queenSide + if location == color.queenSideRook(): + break + if location == loc + longCastleKing(): + moveKing = false otherPiece = self.grid[location.row, location.col] - if otherPiece.color != None or self.isAttacked(location, color.opposite()): + if otherPiece.color != None: + result.queen = false + break + if moveKing and self.isAttacked(location, color.opposite()): result.queen = false break - - #[case color: - of White: - self.cache.canCastle.white.data = result - self.cache.canCastle.white.valid = true - of Black: - self.cache.canCastle.black.data = result - self.cache.canCastle.black.valid = true - else: - discard]# proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = ## Returns the squares that need to be covered to - ## resolve the current check (includes capturing + ## resolve the current check (including capturing ## the checking piece). In case of double check, an ## empty list is returned (as the king must move) var king: Location @@ -707,7 +693,7 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = of Black: king = self.position.pieces.black.king else: - return @[] + return let attackers: seq[Location] = self.getAttackers(king, color.opposite()) if attackers.len() > 1: @@ -722,14 +708,14 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = # Blocking the attack is also a viable strategy # (unless the check is from a knight or a pawn, # in which case either the king has to move or - # that piece has to be captured) - if attackerPiece.kind notin [Knight, Pawn]: - var location = attacker - while location != king: - location = location + attack.direction - if not location.isValid(): - break - result.add(location) + # that piece has to be captured, but this is + # already implicitly handled by the loop below) + var location = attacker + while location != king: + location = location + attack.direction + if not location.isValid(): + break + result.add(location) func getKing(self: ChessBoard, color: PieceColor): Location {.inline.} = @@ -754,7 +740,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = flags: seq[MoveFlag] = @[] doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" # Pawns can move forward one square - let forward = (piece.color.forward() + location) + let forward = (piece.color.topSide() + location) # Only if the square is empty though if forward.isValid() and self.grid[forward.row, forward.col].color == None: locations.add(forward) @@ -766,13 +752,12 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None: locations.add(double) flags.add(DoublePush) - let enPassantPiece = self.getEnPassantTarget() + piece.color.opposite().topSide() - let enPassantPawn = self.grid[enPassantPiece.row, enPassantPiece.col] + let enPassantPawn = self.getEnPassantTarget() + piece.color.opposite().topSide() # They can also move on either diagonal one # square, but only to capture or for en passant for diagonal in [location + piece.color.topRightDiagonal(), location + piece.color.topLeftDiagonal()]: if diagonal.isValid(): - if enPassantPawn.color == piece.color.opposite() and diagonal == self.position.enPassantSquare: + if diagonal == self.position.enPassantSquare and self.grid[enPassantPawn.row, enPassantPawn.col].color == self.getActiveColor().opposite(): # Ensure en passant doesn't create a check let king = self.getKing(piece.color) var ok = true @@ -1253,9 +1238,6 @@ proc updateAttackedSquares(self: ChessBoard) = self.updateKnightAttacks() # Kings self.updateKingAttacks() - # Invalidate the cache whenever updates to the - # metadata are made - #self.invalidateCache() proc removePiece(self: ChessBoard, location: Location, attack: bool = true) = @@ -1361,9 +1343,7 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = self.grid[move.targetSquare.row, move.targetSquare.col] = piece if attack: self.updateAttackedSquares() - #else: - # Just to be sure - # self.invalidateCache() + proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) = @@ -1371,38 +1351,42 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bo self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack) -#[func invalidateCache(self: ChessBoard) {.inline.} = - ## Invalidates the internal caches - self.cache.canCastle.white.valid = false - self.cache.canCastle.black.valid = false - self.cache.inCheck.white.valid = false - self.cache.inCheck.black.valid = false]# proc doMove(self: ChessBoard, move: Move) = ## Internal function called by makeMove after - ## performing legality checks on the given move. Can - ## be used in performance-critical paths where - ## a move is already known to be legal + ## performing legality checks. Can be used in + ## performance-critical paths where a move is + ## already known to be legal - # Record final position for future reference self.positions.add(self.position) # Final checks let piece = self.grid[move.startSquare.row, move.startSquare.col] - # Needed to detect draw by the 50 move rule var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable + enPassantTarget = self.getEnPassantTarget() + # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.flag == Capture: halfMoveClock = 0 else: inc(halfMoveClock) if piece.color == Black: inc(fullMoveCount) + + # En passant check + if enPassantTarget != emptyLocation(): + let enPassantPawn = enPassantTarget + piece.color.topSide() + if self.grid[enPassantPawn.row, enPassantPawn.col].color == piece.color.opposite(): + enPassantTarget = emptyLocation() + + if move.flag == DoublePush: + enPassantTarget = move.targetSquare + piece.color.bottomSide() + # Castling check: have the rooks moved? if piece.kind == Rook: case piece.color: @@ -1462,12 +1446,11 @@ proc doMove(self: ChessBoard, move: Move) = self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, - #captured: if move.flag == Capture: self.grid[move.targetSquare.row, move.targetSquare.col] else: emptyPiece(), turn: self.getActiveColor().opposite, castlingAvailable: castlingAvailable, move: move, pieces: self.position.pieces, - enPassantSquare: if move.flag == EnPassant: move.targetSquare + piece.color.bottomSide() else: emptyLocation() + enPassantSquare: enPassantTarget ) # Update position metadata @@ -1489,7 +1472,6 @@ proc doMove(self: ChessBoard, move: Move) = if move.flag == Capture: # Get rid of captured pieces - #self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col] self.removePiece(move.targetSquare, attack=false) if move.flag == EnPassant: @@ -1979,8 +1961,4 @@ when isMainModule: testPiece(b.getPiece("d8"), Queen, Black) setControlCHook(proc () {.noconv.} = quit(0)) - - #b = newChessboardFromFEN("r3k2r/Pppp1ppp/1b3nbN/nP6/BBPPP3/q4N2/Pp4PP/R2Q1RK1 b kq d3 0 1") - #let m = Move(startSquare: "c7".algebraicToLocation, targetSquare: "c5".algebraicToLocation, flag: DoublePush) - #b.makeMove() quit(main()) \ No newline at end of file diff --git a/src/Chess/compare_positions.py b/src/Chess/compare_positions.py index 5aae122..479d480 100644 --- a/src/Chess/compare_positions.py +++ b/src/Chess/compare_positions.py @@ -62,7 +62,10 @@ def main(args: Namespace) -> int: positions["stockfish"][move] = int(nodes) for (source, target, promotion, nodes) in pattern.findall(nimfish_output): move = f"{source}{target}{promotion}" - positions["all"][move].append(int(nodes)) + if move in positions["all"]: + positions["all"][move].append(int(nodes)) + else: + positions["all"][move] = [int(nodes)] positions["nimfish"][move] = int(nodes) missing = { @@ -131,7 +134,7 @@ def main(args: Namespace) -> int: for move in missing["stockfish"]: print(f" - {move}: {positions['stockfish'][move]}") if missing["nimfish"]: - print(" Illegal moves generated: ") + print(" Illegal moves generated: ") for move in missing["nimfish"]: print(f" - {move}: {positions['nimfish'][move]}") if mistakes: