# Copyright 2023 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import ../util/matrix export matrix import std/strutils import std/strformat type # Useful type aliases Location* = tuple[row, col: int] Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], bishops: seq[Location], knights: seq[Location], pawns: seq[Location]] PieceColor* = enum ## A piece color enumeration None = 0, White, Black PieceKind* = enum ## A chess piece enumeration Empty = '\0', # No piece Bishop = 'b', King = 'k' Knight = 'n', Pawn = 'p', Queen = 'q', Rook = 'r', Piece* = object ## A chess piece color*: PieceColor kind*: PieceKind MoveFlag* = enum ## An enumeration of move flags Default, # Move is a regular move # Castling CastleLong, CastleShort, XRay, # Move is an X-ray attack # Move is a pawn promotion PromoteToQueen, PromoteToRook, PromoteToBishop, PromoteToKnight Move* = object ## A chess move piece*: Piece startSquare*: Location targetSquare*: Location flag*: MoveFlag Position* = ref object ## A chess position move: Move # Did the rooks on either side/the king move? castlingAvailable: 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 plyFromRoot: int # Number of half moves since # last piece capture or pawn movement. # Used for the 50-move rule halfMoveClock: int # Full move counter. Increments # every 2 ply fullMoveCount: int # En passant target square (see https://en.wikipedia.org/wiki/En_passant) # If en passant is not possible, both the row and # column of the position will be set to -1 enPassantSquare*: Move # Locations of all pieces pieces: tuple[white: Pieces, black: Pieces] # Potential attacking moves for black and white attacked: tuple[white: seq[Move], black: seq[Move]] # Has any piece been captured to reach this position? captured: Piece # Active color turn: PieceColor ChessBoard* = ref object ## A chess board object grid: Matrix[Piece] position: Position # List of reached positions positions: seq[Position] # Initialized only once, copied every time var empty: seq[Piece] = @[] for _ in countup(0, 63): empty.add(Piece(kind: Empty, color: None)) 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) proc algebraicToLocation*(s: string): Location {.inline.} func getCapture*(self: ChessBoard, move: Move): Location proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move proc makeMove*(self: ChessBoard, move: Move): Move proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col) func isValid*(a: Location): bool = a.row in 0..7 and a.col in 0..7 proc generateMoves(self: ChessBoard, location: Location): seq[Move] proc isAttacked*(self: ChessBoard, loc: Location): bool proc undoLastMove*(self: ChessBoard): Move proc isLegal(self: ChessBoard, move: Move): bool # 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 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)) func bottomRightDiagonal(color: PieceColor): Location {.inline.} = (if color == White: (1, 1) else: (-1, -1)) func leftSide(color: PieceColor): Location {.inline.} = (if color == White: (0, -1) else: (0, 1)) 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(color: PieceColor): Location {.inline.} = (0, -2) func shortCastleKing(color: PieceColor): Location {.inline.} = (0, 2) func longCastleRook(color: PieceColor): Location {.inline.} = (0, 3) func shortCastleRook(color: PieceColor): 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: return (-2, 1) else: return (1, -2) elif color == Black: if long: return (2, -1) else: return (1, -2) func bottomRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = if color == White: if long: return (2, -1) else: return (1, 2) elif color == Black: if long: return (2, 1) else: return (1, 2) func topLeftKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = if color == White: if long: return (-2, -1) else: return (-1, -2) elif color == Black: if long: return (2, 1) else: return (1, 2) func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline.} = if color == White: if long: return (-2, 1) else: return (-1, 2) elif color == Black: if long: return (2, -1) else: return (-1, 2) proc getActiveColor*(self: ChessBoard): PieceColor = ## Returns the currently active color ## (turn of who has to move) return self.position.turn proc getEnPassantTarget*(self: ChessBoard): Location = ## Returns the current en passant target square return self.position.enPassantSquare.targetSquare proc getMoveCount*(self: ChessBoard): int = ## Returns the number of full moves that ## have been played return self.position.fullMoveCount proc getHalfMoveCount*(self: ChessBoard): int = ## Returns the current number of half-moves ## since the last irreversible move return self.position.halfMoveClock func getStartRow(piece: Piece): int {.inline.} = ## Retrieves the starting row of ## the given piece inside our 8x8 ## grid case piece.color: of None: return -1 of White: case piece.kind: of Pawn: return 6 else: return 7 of Black: case piece.kind: of Pawn: return 1 else: return 0 func getLastRow(color: PieceColor): int {.inline.} = ## Retrieves the location of the last ## row relative to the given color case color: of White: return 0 of Black: return 7 else: return -1 proc newChessboard: ChessBoard = ## Returns a new, empty chessboard new(result) # Turns our flat sequence into an 8x8 grid result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) result.position = Position(attacked: (@[], @[]), enPassantSquare: emptyMove(), move: emptyMove(), turn: White) proc newChessboardFromFEN*(state: string): ChessBoard = ## Initializes a chessboard with the ## state encoded by the given FEN string result = newChessboard() var # Current location in the grid row = 0 column = 0 # Current section in the FEN string section = 0 # Current index into the FEN string index = 0 # Temporary variable to store the piece piece: Piece # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation while index <= state.high(): var c = state[index] if c == ' ': # Next section inc(section) inc(index) continue case section: of 0: # Piece placement data case c.toLowerAscii(): # Piece of 'r', 'n', 'b', 'q', 'k', 'p': # We know for a fact these values are in our # enumeration, so all is good {.push.} {.warning[HoleEnumConv]:off.} piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black) {.pop.} case piece.color: of Black: case piece.kind: of Pawn: result.position.pieces.black.pawns.add((row, column)) of Bishop: result.position.pieces.black.bishops.add((row, column)) of Knight: result.position.pieces.black.knights.add((row, column)) of Rook: result.position.pieces.black.rooks.add((row, column)) of Queen: result.position.pieces.black.queens.add((row, column)) of King: result.position.pieces.black.king = (row, column) else: discard of White: case piece.kind: of Pawn: result.position.pieces.white.pawns.add((row, column)) of Bishop: result.position.pieces.white.bishops.add((row, column)) of Knight: result.position.pieces.white.knights.add((row, column)) of Rook: result.position.pieces.white.rooks.add((row, column)) of Queen: result.position.pieces.white.queens.add((row, column)) of King: result.position.pieces.white.king = (row, column) else: discard else: discard result.grid[row, column] = piece inc(column) of '/': # Next row inc(row) column = 0 of '0'..'9': # Skip x columns let x = int(uint8(c) - uint8('0')) - 1 if x > 7: raise newException(ValueError, "invalid skip value (> 8) in FEN string") column += x else: raise newException(ValueError, "invalid piece identifier in FEN string") of 1: # Active color case c: of 'w': result.position.turn = White of 'b': result.position.turn = Black else: raise newException(ValueError, "invalid active color identifier in FEN string") of 2: # Castling availability case c: of '-': # Neither side can castle anywhere: do nothing, # as the castling metadata is set to this state # by default discard of 'K': result.position.castlingAvailable.white.king = true of 'Q': result.position.castlingAvailable.white.queen = true of 'k': result.position.castlingAvailable.black.king = true of 'q': result.position.castlingAvailable.black.queen = true else: raise newException(ValueError, "invalid castling availability in FEN string") of 3: # En passant target square case c: of '-': # Field is already uninitialized to the correct state discard else: result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToLocation() # Just for cleanliness purposes, we fill in the other metadata as # well result.position.enPassantSquare.piece.color = result.getActiveColor() result.position.enPassantSquare.piece.kind = Pawn # Square metadata is 2 bytes long inc(index) of 4: # Halfmove clock var s = "" while not state[index].isSpaceAscii(): s.add(state[index]) inc(index) # Backtrack so the space is seen by the # next iteration of the loop dec(index) result.position.halfMoveClock = parseInt(s) of 5: # Fullmove number var s = "" while index <= state.high(): s.add(state[index]) inc(index) result.position.fullMoveCount = parseInt(s) else: raise newException(ValueError, "too many fields in FEN string") inc(index) proc newDefaultChessboard*: ChessBoard = ## Initializes a chessboard with the ## starting position return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = ## Counts the number of pieces with ## the given color and type case color: of White: case kind: of Pawn: return self.position.pieces.white.pawns.len() of Bishop: return self.position.pieces.white.bishops.len() of Knight: return self.position.pieces.white.knights.len() of Rook: return self.position.pieces.white.rooks.len() of Queen: return self.position.pieces.white.queens.len() of King: # There shall be only one, forever return 1 else: discard of Black: case kind: of Pawn: return self.position.pieces.black.pawns.len() of Bishop: return self.position.pieces.black.bishops.len() of Knight: return self.position.pieces.black.knights.len() of Rook: return self.position.pieces.black.rooks.len() of Queen: return self.position.pieces.black.queens.len() of King: # In perpetuity return 1 else: discard of None: raise newException(ValueError, "invalid piece type") proc countPieces*(self: ChessBoard, piece: Piece): int = ## Returns the number of pieces on the board that ## are of the same type and color of the given piece return self.countPieces(piece.kind, piece.color) func rankToColumn(rank: int): int = ## Converts a chess rank (1-indexed) ## into a 0-indexed column value for our ## board. This converter is necessary because ## chess positions are indexed differently with ## respect to our internal representation const indeces = [7, 6, 5, 4, 3, 2, 1, 0] return indeces[rank - 1] func rowToRank(row: int): int = const indeces = [8, 7, 6, 5, 4, 3, 2, 1] return indeces[row] proc algebraicToLocation*(s: string): Location = ## Converts a square location from algebraic ## notation to its corresponding row and column ## in the chess grid (0 indexed) if len(s) != 2: raise newException(ValueError, "algebraic position must be of length 2") var s = s.toLowerAscii() if s[0] notin 'a'..'h': raise newException(ValueError, &"algebraic position has invalid first character ('{s[0]}')") if s[1] notin '1'..'8': raise newException(ValueError, &"algebraic position has invalid second character ('{s[1]}')") let rank = int(uint8(s[0]) - uint8('a')) # Convert the file character to a number let file = rankToColumn(int(uint8(s[1]) - uint8('0'))) return (file, rank) func locationToAlgebraic*(loc: Location): string {.inline.} = ## Converts a location from our internal row, column ## notation to a square in algebraic notation return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}" func getPiece*(self: ChessBoard, square: string): Piece = ## Gets the piece on the given square ## in algebraic notation let loc = square.algebraicToLocation() return self.grid[loc.row, loc.col] func getCapture*(self: ChessBoard, move: Move): Location = ## Returns the location that would be captured if this ## move were played on the board, taking en passant and ## other things into account (the move is assumed to be ## already valid). An empty location is returned if no ## piece is captured by the given move result = emptyLocation() let target = self.grid[move.targetSquare.row, move.targetSquare.col] if target.color == None: if move.targetSquare != self.position.enPassantSquare.targetSquare: return else: return ((if move.piece.color == White: move.targetSquare.row + 1 else: move.targetSquare.row - 1), move.targetSquare.col) if target.color == move.piece.color.opposite(): return move.targetSquare func isCapture*(self: ChessBoard, move: Move): bool {.inline.} = ## Returns whether the given move is a capture ## or not return self.getCapture(move) != emptyLocation() proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = ## Returns whether the given color's ## king is in check. If the color is ## set to None, checks are checked ## for the active color's king case color: of White: return self.isAttacked(self.position.pieces.white.king) of Black: return self.isAttacked(self.position.pieces.black.king) of None: case self.getActiveColor(): of White: return self.isAttacked(self.position.pieces.white.king) of Black: return self.isAttacked(self.position.pieces.black.king) else: # Unreachable discard proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} = ## Returns the sides on which castling is allowed ## for the given color. If the color is Empty, the ## currently active color is used var color = color if color == None: color = self.getActiveColor() # If the rooks or king have been moved, castling # rights have been lost case color: of White: result.king = self.position.castlingAvailable.white.king result.queen = self.position.castlingAvailable.white.queen of Black: result.king = self.position.castlingAvailable.black.king result.queen = self.position.castlingAvailable.black.queen of None: # Unreachable discard var loc: Location queenSide: Location kingSide: Location # If the path between the king and a rook is blocked, then castling # is prohibited on that side case color: of White: loc = self.position.pieces.white.king queenSide = color.leftSide() kingSide = color.rightSide() of Black: loc = self.position.pieces.black.king queenSide = color.rightSide() kingSide = color.leftSide() of None: # Unreachable discard if result.king: # Short castle var location = loc while true: location = location + kingSide if not location.isValid(): break if self.grid[location.row, location.col].kind == Empty: continue if location == color.kingSideRook() and self.grid[location.row, location.col].kind == Rook: break # Blocked by a piece result.king = false break if result.queen: # Long castle var location = loc while true: location = location + queenSide if not location.isValid(): break if self.grid[location.row, location.col].kind == Empty: continue if location == color.queenSideRook() and self.grid[location.row, location.col].kind == Rook: break # Blocked by a piece result.queen = false break # If the castling king would walk into, through or out of check # while castling on a given side, then it is not possible to castle # on that side until the threat exists if (result.king or result.queen) and self.inCheck(color): # Only check for checks if castling is still available # by this point (if we can avoid calls to generateMoves, # we should) return # TODO: Check for attacks in the various other squares proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = ## Generates the possible moves for the pawn in the given ## location var piece = self.grid[location.row, location.col] locations: seq[Location] = @[] doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" # Pawns can move forward one square let forwardOffset = piece.color.forward() let forward = (forwardOffset + location) if forward.isValid() and self.grid[forward.row, forward.col].color == None: locations.add(forwardOffset) # If the pawn is on its first rank, it can push two squares if location.row == piece.getStartRow(): locations.add(piece.color.doublePush()) if self.position.enPassantSquare.piece.color == piece.color.opposite: if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1: # Only viable if the piece is on the diagonal of the target locations.add(self.position.enPassantSquare.targetSquare) # They can also move on either diagonal one # square, but only to capture if location.col in 1..6: # Top right diagonal locations.add(piece.color.topRightDiagonal()) if location.row in 1..6: # Top left diagonal locations.add(piece.color.topLeftDiagonal()) # Pawn is at the right side, can only capture # on the left one if location.col == 7 and location.row < 7: locations.add(piece.color.topLeftDiagonal()) # Pawn is at the left side, can only capture # on the right one if location.col == 0 and location.row < 7: locations.add(piece.color.topRightDiagonal()) var newLocation: Location targetPiece: Piece for target in locations: newLocation = location + target if not newLocation.isValid(): continue targetPiece = self.grid[newLocation.row, newLocation.col] if targetPiece.color == piece.color: # Can't move over a friendly piece continue if location.col != newLocation.col and not self.isCapture(Move(piece: piece, startSquare: location, targetSquare: newLocation)): # Can only move diagonally when capturing continue if newLocation.row == piece.color.getLastRow(): # Generate all promotion moves for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: promotionType)) continue # Move is just a pawn push result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece)) proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = ## Generates moves for the sliding piece in the given location var piece = self.grid[location.row, location.col] doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}" var directions: seq[Location] = @[] # Only check in the right directions for the chosen piece if piece.kind in [Bishop, Queen]: directions.add(piece.color.topLeftDiagonal()) directions.add(piece.color.topRightDiagonal()) directions.add(piece.color.bottomLeftDiagonal()) directions.add(piece.color.bottomRightDiagonal()) if piece.kind in [Queen, Rook]: directions.add(piece.color.topSide()) directions.add(piece.color.bottomSide()) directions.add(piece.color.rightSide()) directions.add(piece.color.leftSide()) for direction in directions: # Slide in this direction as long as it's possible var square: Location = location otherPiece: Piece while true: square = square + direction # End of board reached if not square.isValid(): break otherPiece = self.grid[square.row, square.col] # A friendly piece is in the way if otherPiece.color == piece.color: break if otherPiece.color == piece.color.opposite: # Target square contains an enemy piece: capture # it and stop going any further result.add(Move(startSquare: location, targetSquare: square, piece: piece)) break # Target square is empty result.add(Move(startSquare: location, targetSquare: square, piece: piece)) proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = ## Generates moves for the king in the given location var piece = self.grid[location.row, location.col] doAssert piece.kind == King, &"generateKingMoves called on a {piece.kind}" var directions: seq[Location] = @[piece.color.topLeftDiagonal(), piece.color.topRightDiagonal(), piece.color.bottomRightDiagonal(), piece.color.bottomLeftDiagonal(), piece.color.topSide(), piece.color.bottomSide(), piece.color.leftSide(), piece.color.rightSide()] # Castling let canCastle = self.canCastle() if canCastle.queen: directions.add(piece.color.longCastleKing()) if canCastle.king: directions.add(piece.color.shortCastleKing()) var flag = Default for direction in directions: if direction == piece.color.longCastleKing(): flag = CastleLong elif direction == piece.color.shortCastleKing(): flag = CastleShort # Step in this direction once let square: Location = location + direction # End of board reached if not square.isValid(): continue let otherPiece = self.grid[square.row, square.col] # A friendly piece is in the way, move onto the next # direction if otherPiece.color == piece.color: continue # Target square is empty or contains an enemy piece: # All good for us! result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: flag)) proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = ## Generates moves for the knight in the given location var piece = self.grid[location.row, location.col] doAssert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}" var directions: seq[Location] = @[piece.color.bottomLeftKnightMove(), piece.color.bottomRightKnightMove(), piece.color.topLeftKnightMove(), piece.color.topRightKnightMove(), piece.color.bottomLeftKnightMove(long=false), piece.color.bottomRightKnightMove(long=false), piece.color.topLeftKnightMove(long=false), piece.color.topRightKnightMove(long=false)] for direction in directions: # Jump to this square let square: Location = location + direction # End of board reached if not square.isValid(): continue let otherPiece = self.grid[square.row, square.col] # A friendly piece is in the way if otherPiece.color == piece.color: continue if otherPiece.color == piece.color.opposite: # Target square contains an enemy piece: capture # it result.add(Move(startSquare: location, targetSquare: square, piece: piece)) continue # Target square is empty result.add(Move(startSquare: location, targetSquare: square, piece: piece)) proc generateMoves(self: ChessBoard, location: Location): seq[Move] = ## Returns the list of possible pseudo-legal chess moves for the ## piece in the given location let piece = self.grid[location.row, location.col] case piece.kind: of Queen, Bishop, Rook: return self.generateSlidingMoves(location) of Pawn: return self.generatePawnMoves(location) of King: return self.generateKingMoves(location) of Knight: return self.generateKnightMoves(location) else: return @[] proc generateLegalMoves(self: ChessBoard, location: Location): seq[Move] = ## Returns the list of possible legal chess moves for the ## piece in the given location for move in self.generateMoves(location): if self.isLegal(move): result.add(move) proc getAttackers*(self: ChessBoard, square: Location): seq[Piece] = ## Returns all the attackers of the given square for move in self.position.attacked.black: if move.targetSquare == square: result.add(move.piece) for move in self.position.attacked.white: if move.targetSquare == square: result.add(move.piece) # We don't use getAttackers because this one only cares about whether # the square is attacked or not (and can therefore exit earlier than # getAttackers) proc isAttacked*(self: ChessBoard, loc: Location): bool = ## Returns whether the given location is attacked ## by the current opponent let piece = self.grid[loc.row, loc.col] case piece.color: of White: for move in self.position.attacked.black: if move.targetSquare == loc: return true of Black: for move in self.position.attacked.white: if move.targetSquare == loc: return true of None: case self.getActiveColor(): of White: for move in self.position.attacked.black: if move.targetSquare == loc: return true of Black: for move in self.position.attacked.white: if move.targetSquare == loc: return true else: discard proc isAttacked*(self: ChessBoard, square: string): bool = ## Returns whether the given square is attacked ## by its opponent return self.isAttacked(square.algebraicToLocation()) proc updateAttackedSquares(self: ChessBoard) = ## Updates internal metadata about which squares ## are attacked. Called internally by doMove # We refresh the attack metadata at every move. This is an # O(1) operation, because we're only updating the length # field without deallocating the memory, which will promptly # be reused by us again. Neat! self.position.attacked.white.setLen(0) self.position.attacked.black.setLen(0) # Go over each piece one by one and see which squares # it currently attacks # Pawns for loc in self.position.pieces.white.pawns: for move in self.generateMoves(loc): self.position.attacked.white.add(move) # Bishops for loc in self.position.pieces.white.bishops: for move in self.generateMoves(loc): self.position.attacked.white.add(move) # Knights for loc in self.position.pieces.white.knights: for move in self.generateMoves(loc): self.position.attacked.white.add(move) # Rooks for loc in self.position.pieces.white.rooks: for move in self.generateMoves(loc): self.position.attacked.white.add(move) # Queens for loc in self.position.pieces.white.queens: for move in self.generateMoves(loc): self.position.attacked.white.add(move) # King for move in self.generateMoves(self.position.pieces.white.king): self.position.attacked.white.add(move) # Same for black for loc in self.position.pieces.black.pawns: for move in self.generateMoves(loc): self.position.attacked.black.add(move) for loc in self.position.pieces.black.bishops: for move in self.generateMoves(loc): self.position.attacked.black.add(move) for loc in self.position.pieces.black.knights: for move in self.generateMoves(loc): self.position.attacked.white.add(move) for loc in self.position.pieces.black.rooks: for move in self.generateMoves(loc): self.position.attacked.black.add(move) for loc in self.position.pieces.black.queens: for move in self.generateMoves(loc): self.position.attacked.black.add(move) for move in self.generateMoves(self.position.pieces.black.king): self.position.attacked.black.add(move) proc removePiece(self: ChessBoard, location: Location) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[location.row, location.col] self.grid[location.row, location.col] = emptyPiece() case piece.color: of White: case piece.kind: of Pawn: self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(location)) of Bishop: self.position.pieces.white.pawns.delete(self.position.pieces.white.bishops.find(location)) of Knight: self.position.pieces.white.pawns.delete(self.position.pieces.white.knights.find(location)) of Rook: self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(location)) of Queen: self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(location)) of King: doAssert false, "removePiece: attempted to remove the white king" else: discard of Black: case piece.kind: of Pawn: self.position.pieces.black.pawns.delete(self.position.pieces.white.pawns.find(location)) of Bishop: self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(location)) of Knight: self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(location)) of Rook: self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(location)) of Queen: self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(location)) of King: doAssert false, "removePiece: attempted to remove the black king" else: discard else: discard proc movePiece(self: ChessBoard, move: Move) = ## Internal helper to move a piece. Does ## not update attacked squares, just position ## metadata and the grid itself case move.piece.color: of White: case move.piece.kind: of Pawn: # The way things are structured, we don't care about the order # of this list, so we can add and remove entries as we please self.position.pieces.white.pawns.delete(self.position.pieces.white.pawns.find(move.startSquare)) self.position.pieces.white.pawns.add(move.targetSquare) of Bishop: self.position.pieces.white.bishops.delete(self.position.pieces.white.bishops.find(move.startSquare)) self.position.pieces.white.bishops.add(move.targetSquare) of Knight: self.position.pieces.white.knights.delete(self.position.pieces.white.knights.find(move.startSquare)) self.position.pieces.white.knights.add(move.targetSquare) of Rook: self.position.pieces.white.rooks.delete(self.position.pieces.white.rooks.find(move.startSquare)) self.position.pieces.white.rooks.add(move.targetSquare) of Queen: self.position.pieces.white.queens.delete(self.position.pieces.white.queens.find(move.startSquare)) self.position.pieces.white.queens.add(move.targetSquare) of King: self.position.pieces.white.king = move.targetSquare else: discard of Black: case move.piece.kind: of Pawn: self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(move.startSquare)) self.position.pieces.black.pawns.add(move.targetSquare) of Bishop: self.position.pieces.black.bishops.delete(self.position.pieces.black.bishops.find(move.startSquare)) self.position.pieces.black.bishops.add(move.targetSquare) of Knight: self.position.pieces.black.knights.delete(self.position.pieces.black.knights.find(move.startSquare)) self.position.pieces.black.knights.add(move.targetSquare) of Rook: self.position.pieces.black.rooks.delete(self.position.pieces.black.rooks.find(move.startSquare)) self.position.pieces.black.rooks.add(move.targetSquare) of Queen: self.position.pieces.black.queens.delete(self.position.pieces.black.queens.find(move.startSquare)) self.position.pieces.black.queens.add(move.targetSquare) of King: self.position.pieces.black.king = move.targetSquare else: discard else: discard # Empty out the starting square self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece() # Actually move the piece self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece proc updatePositions(self: ChessBoard, move: Move) = ## Internal helper to update the position of ## the pieces on the board after a move let capture = self.getCapture(move) if capture != emptyLocation(): self.position.captured = self.grid[capture.row, capture.col] if capture != self.getEnPassantTarget(): # En passant is handled elsewhere self.removePiece(capture) # Update the positional metadata of the moving piece self.movePiece(move) 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 # Final checks # Record the move in the position self.position.move = move # Needed to detect draw by the 50 move rule var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable if move.piece.kind == Pawn or self.isCapture(move): halfMoveClock = 0 else: inc(halfMoveClock) if move.piece.color == Black: inc(fullMoveCount) # Castling check: have the rooks moved? if move.piece.kind == Rook: case move.piece.color: of White: if move.startSquare.row == move.piece.getStartRow(): if move.startSquare.col == 0: # Queen side castlingAvailable.white.queen = false elif move.startSquare.col == 7: # King side castlingAvailable.white.king = false of Black: if move.startSquare.row == move.piece.getStartRow(): if move.startSquare.col == 0: # Queen side castlingAvailable.black.queen = false elif move.startSquare.col == 7: # King side castlingAvailable.black.king = false else: discard # Has a rook been captured? let capture = self.getCapture(move) if capture != emptyLocation(): let piece = self.grid[capture.row, capture.col] if piece.kind == Rook: case piece.color: of White: if capture == piece.color.queenSideRook(): # Queen side castlingAvailable.white.queen = false elif capture == piece.color.kingSideRook(): # King side castlingAvailable.white.king = false of Black: if capture == piece.color.queenSideRook(): # Queen side castlingAvailable.black.queen = false elif capture == piece.color.kingSideRook(): # King side castlingAvailable.black.king = false else: # Unreachable discard # Has the king moved? if move.piece.kind == King: case move.piece.color: of White: castlingAvailable.white.king = false castlingAvailable.white.queen = false of Black: castlingAvailable.black.king = false castlingAvailable.black.queen = false else: discard # Update position and attack metadata self.updatePositions(move) self.updateAttackedSquares() var location: Location if move.flag in [CastleShort, CastleLong]: # Move the rook onto the # correct file var location: Location target: Location if move.flag == CastleShort: location = move.piece.color.kingSideRook() target = move.piece.color.shortCastleRook() else: location = move.piece.color.queenSideRook() target = move.piece.color.longCastleRook() let rook = self.grid[location.row, location.col] let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag) self.updatePositions(move) self.updateAttackedSquares() # Record final position for future reference self.positions.add(self.position) # Create new position with var newPos = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, captured: emptyPiece(), turn: self.getActiveColor().opposite, castlingAvailable: castlingAvailable, # Updated at the next call to doMove() move: emptyMove(), # Inherit values from current position # (they are updated later anyway) pieces: self.position.pieces, attacked: self.position.attacked, ) # Check for double pawn push if move.piece.kind == Pawn and abs(move.startSquare.row - move.targetSquare.row) == 2: newPos.enPassantSquare = Move(piece: move.piece, startSquare: (move.startSquare.row, move.startSquare.col), targetSquare: move.targetSquare + move.piece.color.bottomSide()) else: newPos.enPassantSquare = emptyMove() self.position = newPos proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = ## Internal helper to "spawn" a given piece at the given ## location. Note that this will overwrite whatever piece ## was previously located there: use with caution. Does ## not automatically update the attacked square metadata ## or other positional information case piece.color: of White: case piece.kind: of Pawn: self.position.pieces.white.pawns.add(location) of Knight: self.position.pieces.white.knights.add(location) of Bishop: self.position.pieces.white.bishops.add(location) of Rook: self.position.pieces.white.rooks.add(location) of Queen: self.position.pieces.white.queens.add(location) of King: self.position.pieces.white.king = location else: discard of Black: case piece.kind: of Pawn: self.position.pieces.black.pawns.add(location) of Knight: self.position.pieces.black.knights.add(location) of Bishop: self.position.pieces.black.bishops.add(location) of Rook: self.position.pieces.black.rooks.add(location) of Queen: self.position.pieces.black.queens.add(location) of King: self.position.pieces.black.king = location else: discard else: # Unreachable discard self.grid[location.row, location.col] = piece proc undoLastMove*(self: ChessBoard): Move {.discardable.} = ## Undoes the last move, restoring the previous position. ## If there are no positions to roll back to to, this is a ## no-op. Returns the move that was performed (may be empty) result = emptyMove() if self.positions.len() == 0: return var previous = self.positions[^1] oppositeMove = Move(piece: previous.move.piece, targetSquare: previous.move.startSquare, startSquare: previous.move.targetSquare) self.removePiece(previous.move.startSquare) self.position = previous if previous.move != emptyMove(): self.spawnPiece(previous.move.startSquare, previous.move.piece) self.updateAttackedSquares() self.updatePositions(oppositeMove) if previous.captured != emptyPiece(): self.spawnPiece(previous.move.targetSquare, previous.captured) discard self.positions.pop() return self.position.move proc isLegal(self: ChessBoard, move: Move): bool = ## Returns whether the given move is legal var move = move # Start square doesn't contain a piece (and it isn't the en passant square) # or it's not this player's turn to move if (move.piece.kind == Empty and move.targetSquare != self.getEnPassantTarget()) or move.piece.color != self.getActiveColor(): return false var destination = self.grid[move.targetSquare.row, move.targetSquare.col] # Destination square is occupied by a friendly piece if destination.kind != Empty and destination.color == self.getActiveColor(): return false if move.piece.kind == King and move.piece.color.longCastleKing() + move.startSquare == move.targetSquare: move.flag = CastleLong elif move.piece.kind == King and move.piece.color.shortCastleKing() + move.startSquare == move.targetSquare: move.flag = CastleShort if move notin self.generateMoves(move.startSquare): # Piece cannot arrive to destination (blocked # or otherwise invalid move) return false self.doMove(move) defer: discard self.undoLastMove() # Move would reveal an attack # on our king: not allowed if self.inCheck(move.piece.color): return false # All checks have passed: move is legal result = true proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = ## Like the other makeMove(), but with a Move object result = move if not self.isLegal(move): return emptyMove() self.doMove(result) proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} = ## Makes a move on the board from the chosen start square to ## the chosen target square, ensuring it is legal (turns are ## taken into account!). This function returns a Move object: if the move ## is legal and has been performed, the fields will be populated properly. ## For efficiency purposes, no exceptions are raised if the move is ## illegal, but the move's piece kind will be Empty (its color will be None ## too) and the locations will both be set to the tuple (-1, -1) var startLocation = startSquare.algebraicToLocation() targetLocation = targetSquare.algebraicToLocation() result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col]) return self.makeMove(result) proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} = ## Like the other makeMove(), but with two locations result = Move(startSquare: startSquare, targetSquare: targetSquare, piece: self.grid[startSquare.row, startSquare.col]) return self.makeMove(result) proc `$`*(self: ChessBoard): string = result &= "- - - - - - - -" for i, row in self.grid: result &= "\n" for piece in row: if piece.kind == Empty: result &= "x " continue if piece.color == White: result &= &"{char(piece.kind).toUpperAscii()} " else: result &= &"{char(piece.kind)} " result &= &"{rankToColumn(i + 1) + 1}" result &= "\n- - - - - - - -" result &= "\na b c d e f g h" proc pretty*(self: ChessBoard): string = ## Returns a colorized version of the ## board for easier visualization result &= "- - - - - - - -" for i, row in self.grid: result &= "\n" for j, piece in row: if piece.kind == Empty: result &= "\x1b[36;1mx" # Avoids the color overflowing # onto the numbers if j < 7: result &= " \x1b[0m" else: result &= "\x1b[0m " continue if piece.color == White: result &= &"\x1b[37;1m{char(piece.kind).toUpperAscii()}\x1b[0m " else: result &= &"\x1b[30;1m{char(piece.kind)} " result &= &"\x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m" result &= "\n- - - - - - - -" result &= "\n\x1b[31;1ma b c d e f g h" result &= "\x1b[0m" when isMainModule: proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) = doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead" proc testPieceCount(board: ChessBoard, kind: PieceKind, color: PieceColor, count: int) = let pieces = board.countPieces(kind, color) doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead" echo "Running tests" var b = newDefaultChessboard() # Ensure correct number of pieces testPieceCount(b, Pawn, White, 8) testPieceCount(b, Pawn, Black, 8) testPieceCount(b, Knight, White, 2) testPieceCount(b, Knight, Black, 2) testPieceCount(b, Bishop, White, 2) testPieceCount(b, Bishop, Black, 2) testPieceCount(b, Rook, White, 2) testPieceCount(b, Rook, Black, 2) testPieceCount(b, Queen, White, 1) testPieceCount(b, Queen, Black, 1) testPieceCount(b, King, White, 1) testPieceCount(b, King, Black, 1) # Ensure pieces are in the correct location # Pawns for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]: testPiece(b.getPiece(loc), Pawn, White) for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]: testPiece(b.getPiece(loc), Pawn, Black) # Rooks testPiece(b.getPiece("a1"), Rook, White) testPiece(b.getPiece("h1"), Rook, White) testPiece(b.getPiece("a8"), Rook, Black) testPiece(b.getPiece("h8"), Rook, Black) # Knights testPiece(b.getPiece("b1"), Knight, White) testPiece(b.getPiece("g1"), Knight, White) testPiece(b.getPiece("b8"), Knight, Black) testPiece(b.getPiece("g8"), Knight, Black) # Bishops testPiece(b.getPiece("c1"), Bishop, White) testPiece(b.getPiece("f1"), Bishop, White) testPiece(b.getPiece("c8"), Bishop, Black) testPiece(b.getPiece("f8"), Bishop, Black) # Kings testPiece(b.getPiece("e1"), King, White) testPiece(b.getPiece("e8"), King, Black) # Queens testPiece(b.getPiece("d1"), Queen, White) testPiece(b.getPiece("d8"), Queen, Black) echo "All tests were successful"