# 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 nimfishpkg/bitboards import nimfishpkg/magics import nimfishpkg/pieces import nimfishpkg/moves import nimfishpkg/position import nimfishpkg/rays export bitboards, magics, pieces, moves, position type Chessboard* = ref object ## A chessboard # The actual board where pieces live grid: array[64, Piece] # The current position position*: Position # List of all previously reached positions positions*: seq[Position] # A bunch of simple utility functions and forward declarations proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} proc isLegal(self: Chessboard, move: Move): bool {.inline.} proc doMove*(self: Chessboard, move: Move) proc pretty*(self: Chessboard): string proc spawnPiece(self: Chessboard, square: Square, piece: Piece) proc toFEN*(self: Chessboard): string proc unmakeMove*(self: Chessboard) proc movePiece(self: Chessboard, move: Move) proc removePiece(self: Chessboard, square: Square) proc update*(self: Chessboard) func inCheck*(self: Chessboard): bool {.inline.} proc fromChar*(c: char): Piece proc updateChecksAndPins*(self: Chessboard) func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare()) func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare()) func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare()) func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare()) func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare()) proc newChessboard: Chessboard = ## Returns a new, empty chessboard new(result) for i in 0..63: result.grid[i] = nullPiece() result.position = Position(enPassantSquare: nullSquare(), sideToMove: White) # Indexing operations func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8] func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece func getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} = ## Returns the positional bitboard for the given piece kind and color return self.position.getBitboard(kind, color) func getBitboard*(self: Chessboard, piece: Piece): Bitboard {.inline.} = ## Returns the positional bitboard for the given piece type return self.getBitboard(piece.kind, piece.color) proc newChessboardFromFEN*(fen: string): Chessboard = ## Initializes a chessboard with the ## position encoded by the given FEN string result = newChessboard() var # Current square 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': let square = makeSquare(row, column) piece = c.fromChar() result.position.pieces[piece.color][piece.kind][].setBit(square) result.grid[square] = 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.sideToMove = White of 'b': result.position.sideToMove = Black else: raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'") of 2: # Castling availability case c: # TODO of '-': discard of 'K': result.position.castlingAvailability.white.king = true of 'Q': result.position.castlingAvailability.white.queen = true of 'k': result.position.castlingAvailability.black.king = true of 'q': result.position.castlingAvailability.black.queen = true else: raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights 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].toSquare() # 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.updateChecksAndPins() 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") func countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.inline.} = ## Returns the number of pieces with ## the given color and type in the ## current position return self.position.pieces[color][kind][].countSquares() 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 getPiece*(self: Chessboard, square: Square): Piece {.inline.} = ## Gets the piece at the given square return self.grid[square] func getPiece*(self: Chessboard, square: string): Piece {.inline.} = ## Gets the piece on the given square ## in algebraic notation return self.getPiece(square.toSquare()) func getOccupancyFor(self: Chessboard, color: PieceColor): Bitboard = ## Get the occupancy bitboard for every piece of the given color result = Bitboard(0) for b in self.position.pieces[color][]: result = result or b func getOccupancy(self: Chessboard): Bitboard {.inline.} = ## Get the occupancy bitboard for every piece on ## the chessboard result = self.getOccupancyFor(Black) or self.getOccupancyFor(White) func getPawnAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = ## Returns the locations of the pawns attacking the given square let sq = square.toBitboard() pawns = self.getBitboard(Pawn, attacker) bottomLeft = sq.backwardLeftRelativeTo(attacker) bottomRight = sq.backwardRightRelativeTo(attacker) return pawns and (bottomLeft or bottomRight) func getKingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} = ## Returns the location of the king if it is attacking the given square result = Bitboard(0) let king = self.getBitboard(King, attacker) if (getKingAttacks(square) and king) != 0: result = result or king func getKnightAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = ## Returns the locations of the knights attacking the given square let knights = self.getBitboard(Knight, attacker) result = Bitboard(0) for knight in knights: let knightBB = knight.toBitboard() if (getKnightAttacks(knight) and knightBB) != 0: result = result or knightBB proc getSlidingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = ## Returns the locations of the sliding pieces attacking the given square let queens = self.getBitboard(Queen, attacker) rooks = self.getBitboard(Rook, attacker) or queens bishops = self.getBitboard(Bishop, attacker) or queens occupancy = self.getOccupancy() squareBB = square.toBitboard() result = Bitboard(0) for rook in rooks: let blockers = occupancy and Rook.getRelevantBlockers(rook) moves = getRookMoves(rook, blockers) # Attack set intersects our chosen square if (moves and squareBB) != 0: result = result or rook.toBitboard() for bishop in bishops: let blockers = occupancy and Bishop.getRelevantBlockers(bishop) moves = getBishopMoves(bishop, blockers) if (moves and squareBB) != 0: result = result or bishop.toBitboard() proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard = ## Computes the attack bitboard for the given square from ## the given side result = Bitboard(0) result = result or self.getPawnAttacks(square, attacker) result = result or self.getKingAttacks(square, attacker) result = result or self.getKnightAttacks(square, attacker) result = result or self.getSlidingAttacks(square, attacker) proc isOccupancyAttacked*(self: Chessboard, square: Square, occupancy: Bitboard): bool = ## Returns whether the given square would be attacked by the ## enemy side if the board had the given occupancy. This function ## is necessary mostly to make sure sliding attacks can check the ## king properly: due to how we generate our attack bitboards, if ## the king moved backwards along a ray from a slider we would not ## consider it to be in check (because the ray stops at the first ## blocker). In order to fix that, in generateKingMoves() we use this ## function and pass in the board's occupancy without the moving king so ## that we can pick the correct magic bitboard and ray. Also, since this ## function doesn't need to generate all the attacks to know whether a ## given square is unsafe, it can short circuit at the first attack and ## exit early, unlike getAttacksTo let sideToMove = self.position.sideToMove nonSideToMove = sideToMove.opposite() knights = self.getBitboard(Knight, nonSideToMove) # Let's do the cheap ones first (the ones which are precomputed) if (getKnightAttacks(square) and knights) != 0: return true let king = self.getBitboard(King, nonSideToMove) if (getKingAttacks(square) and king) != 0: return true let queens = self.getBitboard(Queen, nonSideToMove) bishops = self.getBitboard(Bishop, nonSideToMove) or queens if (getBishopMoves(square, occupancy) and bishops) != 0: return true let rooks = self.getBitboard(Rook, nonSideToMove) or queens if (getRookMoves(square, occupancy) and rooks) != 0: return true # TODO: Precompute pawn moves as well? let pawns = self.getBitboard(Pawn, nonSideToMove) if (self.getPawnAttacks(square, nonSideToMove) and pawns) != 0: return true proc updateChecksAndPins*(self: Chessboard) = ## Updates internal metadata about checks and ## pinned pieces # *Ahem*, stolen from https://github.com/Ciekce/voidstar/blob/424ac4624011271c4d1dbd743602c23f6dbda1de/src/position.rs # Can you tell I'm a *great* coder? let sideToMove = self.position.sideToMove nonSideToMove = sideToMove.opposite() friendlyKing = self.getBitboard(King, sideToMove).toSquare() friendlyPieces = self.getOccupancyFor(sideToMove) enemyPieces = self.getOccupancyFor(nonSideToMove) # Update checks self.position.checkers = self.getAttacksTo(friendlyKing, nonSideToMove) # Update pins self.position.diagonalPins = Bitboard(0) self.position.orthogonalPins = Bitboard(0) let diagonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Bishop, nonSideToMove) orthogonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Rook, nonSideToMove) canPinDiagonally = diagonalAttackers and getBishopMoves(friendlyKing, enemyPieces) canPinOrthogonally = orthogonalAttackers and getRookMoves(friendlyKing, enemyPieces) for piece in canPinDiagonally: let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() # Is the pinning ray obstructed by any of our friendly pieces? If so, the # piece is pinned if (pinningRay and friendlyPieces).countSquares() > 0: self.position.diagonalPins = self.position.diagonalPins or pinningRay for piece in canPinOrthogonally: let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard() if (pinningRay and friendlyPieces).countSquares() > 0: self.position.orthogonalPins = self.position.orthogonalPins or pinningRay func inCheck(self: Chessboard): bool {.inline.} = ## Returns if the current side to move is in check return self.position.checkers != 0 proc canCastle*(self: Chessboard, side: PieceColor): tuple[king, queen: bool] = ## Returns if the current side to move can castle return (false, false) # TODO proc removePieceFromBitboard(self: Chessboard, square: Square) = ## Removes a piece at the given square in the chessboard from ## its respective bitboard let piece = self.grid[square] self.position.pieces[piece.color][piece.kind][].clearBit(square) proc addPieceToBitboard(self: Chessboard, square: Square, piece: Piece) = ## Adds the given piece at the given square in the chessboard to ## its respective bitboard self.position.pieces[piece.color][piece.kind][].setBit(square) proc spawnPiece(self: Chessboard, square: Square, piece: Piece) = ## Internal helper to "spawn" a given piece at the given ## square when not defined(danger): doAssert self.grid[square].kind == Empty self.addPieceToBitboard(square, piece) self.grid[square] = piece proc removePiece(self: Chessboard, square: Square) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[square] when not defined(danger): doAssert piece.kind != Empty and piece.color != None, self.toFEN() self.removePieceFromBitboard(square) self.grid[square] = nullPiece() proc movePiece(self: Chessboard, move: Move) = ## Internal helper to move a piece from ## its current square to a target square let piece = self.grid[move.startSquare] when not defined(danger): let targetSquare = self.getPiece(move.targetSquare) if targetSquare.color != None: raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}") # Update positional metadata self.removePiece(move.startSquare) self.spawnPiece(move.targetSquare, piece) 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 (i.e. during search) # Record final position for future reference self.positions.add(self.position) # Final checks let piece = self.grid[move.startSquare] when not defined(danger): doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}" var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingRights = self.position.castlingRights enPassantTarget = nullSquare() # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): # Number of half-moves since the last reversible half-move halfMoveClock = 0 else: inc(halfMoveClock) if piece.color == Black: inc(fullMoveCount) if move.isDoublePush(): enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() # Create new position self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, sideToMove: self.position.sideToMove.opposite(), castlingRights: castlingRights, enPassantSquare: enPassantTarget, pieces: self.position.pieces ) # Update position metadata if move.isEnPassant(): # Make the en passant pawn disappear self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()) if move.isCapture(): # Get rid of captured pieces self.removePiece(move.targetSquare) # Move the piece to its target square self.movePiece(move) # TODO: Castling! if move.isPromotion(): # Move is a pawn promotion: get rid of the pawn # and spawn a new piece self.removePiece(move.targetSquare) 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: # Unreachable discard # Updates checks and pins for the side to move self.updateChecksAndPins() proc update*(self: Chessboard) = ## Updates the internal grid representation ## according to the positional data stored ## in the chessboard for i in 0..63: self.grid[i] = nullPiece() for sq in self.position.pieces[White][Pawn][]: self.grid[sq] = Piece(color: White, kind: Pawn) for sq in self.position.pieces[Black][Pawn][]: self.grid[sq] = Piece(color: Black, kind: Pawn) for sq in self.position.pieces[White][Bishop][]: self.grid[sq] = Piece(color: White, kind: Bishop) for sq in self.position.pieces[Black][Bishop][]: self.grid[sq] = Piece(color: Black, kind: Bishop) for sq in self.position.pieces[White][Knight][]: self.grid[sq] = Piece(color: White, kind: Knight) for sq in self.position.pieces[Black][Knight][]: self.grid[sq] = Piece(color: Black, kind: Knight) for sq in self.position.pieces[White][Rook][]: self.grid[sq] = Piece(color: White, kind: Rook) for sq in self.position.pieces[Black][Rook][]: self.grid[sq] = Piece(color: Black, kind: Rook) for sq in self.position.pieces[White][Queen][]: self.grid[sq] = Piece(color: White, kind: Queen) for sq in self.position.pieces[Black][Queen][]: self.grid[sq] = Piece(color: Black, kind: Queen) for sq in self.position.pieces[White][King][]: self.grid[sq] = Piece(color: White, kind: King) for sq in self.position.pieces[Black][King][]: self.grid[sq] = Piece(color: Black, kind: King) proc unmakeMove*(self: Chessboard) = ## Reverts to the previous board position, ## if one exists self.position = self.positions.pop() self.update() proc generatePawnMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = let sideToMove = self.position.sideToMove pawns = self.getBitboard(Pawn, sideToMove) occupancy = self.getOccupancy() # We can only capture enemy pieces (except the king) enemyPieces = self.getOccupancyFor(sideToMove.opposite()) # We can only capture diagonally and forward rightMovement = pawns.forwardRightRelativeTo(sideToMove) leftMovement = pawns.forwardLeftRelativeTo(sideToMove) epTarget = self.position.enPassantSquare checkers = self.position.checkers diagonalPins = self.position.diagonalPins orthogonalPins = self.position.orthogonalPins promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7) # The rank where each color's side starts # TODO: Give names to ranks and files so we don't have to assume a # specific board layout when calling get(Rank|File)Mask startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1) var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0) let epPawn = if epBitboard == 0: Bitboard(0) else: epBitboard.forwardRelativeTo(sideToMove) # If we are in check, en passant is only possible if we'd capture the (only) # checking pawn with it if epBitboard != 0 and self.inCheck() and (epPawn and checkers).countSquares() == 0: epBitboard = Bitboard(0) # Single and double pushes let # If a pawn is pinned diagonally, it cannot move pushablePawns = pawns and not diagonalPins # Neither can it move if it's pinned orthogonally singlePushes = pushablePawns.forwardRelativeTo(sideToMove) and not occupancy and not orthogonalPins # Only pawns on their starting rank can double push doublePushes = (pushablePawns and startingRank).doubleForwardRelativeTo(sideToMove) and not occupancy and orthogonalPins proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = ## Helper of generateSlidingMoves to generate rook moves let sideToMove = self.position.sideToMove occupancy = self.getOccupancy() enemyPieces = self.getOccupancyFor(sideToMove.opposite()) rooks = self.getBitboard(Rook, sideToMove) queens = self.getBitboard(Queen, sideToMove) movableRooks = not self.position.diagonalPins and (queens or rooks) pinMask = self.position.orthogonalPins pinnedRooks = movableRooks and pinMask unpinnedRooks = movableRooks and not pinnedRooks for square in pinnedRooks: let blockers = occupancy and Rook.getRelevantBlockers(square) moveset = getRookMoves(square, blockers) for target in moveset and not occupancy and pinMask and mask: moves.add(createMove(square, target)) for target in moveset and enemyPieces and pinMask and mask: moves.add(createMove(square, target, Capture)) for square in unpinnedRooks: let blockers = occupancy and Rook.getRelevantBlockers(square) moveset = getRookMoves(square, blockers) for target in moveset and not occupancy and mask: moves.add(createMove(square, target)) for target in moveset and enemyPieces and mask: moves.add(createMove(square, target, Capture)) proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = ## Helper of generateSlidingMoves to generate bishop moves let sideToMove = self.position.sideToMove occupancy = self.getOccupancy() enemyPieces = self.getOccupancyFor(sideToMove.opposite()) bishops = self.getBitboard(Bishop, sideToMove) queens = self.getBitboard(Queen, sideToMove) movableBishops = not self.position.orthogonalPins and (queens or bishops) pinMask = self.position.diagonalPins pinnedBishops = movableBishops and pinMask unpinnedBishops = movableBishops and not pinnedBishops for square in pinnedBishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) moveset = getBishopMoves(square, blockers) for target in moveset and pinMask and mask: moves.add(createMove(square, target)) for target in moveset and enemyPieces and pinMask and mask: moves.add(createMove(square, target, Capture)) for square in unpinnedBishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) moveset = getBishopMoves(square, blockers) for target in moveset and mask: moves.add(createMove(square, target)) for target in moveset and enemyPieces and mask: moves.add(createMove(square, target, Capture)) proc generateKingMoves(self: Chessboard, moves: var MoveList) = ## Generates all legal king moves for the side to move let sideToMove = self.position.sideToMove king = self.getBitboard(King, sideToMove) occupancy = self.getOccupancy() nonSideToMove = sideToMove.opposite() enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) bitboard = getKingAttacks(king.toSquare()) noKingOccupancy = occupancy and not king for square in bitboard and not occupancy: if not self.isOccupancyAttacked(square, noKingOccupancy): moves.add(createMove(king, square)) for square in bitboard and enemyPieces: if not self.isOccupancyAttacked(square, noKingOccupancy): moves.add(createMove(king, square, Capture)) proc generateKnightMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) = ## Generates all the legal knight moves for the side to move let sideToMove = self.position.sideToMove knights = self.getBitboard(Knight, sideToMove) nonSideToMove = sideToMove.opposite() pinned = self.position.diagonalPins or self.position.orthogonalPins unpinnedKnights = knights and not pinned enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) for square in unpinnedKnights: let bitboard = getKnightAttacks(square) for target in bitboard and mask: moves.add(createMove(square, target)) for target in bitboard and enemyPieces: moves.add(createMove(square, target, Capture)) proc generateMoves*(self: Chessboard, moves: var MoveList) = ## Generates the list of all possible legal moves ## in the current position if self.position.halfMoveClock >= 100: # Draw by 50-move rule return let sideToMove = self.position.sideToMove # TODO: Check for draw by insufficient material # TODO: Check for repetitions (requires zobrist hashing + table) self.generateKingMoves(moves) if self.position.checkers.countSquares() > 1: # King is in double check: no need to generate any more # moves return if not self.inCheck(): # TODO: Castling discard # We pass a mask to our move generators to remove stuff # like our friendly pieces from the set of possible # target squares, as well as to ensure checks are not # ignored var mask: Bitboard if not self.inCheck(): # Not in check: cannot move over friendly pieces mask = not self.getOccupancyFor(sideToMove) else: # We *are* in check (from a single piece, because the two checks # case was handled above already). If the piece is a slider, we'll # extract the ray from it to our king and add the checking piece to # it, meaning the only legal moves are those that either block the # check or capture the checking piece. For other non-sliding pieces # the ray will be empty so the only legal move will be to capture # the checking piece let checker = self.position.checkers.lowestSquare() mask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard() self.generatePawnMoves(moves, mask) self.generateKnightMoves(moves, mask) self.generateRookMoves(moves, mask) self.generateBishopMoves(moves, mask) # Queens are just handled rooks + bishops proc isLegal(self: Chessboard, move: Move): bool {.inline.} = ## Returns whether the given move is legal var moves = MoveList() self.generateMoves(moves) return move in moves proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move if not self.isLegal(move): return nullMove() self.doMove(move) proc toChar*(piece: Piece): char = case piece.kind: of Bishop: result = 'b' of King: result = 'k' of Knight: result = 'n' of Pawn: result = 'p' of Queen: result = 'q' of Rook: result = 'r' else: discard if piece.color == White: result = result.toUpperAscii() proc fromChar*(c: char): Piece = var kind: PieceKind color = Black case c.toLowerAscii(): of 'b': kind = Bishop of 'k': kind = King of 'n': kind = Knight of 'p': kind = Pawn of 'q': kind = Queen of 'r': kind = Rook else: discard if c.isUpperAscii(): color = White result = Piece(kind: kind, color: color) proc `$`*(self: Chessboard): string = result &= "- - - - - - - -" var file = 8 for i in 0..7: result &= "\n" for j in 0..7: let piece = self.grid[makeSquare(i, j)] if piece.kind == Empty: result &= "x " continue result &= &"{piece.toChar()} " result &= &"{file}" dec(file) result &= "\n- - - - - - - -" result &= "\na b c d e f g h" proc toPretty*(piece: Piece): string = case piece.color: of White: 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 Black: 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 "\240\159\168\133" else: discard else: discard proc pretty*(self: Chessboard): string = ## Returns a colored version of the ## board for easier visualization var file = 8 for i in 0..7: if i > 0: result &= "\n" for j in 0..7: # Equivalent to (i + j) mod 2 # (I'm just evil) if ((i + j) and 1) == 0: result &= "\x1b[39;44;1m" else: result &= "\x1b[39;40;1m" let piece = self.grid[makeSquare(i, j)] if piece.kind == Empty: result &= " \x1b[0m" else: result &= &"{piece.toPretty()} \x1b[0m" result &= &" \x1b[33;1m{file}\x1b[0m" dec(file) 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[makeSquare(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.position.sideToMove == White: "w" else: "b") result &= " " # Castling availability let castleWhite = self.position.castlingAvailability.white let castleBlack = self.position.castlingAvailability.black if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): result &= "-" else: if castleWhite.king: result &= "K" if castleWhite.queen: result &= "Q" if castleBlack.king: result &= "k" if castleBlack.queen: result &= "q" result &= " " # En passant target if self.position.enPassantSquare == nullSquare(): result &= "-" else: result &= self.position.enPassantSquare.toAlgebraic() result &= " " # Halfmove clock result &= $self.position.halfMoveClock result &= " " # Fullmove number result &= $self.position.fullMoveCount when isMainModule: import nimfishpkg/tui import nimfishpkg/misc basicTests() setControlCHook(proc () {.noconv.} = quit(0)) quit(commandLoop())