# 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 std/strutils import std/strformat import std/sequtils type PieceColor* = enum ## A piece color enumeration None = 0'i8, White, Black PieceKind* = enum ## A chess piece enumeration Empty = 0'i8, # 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 = 0'u16, # No flag EnPassant = 1, # Move is a capture with en passant Capture = 2, # Move is a capture DoublePush = 4, # Move is a double pawn push # Castling metadata CastleLong = 8, CastleShort = 16, # Pawn promotion metadata PromoteToQueen = 32, PromoteToRook = 64, PromoteToBishop = 128, PromoteToKnight = 256 # Useful type aliases Location* = tuple[row, col: int8] Attacked = seq[tuple[source, target, direction: Location]] Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], bishops: seq[Location], knights: seq[Location], pawns: seq[Location]] CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64] Move* = object ## A chess move startSquare*: Location targetSquare*: Location flags*: uint16 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: int16 # Number of half moves since # last piece capture or pawn movement. # Used for the 50-move rule halfMoveClock: int8 # Full move counter. Increments # every 2 ply fullMoveCount: int16 # En passant target square (see https://en.wikipedia.org/wiki/En_passant) enPassantSquare*: Location # Locations of all pieces pieces: tuple[white: Pieces, black: Pieces] # Squares attacked by both sides attacked: tuple[white: Attacked, black: Attacked] # Pieces pinned by both sides pinned: tuple[white: Attacked, black: Attacked] # Active color turn: PieceColor ChessBoard* = ref object ## A chess board object # An 8x8 matrix we use for constant # time lookup of pieces by their location grid: seq[Piece] # The current position position: Position # List of all reached positions positions: seq[Position] # 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) proc algebraicToLocation*(s: string): Location {.inline.} proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation()) func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col) func `-`*(a: Location): Location = (-a.row, -a.col) func `-`*(a, b: Location): Location = (a.row - b.row, a.col - b.col) func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7 proc generateMoves(self: ChessBoard, location: Location): seq[Move] proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location] proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool proc isLegal(self: ChessBoard, move: Move): bool {.inline.} proc doMove(self: ChessBoard, move: Move) proc pretty*(self: ChessBoard): string proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) 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] proc inCheck*(self: ChessBoard, color: PieceColor = None): bool proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = for x in other: self.add(x) # 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)) 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 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 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) # 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) return self.position.turn func getEnPassantTarget*(self: ChessBoard): Location {.inline.} = ## Returns the current en passant target square return self.position.enPassantSquare func getMoveCount*(self: ChessBoard): int {.inline.} = ## Returns the number of full moves that ## have been played return self.position.fullMoveCount func getHalfMoveCount*(self: ChessBoard): int {.inline.} = ## 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 = newSeqOfCap[Piece](64) result.position = Position(attacked: (@[], @[]), enPassantSquare: emptyLocation(), move: emptyMove(), turn: White, fullMoveCount: 1, pieces: (white: (king: emptyLocation(), queens: @[], rooks: @[], bishops: @[], knights: @[], pawns: @[]), black: (king: emptyLocation(), queens: @[], rooks: @[], bishops: @[], knights: @[], pawns: @[]))) func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col func `[]`(self: seq[Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)] proc `[]=`(self: var seq[Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece proc newChessboardFromFEN*(fen: string): ChessBoard = ## Initializes a chessboard with the ## position encoded by the given FEN string result = newChessboard() var # Current location in the grid row: int8 = 0 column: int8 = 0 # Current section in the FEN string section = 0 # Current index into the FEN string index = 0 # Temporary variable to store a piece piece: Piece # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation while index <= fen.high(): var c = fen[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: 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 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: 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 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')) if x > 8: raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)") column += int8(x) else: raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'") of 1: # Active color case c: of 'w': result.position.turn = White of 'b': result.position.turn = Black else: raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'") 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 FEN: unknown symbol '{c}' found in castling availability section") of 3: # En passant target square case c: of '-': # Field is already uninitialized to the correct state discard else: result.position.enPassantSquare = fen[index..index+1].algebraicToLocation() # Square metadata is 2 bytes long inc(index) of 4: # Halfmove clock var s = "" while not fen[index].isSpaceAscii(): s.add(fen[index]) inc(index) # Backtrack so the space is seen by the # next iteration of the loop dec(index) result.position.halfMoveClock = parseInt(s).int8 of 5: # Fullmove number var s = "" while index <= fen.high(): s.add(fen[index]) inc(index) result.position.fullMoveCount = parseInt(s).int8 else: 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 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.} = ## 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 = ## Returns the number of pieces with ## the given color and type in the given ## position 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: raise newException(ValueError, "invalid piece type") 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: raise newException(ValueError, "invalid piece type") of None: raise newException(ValueError, "invalid piece color") func countPieces*(self: ChessBoard, piece: Piece): int {.inline.} = ## Returns the number of pieces on the board that ## are of the same type and color as the given piece return self.countPieces(piece.kind, piece.color) func rankToColumn(rank: int): int8 {.inline.} = ## 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: array[8, int8] = [7, 6, 5, 4, 3, 2, 1, 0] return indeces[rank - 1] func rowToRank(row: int): int {.inline.} = ## Converts a row into our grid into ## a chess rank 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 = int8(uint8(s[0]) - uint8('a')) # Convert the file character to a number let file = rankToColumn(int8(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, loc: Location): Piece {.inline.} = ## Gets the piece at the given location return self.grid[loc.row, loc.col] func getPiece*(self: ChessBoard, square: string): Piece {.inline.} = ## Gets the piece on the given square ## in algebraic notation return self.getPiece(square.algebraicToLocation()) func isPromotion*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## pawn promotion for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]: if (move.flags and promotion.uint16) != 0: return true func getPromotionType*(move: Move): MoveFlag {.inline.} = ## Returns the promotion type of the given move. ## The return value of this function is only valid ## if isPromotion() returns true for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]: if (move.flags and promotion.uint16) != 0: return promotion func isCapture*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## cature result = (move.flags and Capture.uint16) != 0 func isCastling*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## castle for flag in [CastleLong, CastleShort]: if (move.flags and flag.uint16) != 0: return true func getCastlingType*(move: Move): MoveFlag {.inline.} = ## Returns the castling type of the given move. ## The return value of this function is only valid ## if isCastling() returns true for flag in [CastleLong, CastleShort]: if (move.flags and flag.uint16) != 0: return flag func isEnPassant*(move: Move): bool {.inline.} = ## Returns whether the given move is an ## en passant capture result = (move.flags and EnPassant.uint16) != 0 func isDoublePush*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## double pawn push result = (move.flags and DoublePush.uint16) != 0 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 var color = color if color == None: color = self.getActiveColor() case color: of White: result = self.isAttacked(self.position.pieces.white.king, Black) of Black: result = self.isAttacked(self.position.pieces.black.king, White) 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 None, the ## currently active color is used var color = color if color == None: color = self.getActiveColor() # Check if castling rights are still available for moving side 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 if self.inCheck(color): # King can not castle out of check return (false, false) if result.king or result.queen: var 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 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 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 otherPiece: Piece 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: 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 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: result.queen = false break if moveKing and self.isAttacked(location, color.opposite()): result.queen = false break proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = ## Returns the squares that need to be covered to ## 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 case color: of White: king = self.position.pieces.white.king of Black: king = self.position.pieces.black.king else: return let attackers: seq[Location] = self.getAttackers(king, color.opposite()) if attackers.len() > 1: # Double checks require to move the king return @[] let attacker = attackers[0] attackerPiece = self.grid[attacker.row, attacker.col] attack = self.getAttackFor(attacker, king) # Capturing the piece resolves the check result.add(attacker) # 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, 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.} = var color = color if color == None: color = self.getActiveColor() case color: of White: return self.position.pieces.white.king of Black: return self.position.pieces.black.king else: discard 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] = @[] flags: seq[MoveFlag] = @[] doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" # Pawns can move forward one square 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) flags.add(Default) # If the pawn is on its first rank, it can push two squares if location.row == piece.getStartRow(): let double = location + piece.color.doublePush() # Check that both squares are empty 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 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(): let otherPiece = self.grid[diagonal.row, diagonal.col] 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 if king.row == location.row: var current = location + piece.color.rightSide() while true: current = current + piece.color.rightSide() if not current.isValid(): break let p = self.grid[current.row, current.col] if p.color == piece.color: break if p.color == None: continue # Bishops can't create checks through en passant if p.color == piece.color.opposite() and p.kind in [Queen, Rook]: ok = false if ok: locations.add(diagonal) flags.add(EnPassant) elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King: locations.add(diagonal) flags.add(Capture) var newLocation: Location newFlags: seq[MoveFlag] newLocations: seq[Location] # Check for pins let pins = self.getPinnedDirections(location) for pin in pins: newLocation = location + pin let loc = locations.find(newLocation) if loc != -1: # Pin direction is legal for this piece newLocations.add(newLocation) newFlags.add(flags[loc]) if pins.len() > 0: locations = newLocations flags = newFlags let checked = self.inCheck() let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) var targetPiece: Piece for (target, flag) in zip(locations, flags): if checked and target notin resolutions: continue targetPiece = self.grid[target.row, target.col] if target.row == piece.color.getLastRow(): # Pawn reached the other side of the board: generate all potential piece promotions for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: result.add(Move(startSquare: location, targetSquare: target, flags: promotionType.uint16 or flag.uint16)) continue result.add(Move(startSquare: location, targetSquare: target, flags: flag.uint16)) proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = ## Generates moves for the sliding piece in the given location let 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()) let pinned = self.getPinnedDirections(location) if pinned.len() > 0: directions = pinned let checked = self.inCheck() let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) 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 checked and square notin resolutions: break if otherPiece.color == piece.color.opposite: # Target square contains an enemy piece: capture # it and stop going any further if otherPiece.kind != King: # Can't capture the king result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16)) break # Target square is empty result.add(Move(startSquare: location, targetSquare: square)) 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(piece.color) if canCastle.queen: directions.add(longCastleKing()) if canCastle.king: directions.add(shortCastleKing()) var flag = Default for direction in directions: # Step in this direction once let square: Location = location + direction # End of board reached if not square.isValid(): continue if self.isAttacked(square, piece.color.opposite()): continue if direction == longCastleKing(): flag = CastleLong elif direction == shortCastleKing(): flag = CastleShort else: flag = Default let otherPiece = self.grid[square.row, square.col] if otherPiece.color == self.getActiveColor.opposite(): flag = Capture # 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, flags: flag.uint16)) 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)] let pinned = self.getPinnedDirections(location) if pinned.len() > 0: # Knight is pinned: can't move! return @[] let checked = self.inCheck() let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) 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 or the opponent king is is in the way if otherPiece.color == piece.color or otherPiece.kind == King: continue if checked and square notin resolutions: continue if otherPiece.color != None: # Target square contains an enemy piece: capture # it result.add(Move(startSquare: location, targetSquare: square, flags: Capture.uint16)) else: # Target square is empty result.add(Move(startSquare: location, targetSquare: square)) proc generateMoves(self: ChessBoard, location: Location): seq[Move] = ## Returns the list of possible 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 generateAllMoves*(self: ChessBoard): seq[Move] = ## Returns the list of all possible legal moves ## in the current position for i in 0..7: for j in 0..7: if self.grid[i, j].color == self.getActiveColor(): for move in self.generateMoves((int8(i), int8(j))): result.add(move) proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool = ## Returns whether the given location is attacked ## by the given color var color = color if color == None: color = self.getActiveColor().opposite() case color: of Black: for attack in self.position.attacked.black: if attack.target == loc: return true of White: for attack in self.position.attacked.white: if attack.target == loc: return true of None: discard proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location] = ## Returns all the attackers of the given color ## for the given square case color: of Black: for attack in self.position.attacked.black: if attack.target == loc: result.add(attack.source) of White: for attack in self.position.attacked.white: if attack.target == loc: result.add(attack.source) of None: discard proc getAttacks*(self: ChessBoard, loc: Location): Attacked = ## Returns all the squares attacked by the piece in the given ## location let piece = self.grid[loc.row, loc.col] case piece.color: of Black: for attack in self.position.attacked.black: if attack.source == loc: result.add(attack) of White: for attack in self.position.attacked.white: if attack.source == loc: result.add(attack) of None: discard proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] = ## Returns the first attacks of the piece in the given ## source location that also attacks the target location let piece = self.grid[source.row, source.col] case piece.color: of Black: for attack in self.position.attacked.black: if attack.target == target and attack.source == source: return attack of White: for attack in self.position.attacked.white: if attack.target == target and attack.source == source: return attack of None: discard proc isAttacked*(self: ChessBoard, square: string): bool = ## Returns whether the given square is attacked ## by the current return self.isAttacked(square.algebraicToLocation()) func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Location], color: PieceColor) {.inline.} = if attack.source.isValid() and attack.target.isValid(): case color: of White: self.position.attacked.white.add(attack) of Black: self.position.attacked.black.add(attack) else: discard proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] = let piece = self.grid[loc.row, loc.col] case piece.color: of None: discard of White: for pin in self.position.pinned.black: if pin.target == loc: result.add(pin.direction) of Black: for pin in self.position.pinned.white: if pin.target == loc: result.add(pin.direction) proc updatePawnAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares for loc in self.position.pieces.white.pawns: # Pawns are special in how they capture (i.e. the # squares they can move to do not match the squares # they can capture on. Sneaky fucks) self.addAttack((loc, loc + White.topRightDiagonal(), White.topRightDiagonal()), White) self.addAttack((loc, loc + White.topLeftDiagonal(), White.topRightDiagonal()), White) # We do the same thing for black for loc in self.position.pieces.black.pawns: self.addAttack((loc, loc + Black.topRightDiagonal(), Black.topRightDiagonal()), Black) self.addAttack((loc, loc + Black.topLeftDiagonal(), Black.topRightDiagonal()), Black) proc updateKingAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares var king = self.position.pieces.white.king self.addAttack((king, king + White.topRightDiagonal(), White.topRightDiagonal()), White) self.addAttack((king, king + White.topLeftDiagonal(), White.topLeftDiagonal()), White) self.addAttack((king, king + White.bottomLeftDiagonal(), White.bottomLeftDiagonal()), White) self.addAttack((king, king + White.bottomRightDiagonal(), White.bottomRightDiagonal()), White) king = self.position.pieces.black.king self.addAttack((king, king + Black.topRightDiagonal(), Black.topRightDiagonal()), Black) self.addAttack((king, king + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black) self.addAttack((king, king + Black.bottomLeftDiagonal(), Black.bottomLeftDiagonal()), Black) self.addAttack((king, king + Black.bottomRightDiagonal(), Black.bottomRightDiagonal()), Black) proc updateKnightAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares for loc in self.position.pieces.white.knights: self.addAttack((loc, loc + White.topLeftKnightMove(), White.topLeftKnightMove()), White) self.addAttack((loc, loc + White.topRightKnightMove(), White.topRightKnightMove()), White) self.addAttack((loc, loc + White.bottomLeftKnightMove(), White.bottomLeftKnightMove()), White) self.addAttack((loc, loc + White.bottomRightKnightMove(), White.bottomRightKnightMove()), White) self.addAttack((loc, loc + White.topLeftKnightMove(long=false), White.topLeftKnightMove(long=false)), White) self.addAttack((loc, loc + White.topRightKnightMove(long=false), White.topRightKnightMove(long=false)), White) self.addAttack((loc, loc + White.bottomLeftKnightMove(long=false), White.bottomLeftKnightMove(long=false)), White) self.addAttack((loc, loc + White.bottomRightKnightMove(long=false), White.bottomRightKnightMove(long=false)), White) for loc in self.position.pieces.black.knights: self.addAttack((loc, loc + Black.topLeftKnightMove(), Black.topLeftKnightMove()), Black) self.addAttack((loc, loc + Black.topRightKnightMove(), Black.topRightKnightMove()), Black) self.addAttack((loc, loc + Black.bottomLeftKnightMove(), Black.bottomLeftKnightMove()), Black) self.addAttack((loc, loc + Black.bottomRightKnightMove(), Black.bottomRightKnightMove()), Black) self.addAttack((loc, loc + Black.topLeftKnightMove(long=false), Black.topLeftKnightMove(long=false)), Black) self.addAttack((loc, loc + Black.topRightKnightMove(long=false), Black.topRightKnightMove(long=false)), Black) self.addAttack((loc, loc + Black.bottomLeftKnightMove(long=false), Black.bottomLeftKnightMove(long=false)), Black) self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false), Black.bottomRightKnightMove(long=false)), Black) proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] = ## Internal helper of updateSlidingAttacks var directions: seq[Location] = @[] let piece = self.grid[loc.row, loc.col] 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: var square = loc otherPiece: Piece # Slide in this direction as long as it's possible while true: square = square + direction # End of board reached if not square.isValid(): break otherPiece = self.grid[square.row, square.col] # Target square is attacked (even if a friendly piece # is present, because in this case we're defending # it) result.attacks.add((loc, square, direction)) # Empty square, keep going if otherPiece.color == None: continue if otherPiece.color == piece.color.opposite(): if otherPiece.kind != King: # We found an enemy piece that is not # the enemy king. We don't break out # immediately because we first want # to check if we've pinned a piece var otherSquare: Location = square behindPiece: Piece while true: otherSquare = otherSquare + direction if not otherSquare.isValid(): break behindPiece = self.grid[otherSquare.row, otherSquare.col] if behindPiece.color == None: continue if behindPiece.color == piece.color.opposite and behindPiece.kind == King: # The enemy king is behind this enemy piece: pin it along # this axis in both directions result.pins.add((loc, square, direction)) result.pins.add((loc, square, -direction)) else: break else: # Enemy king is here: ensure it cannot move backwards by # attacking the square behind it (if one exists and is # valid) let target = square + direction if target.isValid(): result.attacks.add((loc, target, direction)) break proc updateSlidingAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares var data: tuple[attacks: Attacked, pins: Attacked] for loc in self.position.pieces.white.bishops: data = self.getSlidingAttacks(loc) self.position.attacked.white.extend(data.attacks) self.position.pinned.white.extend(data.pins) for loc in self.position.pieces.white.rooks: data = self.getSlidingAttacks(loc) self.position.attacked.white.extend(data.attacks) self.position.pinned.white.extend(data.pins) for loc in self.position.pieces.white.queens: data = self.getSlidingAttacks(loc) self.position.attacked.white.extend(data.attacks) self.position.pinned.white.extend(data.pins) for loc in self.position.pieces.black.bishops: data = self.getSlidingAttacks(loc) self.position.attacked.black.extend(data.attacks) self.position.pinned.black.extend(data.pins) for loc in self.position.pieces.black.rooks: data = self.getSlidingAttacks(loc) self.position.attacked.black.extend(data.attacks) self.position.pinned.black.extend(data.pins) for loc in self.position.pieces.black.queens: data = self.getSlidingAttacks(loc) self.position.attacked.black.extend(data.attacks) self.position.pinned.black.extend(data.pins) proc updateAttackedSquares(self: ChessBoard) = ## Updates internal metadata about which squares ## are attacked self.position.attacked.white.setLen(0) self.position.attacked.black.setLen(0) # Pawns self.updatePawnAttacks() # Sliding pieces self.updateSlidingAttacks() # Knights self.updateKnightAttacks() # Kings self.updateKingAttacks() proc removePiece(self: ChessBoard, location: Location, attack: bool = true) = ## 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.black.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 if attack: self.updateAttackedSquares() proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = ## Internal helper to move a piece. If attack ## is set to false, then this function does ## not update attacked squares metadata, just ## positional info and the grid itself let piece = self.grid[move.startSquare.row, move.startSquare.col] case piece.color: of White: case 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 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] = piece if attack: self.updateAttackedSquares() proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) = ## Like the other movePiece(), but with two locations self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack) proc doMove(self: ChessBoard, move: Move) = ## Internal function called by makeMove after ## 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] 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.isCapture(): 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.isDoublePush(): enPassantTarget = move.targetSquare + piece.color.bottomSide() # Castling check: have the rooks moved? if piece.kind == Rook: case piece.color: of White: if move.startSquare.row == 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 == 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? if move.isCapture(): let captured = self.grid[move.targetSquare.row, move.targetSquare.col] if captured.kind == Rook: case piece.color: of White: if move.targetSquare == captured.color.queenSideRook(): # Queen side castlingAvailable.white.queen = false elif move.targetSquare == captured.color.kingSideRook(): # King side castlingAvailable.white.king = false of Black: if move.targetSquare == captured.color.queenSideRook(): # Queen side castlingAvailable.black.queen = false elif move.targetSquare == captured.color.kingSideRook(): # King side castlingAvailable.black.king = false else: # Unreachable discard # Has the king moved? if piece.kind == King or move.isCastling(): # Revoke all castling rights for the moving king case piece.color: of White: castlingAvailable.white.king = false castlingAvailable.white.queen = false of Black: castlingAvailable.black.king = false castlingAvailable.black.queen = false else: discard # Create new position self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, turn: self.getActiveColor().opposite, castlingAvailable: castlingAvailable, move: move, pieces: self.position.pieces, enPassantSquare: enPassantTarget ) # Update position metadata if move.isCastling(): # Move the rook onto the # correct file when castling var location: Location target: Location if move.getCastlingType() == CastleShort: location = piece.color.kingSideRook() target = shortCastleRook() else: location = piece.color.queenSideRook() target = longCastleRook() let rook = self.grid[location.row, location.col] let move = Move(startSquare: location, targetSquare: location + target, flags: move.flags) self.movePiece(move, attack=false) if move.isCapture(): # Get rid of captured pieces self.removePiece(move.targetSquare, attack=false) if move.isEnPassant(): # Make the en passant pawn disappear self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false) self.movePiece(move, attack=false) if move.isPromotion(): # Move is a pawn promotion: get rid of the pawn # and spawn a new piece self.removePiece(move.targetSquare, attack=false) case move.getPromotionType(): of PromoteToBishop: self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color)) of PromoteToKnight: self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color)) of PromoteToRook: self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color)) of PromoteToQueen: self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color)) else: discard # Update attack metadata self.updateAttackedSquares() 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 resetBoard*(self: ChessBoard) = ## Resets the internal grid representation ## according to the positional data stored ## in the chessboard. Warning: this can be ## expensive, especially in critical paths ## or tight loops for i in 0..63: self.grid[i] = emptyPiece() for loc in self.position.pieces.white.pawns: self.grid[loc.row, loc.col] = Piece(color: White, kind: Pawn) for loc in self.position.pieces.black.pawns: self.grid[loc.row, loc.col] = Piece(color: Black, kind: Pawn) for loc in self.position.pieces.white.bishops: self.grid[loc.row, loc.col] = Piece(color: White, kind: Bishop) for loc in self.position.pieces.black.bishops: self.grid[loc.row, loc.col] = Piece(color: Black, kind: Bishop) for loc in self.position.pieces.white.knights: self.grid[loc.row, loc.col] = Piece(color: White, kind: Knight) for loc in self.position.pieces.black.knights: self.grid[loc.row, loc.col] = Piece(color: Black, kind: Knight) for loc in self.position.pieces.white.rooks: self.grid[loc.row, loc.col] = Piece(color: White, kind: Rook) for loc in self.position.pieces.black.rooks: self.grid[loc.row, loc.col] = Piece(color: Black, kind: Rook) for loc in self.position.pieces.white.queens: self.grid[loc.row, loc.col] = Piece(color: White, kind: Queen) for loc in self.position.pieces.black.queens: self.grid[loc.row, loc.col] = Piece(color: Black, kind: Queen) self.grid[self.position.pieces.white.king.row, self.position.pieces.white.king.col] = Piece(color: White, kind: King) self.grid[self.position.pieces.black.king.row, self.position.pieces.black.king.col] = Piece(color: Black, kind: King) proc undoLastMove*(self: ChessBoard) = if self.positions.len() == 0: return self.position = self.positions.pop() self.resetBoard() proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = ## Returns whether the given move is legal if self.grid[move.startSquare.row, move.startSquare.col].color != self.getActiveColor(): return false return move in self.generateMoves(move.startSquare) proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move if not self.isLegal(move): return emptyMove() self.doMove(move) proc toChar*(piece: Piece): char = if piece.color == White: return char(piece.kind).toUpperAscii() return char(piece.kind) proc `$`*(self: ChessBoard): string = result &= "- - - - - - - -" for i in 0..7: result &= "\n" for j in 0..7: let piece = self.grid[i, j] if piece.kind == Empty: result &= "x " continue result &= &"{piece.toChar()} " result &= &"{rankToColumn(i + 1) + 1}" result &= "\n- - - - - - - -" result &= "\na b c d e f g h" proc toPretty*(piece: Piece): string = case piece.color: of Black: case piece.kind: of King: return "\U2654" of Queen: return "\U2655" of Rook: return "\U2656" of Bishop: return "\U2657" of Knight: return "\U2658" of Pawn: return "\U2659" else: discard of White: case piece.kind: of King: return "\U265A" of Queen: return "\U265B" of Rook: return "\U265C" of Bishop: return "\U265D" of Knight: return "\U265E" of Pawn: return "\U265F" else: discard else: discard proc pretty*(self: ChessBoard): string = ## Returns a colorized version of the ## board for easier visualization for i in 0..7: if i > 0: result &= "\n" for j in 0..7: if ((i + j) mod 2) == 0: result &= "\x1b[39;44;1m" else: result &= "\x1b[39;40;1m" let piece = self.grid[i, j] if piece.kind == Empty: result &= " \x1b[0m" else: result &= &"{piece.toPretty()} \x1b[0m" result &= &" \x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m" result &= "\n\x1b[31;1ma b c d e f g h" result &= "\x1b[0m" proc toFEN*(self: ChessBoard): string = ## Returns a FEN string of the current ## position in the chessboard var skip: int # Piece placement data for i in 0..7: skip = 0 for j in 0..7: let piece = self.grid[i, j] if piece.kind == Empty: inc(skip) elif skip > 0: result &= &"{skip}{piece.toChar()}" skip = 0 else: result &= piece.toChar() if skip > 0: result &= $skip if i < 7: result &= "/" result &= " " # Active color result &= (if self.getActiveColor() == White: "w" else: "b") result &= " " # Castling availability let castleWhite = self.position.castlingAvailable.white let castleBlack = self.position.castlingAvailable.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.getEnPassantTarget() == emptyLocation(): result &= "-" else: result &= self.getEnPassantTarget().locationToAlgebraic() result &= " " # Halfmove clock result &= $self.getHalfMoveCount() result &= " " # Fullmove number result &= $self.getMoveCount() proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData = ## Counts (and debugs) the number of legal positions reached after ## the given number of ply let moves = self.generateAllMoves() if len(moves) == 0: result.checkmates = 1 if ply == 0: result.nodes = 1 return if ply == 1 and bulk: if divide: var postfix = "" for move in moves: case move.getPromotionType(): of PromoteToBishop: postfix = "b" of PromoteToKnight: postfix = "n" of PromoteToRook: postfix = "r" of PromoteToQueen: postfix = "q" else: postfix = "" echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: 1" if verbose: echo "" return (uint64(len(moves)), 0, 0, 0, 0, 0, 0) for move in moves: if verbose: let canCastle = self.canCastle(self.getActiveColor()) echo &"Ply (from root): {self.position.plyFromRoot}" echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})" echo &"Turn: {self.getActiveColor()}" echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}" echo &"Flag: {move.flags}" echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}" echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" echo &"Position before move: {self.toFEN()}" stdout.write("En Passant target: ") if self.getEnPassantTarget() != emptyLocation(): echo self.getEnPassantTarget().locationToAlgebraic() else: echo "None" echo "\n", self.pretty() self.doMove(move) if move.isCapture(): inc(result.captures) if move.isCastling(): inc(result.castles) if move.isPromotion(): inc(result.promotions) if move.isEnPassant(): inc(result.enPassant) if self.inCheck(): # Opponent king is in check inc(result.checks) if verbose: let canCastle = self.canCastle(self.getActiveColor()) echo "\n" echo &"Opponent in check: {(if self.inCheck(): \"yes\" else: \"no\")}" echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" echo &"Position after move: {self.toFEN()}" echo "\n", self.pretty() stdout.write("nextpos>> ") try: discard readLine(stdin) except IOError: discard except EOFError: discard let next = self.perft(ply - 1, verbose, bulk=bulk) self.undoLastMove() if divide and (not bulk or ply > 1): var postfix = "" if move.isPromotion(): case move.getPromotionType(): of PromoteToBishop: postfix = "b" of PromoteToKnight: postfix = "n" of PromoteToRook: postfix = "r" of PromoteToQueen: postfix = "q" else: discard echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: {next.nodes}" if verbose: echo "" result.nodes += next.nodes result.captures += next.captures result.checks += next.checks result.promotions += next.promotions result.castles += next.castles result.enPassant += next.enPassant result.checkmates += next.checkmates proc handleGoCommand(board: ChessBoard, command: seq[string]) = if len(command) < 2: echo &"Error: go: invalid number of arguments" return case command[1]: of "perft": if len(command) == 2: echo &"Error: go: perft: invalid number of arguments" return var args = command[2].splitWhitespace() bulk = false verbose = false if args.len() > 1: var ok = true for arg in args[1..^1]: case arg: of "bulk-count", "bulk": bulk = true of "verbose": verbose = true else: echo &"Error: go: perft: invalid argument '{args[1]}'" ok = false break if not ok: return try: let ply = parseInt(args[0]) if bulk: echo &"\nNodes searched (bulk-counting: on): {board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes}\n" else: let data = board.perft(ply, divide=true, verbose=verbose) echo &"\nNodes searched (bulk-counting: off): {data.nodes}" echo &" - Captures: {data.captures}" echo &" - Checks: {data.checks}" echo &" - E.P: {data.enPassant}" echo &" - Checkmates: {data.checkmates}" echo &" - Castles: {data.castles}" echo &" - Promotions: {data.promotions}" echo "" except ValueError: echo "Error: go: perft: invalid depth" else: echo &"Error: go: unknown subcommand '{command[1]}'" proc handlePositionCommand(board: var ChessBoard, command: seq[string]) = case len(command): of 2: case command[1]: of "startpos": board = newDefaultChessboard() of "current", "cur": echo &"Current position: {board.toFEN()}" of "pretty": echo board.pretty() of "print", "show": echo board else: echo &"Error: position: invalid argument '{command[1]}'" of 3: case command[1]: of "fen": try: board = newChessboardFromFEN(command[2]) except ValueError: echo &"Error: position: invalid FEN string '{command[2]}': {getCurrentExceptionMsg()}" else: echo &"Error: position: unknown subcommand '{command[1]}'" else: echo &"Error: position: invalid number of arguments" proc handleMoveCommand(board: ChessBoard, command: seq[string]) = if len(command) != 2: echo &"Error: move: invalid number of arguments" return let moveString = command[1] if len(moveString) notin 4..5: echo &"Error: move: invalid move syntax" return var startSquare: Location targetSquare: Location flags: uint16 try: startSquare = moveString[0..1].algebraicToLocation() except ValueError: echo &"Error: move: invalid start square" return try: targetSquare = moveString[2..3].algebraicToLocation() except ValueError: echo &"Error: move: invalid target square" return if board.grid[targetSquare.row, targetSquare.col].kind != Empty: flags = flags or Capture.uint16 if board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2: flags = flags or DoublePush.uint16 if len(moveString) == 5: # Promotion case moveString[4]: of 'b': flags = flags or PromoteToBishop.uint16 of 'n': flags = flags or PromoteToKnight.uint16 of 'q': flags = flags or PromoteToQueen.uint16 of 'r': flags = flags or PromoteToRook.uint16 else: echo &"Error: move: invalid promotion type" return let move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags) if board.makeMove(move) == emptyMove(): echo "Error: move: illegal move" proc main: int = ## Nimfish's control interface echo "Nimfish by nocturn9x (see LICENSE)" var board = newDefaultChessboard() while true: var cmd: seq[string] cmdStr: string try: stdout.write(">>> ") stdout.flushFile() cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '}) if cmdStr.len() == 0: continue cmd = cmdStr.splitWhitespace(maxsplit=2) case cmd[0]: of "clear": echo "\x1Bc" of "help": echo "TODO" of "go": handleGoCommand(board, cmd) of "position": handlePositionCommand(board, cmd) of "move": handleMoveCommand(board, cmd) of "pretty": echo board.pretty() of "undo": board.undoLastMove() else: echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." except IOError: echo "" return -1 except EOFError: echo "" return 0 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" 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) setControlCHook(proc () {.noconv.} = quit(0)) quit(main())