# 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/times import std/math import std/bitops 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', Direction* = enum ## A move direction enumeration Forward, Backward, Left, Right ForwardLeft, ForwardRight, BackwardLeft, BackwardRight 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 Bitboard* = distinct uint64 Square* = tuple[rank, file: int8] Attacked = seq[tuple[source, target, direction: Square]] Pieces = tuple[king: Square, queens: seq[Square], rooks: seq[Square], bishops: seq[Square], knights: seq[Square], pawns: seq[Square]] CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64] Move* = object ## A chess move startSquare*: Square targetSquare*: Square flags*: uint16 Position* = object ## A chess position # Did the rooks on either side or 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: int8 # 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: int8 # En passant target square (see https://en.wikipedia.org/wiki/En_passant) enPassantSquare*: Square # Active color turn: PieceColor # Bitboards for all pieces bitboards: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]] ChessBoard* = ref object ## A chess board object # The actual board where pieces live # (flattened 8x8 matrix) 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 func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) func emptySquare*: Square {.inline.} = (-1 , -1) func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) func algebraicToSquare*(s: string): Square {.inline.} proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} func emptyMove*: Move {.inline.} = Move(startSquare: emptySquare(), targetSquare: emptySquare()) func `+`*(a, b: Square): Square {.inline.} = (a.rank + b.rank, a.file + b.file) func `-`*(a: Square): Square {.inline.} = (-a.rank, -a.file) func `-`*(a, b: Square): Square{.inline.} = (a.rank - b.rank, a.file - b.file) func isValid*(a: Square): bool {.inline.} = a.rank in 0..7 and a.file in 0..7 func isLightSquare(a: Square): bool {.inline.} = (a.rank + a.file and 2) == 0 proc generateMoves(self: ChessBoard, square: Square): seq[Move] # proc getAttackers*(self: ChessBoard, square: Square, color: PieceColor): seq[Square] # proc getAttackFor*(self: ChessBoard, source, target: Square): tuple[source, target, direction: Square] # proc isAttacked*(self: ChessBoard, square: Square, 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, square: Square, piece: Piece) # proc updateAttackedSquares(self: ChessBoard) # proc updateSlidingAttacks(self: ChessBoard) # proc getPinnedDirections(self: ChessBoard, square: Square): seq[Square] # proc getAttacks*(self: ChessBoard, square: Square): Attacked # proc getSlidingAttacks(self: ChessBoard, square: Square): tuple[attacks: Attacked, pins: Attacked] proc inCheck*(self: ChessBoard, color: PieceColor = None): bool proc toFEN*(self: ChessBoard): string proc undoLastMove*(self: ChessBoard) proc movePiece(self: ChessBoard, move: Move, attack: bool = true) proc removePiece(self: ChessBoard, square: Square, attack: bool = true) proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = for x in other: self.add(x) proc updateBoard*(self: ChessBoard) func createMove(startSquare, targetSquare: string, flags: seq[MoveFlag] = @[]): Move = result = Move(startSquare: startSquare.algebraicToSquare(), targetSquare: targetSquare.algebraicToSquare(), flags: Default.uint16) for flag in flags: result.flags = result.flags or flag.uint16 func makeSquare(rank, file: SomeInteger): Square = (rank: rank.int8, file: file.int8) # 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): Square {.inline.} = (if color == White: (-1, -1) else: (1, 1)) func topRightDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (-1, 1) else: (1, -1)) func bottomLeftDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (1, -1) else: (-1, 1)) func bottomRightDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (1, 1) else: (-1, -1)) func leftSide(color: PieceColor): Square {.inline.} = (if color == White: (0, -1) else: (0, 1)) func rightSide(color: PieceColor): Square {.inline.} = (if color == White: (0, 1) else: (0, -1)) func topSide(color: PieceColor): Square {.inline.} = (if color == White: (-1, 0) else: (1, 0)) func bottomSide(color: PieceColor): Square {.inline.} = (if color == White: (1, 0) else: (-1, 0)) func doublePush(color: PieceColor): Square {.inline.} = (if color == White: (-2, 0) else: (2, 0)) func longCastleKing: Square {.inline.} = (0, -2) func shortCastleKing: Square {.inline.} = (0, 2) func longCastleRook: Square {.inline.} = (0, 3) func shortCastleRook: Square {.inline.} = (0, -2) func bottomLeftKnightMove(color: PieceColor, long: bool = true): Square {.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): Square {.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): Square {.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): Square {.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 squares rather than relative direction offsets func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: (7, 7) else: (0, 7)) func queenSideRook(color: PieceColor): Square {.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): Square {.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 getStartRank(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 getKingStartingSquare(color: PieceColor): Square {.inline.} = ## Retrieves the starting square of the king ## for the given color case color: of White: return (7, 4) of Black: return (0, 4) else: discard func getLastRank(color: PieceColor): int {.inline.} = ## Retrieves the square of the last ## rank 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) for i in 0..63: result.grid[i] = emptyPiece() result.position = Position(enPassantSquare: emptySquare(), turn: White) # Handy wrappers and utilities for handling low-level stuff func coordToIndex(row, col: SomeInteger): int {.inline.} = (row * 8) + col func coordToIndex(square: Square): int {.inline.} = coordToIndex(square.rank, square.file) func indexToCoord(index: SomeInteger): Square {.inline.} = ((index div 8).int8, (index mod 8).int8) # Indexing operations func `[]`(self: array[64, Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)] proc `[]=`(self: var array[64, Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.rank, square.file] func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.rank, square.file] = piece # Overloaded operators and functions for our bitboard type func `shl`(a: Bitboard, x: Positive): Bitboard = Bitboard(a.uint64 shl x) func `shr`(a: Bitboard, x: Positive): Bitboard = Bitboard(a.uint64 shr x) func `and`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 and b.uint64) func `shr`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 and b.uint64) func `+`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 + b.uint64) func `-`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 - b.uint64) func `div`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 div b.uint64) func `*`(a, b: Bitboard): Bitboard = Bitboard(a.uint64 * b.uint64) func `+`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 + b) func `-`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 - b.uint64) func `div`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 div b) func `*`(a: Bitboard, b: SomeUnsignedInt): Bitboard = Bitboard(a.uint64 * b) func `==`(a, b: Bitboard): bool {.inline.} = a.uint64 == b.uint64 func `==`(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 == b.uint64 func getFileMask(file: Positive): Bitboard = Bitboard(0x101010101010101'u64) shl file func getRankMask(rank: Positive): Bitboard = Bitboard(uint64.high()) shl Positive(8 * (rank + 1)) func squareToBitboard(square: SomeInteger): Bitboard = Bitboard(1'u64) shl square.uint64 func squareToBitboard(square: Square): Bitboard = squareToBitboard(coordToIndex(square)) proc bitboardToSquare(b: Bitboard): Square = Square(b.uint64.countTrailingZeroBits().indexToCoord()) func toBin(x: Bitboard, b: Positive = 64): string = toBin(BiggestInt(x), b) func toBin(x: uint64, b: Positive = 64): string = toBin(Bitboard(x), b) iterator items(self: Bitboard): Square = ## Iterates ove the given bitboard ## and returns all the squares that ## are set var bits = self while bits != 0: yield bits.bitboardToSquare() bits = bits and bits - 1 func pretty(self: Bitboard): string = iterator items(self: Bitboard): uint8 = ## Iterates over all the bits in the ## given bitboard for i in 0..63: yield self.uint64.bitsliced(i..i).uint8 iterator pairs(self: Bitboard): (int, uint8) = var i = 0 for bit in self: yield (i, bit) inc(i) ## Returns a prettyfied version of ## the given bitboard for i, bit in self: if i > 0 and i mod 8 == 0: result &= "\n" result &= $bit func computeDiagonalBitboards: array[14, Bitboard] {.compileTime.} = ## Precomputes all the bitboards for diagonals ## at compile time result[0] = Bitboard(0x8040201008040201'u64) var col = 1 i = 0 # Left to right while col < 8: result[col] = Bitboard(0x8040201008040201'u64) shl (8 * col) inc(col) inc(i) result[i] = Bitboard(0x102040810204080'u64) inc(i) col = 1 # Right to left while col < 7: result[i] = Bitboard(0x102040810204080'u64) shr (8 * col) inc(i) inc(col) const diagonalBitboards = computeDiagonalBitboards() func getDirectionMask(square: Square, color: PieceColor, direction: Direction): Bitboard = ## Get a bitmask for the given direction for a piece ## of the given color case color: of White: case direction: of Forward: return squareToBitboard(square) shl 8 of Backward: return squareToBitboard(square) shr 8 of ForwardRight: return squareToBitboard(square) shl 9 of ForwardLeft: return squareToBitboard(square) shr 9 of BackwardRight: return squareToBitboard(square) shl 17 of BackwardLeft: return squareToBitboard(square) shr 17 else: discard of Black: # The directions for black are just the opposite of those for white, # so we avoid duplicating any code case direction: of Forward: return getDirectionMask(square, White, Backward) of Backward: return getDirectionMask(square, White, Forward) of ForwardRight: return getDirectionMask(square, White, ForwardLeft) of ForwardLeft: return getDirectionMask(square, White, ForwardRight) of BackwardRight: return getDirectionMask(square, White, BackwardLeft) of BackwardLeft: return getDirectionMask(square, White, BackwardRight) else: discard else: discard func getDirectionMask(self: ChessBoard, square: Square, direction: Direction): Bitboard = ## Like getDirectionMask(), but used within the board context ## with a piece square and direction only return getDirectionMask(square, self.grid[square].color, direction) func getBitboard(self: ChessBoard, kind: PieceKind, color: PieceColor): Bitboard = ## Returns the positional bitboard for the given piece kind and color case color: of White: case kind: of Pawn: return self.position.bitboards.white.pawns of Knight: return self.position.bitboards.white.knights of Bishop: return self.position.bitboards.white.bishops of Rook: return self.position.bitboards.white.rooks of Queen: return self.position.bitboards.white.queens of King: return self.position.bitboards.white.king else: discard of Black: case kind: of Pawn: return self.position.bitboards.black.pawns of Knight: return self.position.bitboards.black.knights of Bishop: return self.position.bitboards.black.bishops of Rook: return self.position.bitboards.black.rooks of Queen: return self.position.bitboards.black.queens of King: return self.position.bitboards.black.king else: discard else: discard func getBitboard(self: ChessBoard, piece: Piece): Bitboard = ## 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 pieces: int # 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: Square = (row, column) bitIndex = square.coordToIndex() # We know for a fact these values are in our # enumeration, so all is good {.warning[HoleEnumConv]:off.} piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black) case piece.color: of Black: case piece.kind: of Pawn: result.position.bitboards.black.pawns.uint64.uint64.setBit(bitIndex) of Bishop: result.position.bitboards.black.bishops.uint64.setBit(bitIndex) of Knight: result.position.bitboards.black.knights.uint64.setBit(bitIndex) of Rook: result.position.bitboards.black.rooks.uint64.setBit(bitIndex) of Queen: result.position.bitboards.black.queens.uint64.setBit(bitIndex) of King: if result.position.bitboards.black.king != Bitboard(0'u64): raise newException(ValueError, "invalid position: exactly one king of each color must be present") result.position.bitboards.black.king.uint64.setBit(bitIndex) else: discard of White: case piece.kind: of Pawn: result.position.bitboards.white.pawns.uint64.setBit(bitIndex) of Bishop: result.position.bitboards.white.bishops.uint64.setBit(bitIndex) of Knight: result.position.bitboards.white.knights.uint64.setBit(bitIndex) of Rook: result.position.bitboards.white.rooks.uint64.setBit(bitIndex) of Queen: result.position.bitboards.white.queens.uint64.setBit(bitIndex) of King: if result.position.bitboards.white.king != 0: raise newException(ValueError, "invalid position: exactly one king of each color must be present") result.position.bitboards.white.king.uint64.setBit(bitIndex) else: discard else: discard 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.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].algebraicToSquare() # 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.bitboards.white.king == Bitboard(0) or result.position.bitboards.black.king == Bitboard(0): # 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 ## current position case color: of White: case kind: of Pawn: return self.position.bitboards.white.pawns.uint64.countSetBits() of Bishop: return self.position.bitboards.white.bishops.uint64.countSetBits() of Knight: return self.position.bitboards.white.knights.uint64.countSetBits() of Rook: return self.position.bitboards.white.rooks.uint64.countSetBits() of Queen: return self.position.bitboards.white.queens.uint64.countSetBits() of King: return self.position.bitboards.white.king.uint64.countSetBits() else: raise newException(ValueError, "invalid piece type") of Black: case kind: of Pawn: return self.position.bitboards.black.pawns.uint64.countSetBits() of Bishop: return self.position.bitboards.black.bishops.uint64.countSetBits() of Knight: return self.position.bitboards.black.knights.uint64.countSetBits() of Rook: return self.position.bitboards.black.rooks.uint64.countSetBits() of Queen: return self.position.bitboards.black.queens.uint64.countSetBits() of King: return self.position.bitboards.black.king.uint64.countSetBits() 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 rowToFile(row: int): int8 {.inline.} = ## Converts a row into our grid into ## a chess file const indeces: array[8, int8] = [8, 7, 6, 5, 4, 3, 2, 1] return indeces[row] func algebraicToSquare*(s: string): Square = ## Converts a square square 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 squareToAlgebraic*(square: Square): string {.inline.} = ## Converts a square from our internal rank/file ## notation to a square in algebraic notation return &"{char(uint8(square.file) + uint8('a'))}{rowToFile(square.rank)}" func getPiece*(self: ChessBoard, square: Square): Piece {.inline.} = ## Gets the piece at the given square return self.grid[square.rank, square.file] func getPiece*(self: ChessBoard, square: string): Piece {.inline.} = ## Gets the piece on the given square ## in algebraic notation return self.getPiece(square.algebraicToSquare()) 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) == Capture.uint16 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 func getFlags*(move: Move): seq[MoveFlag] = ## Gets all the flags of this move for flag in [EnPassant, Capture, DoublePush, CastleLong, CastleShort, PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: if (move.flags and flag.uint16) == flag.uint16: result.add(flag) if result.len() == 0: result.add(Default) func getKing(self: ChessBoard, color: PieceColor = None): Square {.inline.} = ## Returns the square of the king for the given ## color (if it is None, the active color is used) var color = color if color == None: color = self.getActiveColor() case color: of White: return self.position.bitboards.white.king.uint64.countTrailingZeroBits().indexToCoord() of Black: return self.position.bitboards.black.king.uint64.countTrailingZeroBits().indexToCoord() else: discard # TODO proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = false proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} = (false, false) #[ 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 inDoubleCheck*(self: ChessBoard, color: PieceColor = None): bool = ## Returns whether the given color's ## king is in double 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.getAttackers(self.position.pieces.white.king, Black).len() > 1 of Black: result = self.getAttackers(self.position.pieces.black.king, White).len() > 1 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 # Some of these checks may seem redundant, but we # perform them because they're less expensive # King is not on its starting square if self.getKing(color) != getKingStartingSquare(color): return (false, false) if self.inCheck(color): # King can not castle out of check return (false, false) if result.king or result.queen: var square: Square queenSide: Square kingSide: Square # 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: square = self.position.bitboards.white.king.bitboardToSquare() queenSide = color.leftSide() kingSide = color.rightSide() of Black: square = self.position.bitboards.black.king.bitboardToSquare() queenSide = color.rightSide() kingSide = color.leftSide() of None: # Unreachable discard # We only need to check for attacked squares up until # the point where the king arrives on the target castling # square, but we _also_ need to make sure the path is free # of obstacles for the rook to move past the king. This variable # becomes false once the king has arrived on its target square so # that we don't prevent castling when it would otherwise be allowed # (for an example see r3k2r/p1pNqpb1/bn2pnp1/3P4/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 1) var checkAttacks = true if result.king: # Short castle var currentSquare = square otherPiece: Piece while true: currentSquare = currentSquare + kingSide if currentSquare == color.kingSideRook(): break otherPiece = self.grid[currentSquare.rank, currentSquare.file] if otherPiece.color != None: result.king = false break if checkAttacks and self.isAttacked(currentSquare, color.opposite()): result.king = false break # King has arrived at the target square: we no longer # need to check whether subsequent squares are free from # attacks if currentSquare == shortCastleKing() + square: checkAttacks = false if result.queen: checkAttacks = true # Long castle var currentSquare = square otherPiece: Piece while true: currentSquare = currentSquare + queenSide if currentSquare == color.queenSideRook(): break otherPiece = self.grid[currentSquare.rank, currentSquare.file] if otherPiece.color != None: result.queen = false break if checkAttacks and self.isAttacked(currentSquare, color.opposite()): result.queen = false break if currentSquare == longCastleKing() + square: checkAttacks = false proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Square] = ## 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). ## Note that this function does not handle the special ## case of a friendly pawn being able to capture an enemy ## pawn that is checking our friendly king via en passant: ## that is handled internally by generatePawnMoves var king: Square case color: of White: king = self.position.pieces.white.king of Black: king = self.position.pieces.black.king else: return let attackers: seq[Square] = 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] var 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 square = attacker while square != king: square = square + attack.direction if not square.isValid(): break result.add(square) ]# proc generatePawnMoves(self: ChessBoard, square: Square): seq[Move] = ## Generates the possible moves for the pawn in the given ## square #[var piece = self.grid[square.rank, square.file] directions: seq[Square] = @[] assert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" # Pawns can move forward one square let forward = square + piece.color.topSide() # Only if the square is empty though if forward.isValid() and self.grid[forward].color == None: directions.add(piece.color.topSide()) # If the pawn is on its first rank, it can push two squares if square.rank == piece.getStartRank(): let double = square + piece.color.doublePush() # Check that both squares are empty if double.isValid() and self.grid[forward].color == None and self.grid[double].color == None: directions.add(piece.color.doublePush()) let enPassantTarget = self.getEnPassantTarget() enPassantPawn = enPassantTarget + piece.color.opposite().topSide() topLeft = piece.color.topLeftDiagonal() topRight = piece.color.topRightDiagonal() var enPassantLegal = false # They can also move one square on either of their # forward diagonals, but only for captures and en passant for diagonal in [topRight, topLeft]: let target = square + diagonal if target.isValid(): let otherPiece = self.grid[target] if target == enPassantTarget and self.grid[enPassantPawn].color == piece.color.opposite(): # En passant may be possible let targetPawn = self.grid[enPassantPawn] # Simulate the move and see if the king ends up in check self.removePiece(enPassantPawn, attack=false) self.removePiece(square, attack=false) self.spawnPiece(target, piece) self.updateAttackedSquares() if not self.inCheck(piece.color): # King is not in check after en passant: move is legal directions.add(diagonal) enPassantLegal = true # Reset what we just did and reupdate the attack metadata self.removePiece(target, attack=false) self.spawnPiece(square, piece) self.spawnPiece(enPassantPawn, targetPawn) self.updateAttackedSquares() elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King: # Can't capture the king! # A capture may be possible directions.add(diagonal) # Check for pins let pinned = self.getPinnedDirections(square) if pinned.len() > 0: var newDirections: seq[Square] = @[] for direction in directions: if direction in pinned: newDirections.add(direction) directions = newDirections let checked = self.inCheck() var resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) # If the check comes from a pawn and en passant is legal and would capture it, # we add that to the list of possible check resolutions if checked and enPassantLegal: let attackingPawn = self.getAttackFor(enPassantPawn, self.getKing(piece.color)) if attackingPawn.source == enPassantPawn: resolutions.add(enPassantTarget) var targetPiece: Piece for direction in directions: let target = square + direction if checked and target notin resolutions: continue targetPiece = self.grid[target] var flags: uint16 = Default.uint16 if targetPiece.color != None: flags = flags or Capture.uint16 elif abs(square.rank - target.rank) == 2: flags = flags or DoublePush.uint16 elif target == self.getEnPassantTarget(): flags = flags or EnPassant.uint16 if target.rank == piece.color.getLastRank(): # Pawn reached the other side of the board: generate all potential piece promotions for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: result.add(Move(startSquare: square, targetSquare: target, flags: promotionType.uint16 or flags)) continue result.add(Move(startSquare: square, targetSquare: target, flags: flags)) ]# proc generateSlidingMoves(self: ChessBoard, square: Square): seq[Move] = ## Generates moves for the sliding piece in the given square #[ let piece = self.grid[square.rank, square.file] assert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}" var directions: seq[Square] = @[] # 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(square) if pinned.len() > 0: var newDirections: seq[Square] = @[] for direction in directions: if direction in pinned: newDirections.add(direction) directions = newDirections let checked = self.inCheck() 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: Square = square otherPiece: Piece while true: square = square + direction # End of board reached if not square.isValid(): break otherPiece = self.grid[square] # A friendly piece is in the way if otherPiece.color == piece.color: break if checked and square notin resolutions: # We don't always break out of the loop because # we might resolve the check later if otherPiece.color == None: # We can still move in this direction, so maybe # the check can be resolved later continue else: # Our movement is blocked, switch to next direction 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: square, targetSquare: square, flags: Capture.uint16)) break # Target square is empty, keep going result.add(Move(startSquare: square, targetSquare: square)) ]# proc generateKingMoves(self: ChessBoard, square: Square): seq[Move] = ## Generates moves for the king in the given square #[ var piece = self.grid[square.rank, square.file] assert piece.kind == King, &"generateKingMoves called on a {piece.kind}" var directions: seq[Square] = @[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: Square = square + 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] if otherPiece.color == piece.color.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: square, targetSquare: square, flags: flag.uint16)) ]# proc generateKnightMoves(self: ChessBoard, square: Square): seq[Move] = ## Generates moves for the knight in the given square #[ var piece = self.grid[square.rank, square.file] assert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}" var directions: seq[Square] = @[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(square) 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: Square = square + direction # End of board reached if not square.isValid(): continue let otherPiece = self.grid[square] # 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: square, targetSquare: square, flags: Capture.uint16)) else: # Target square is empty result.add(Move(startSquare: square, targetSquare: square)) ]# proc checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool = ## Helper function for checkInsufficientMaterial let friendlyPawns = self.countPieces(Piece(kind: Pawn, color: color)) friendlyRooks = self.countPieces(Piece(kind: Rook, color: color)) friendlyQueens = self.countPieces(Piece(kind: Queen, color: color)) friendlyKnights = self.countPieces(Piece(kind: Knight, color: color)) friendlyBishops = self.countPieces(Piece(kind: Bishop, color: color)) enemyPawns = self.countPieces(Piece(kind: Pawn, color: color.opposite())) enemyRooks = self.countPieces(Piece(kind: Rook, color: color.opposite())) enemyQueens = self.countPieces(Piece(kind: Queen, color: color.opposite())) enemyKnights = self.countPieces(Piece(kind: Knight, color: color.opposite())) enemyBishops = self.countPieces(Piece(kind: Bishop, color: color.opposite())) if friendlyPawns > 0 or friendlyRooks > 0 or friendlyQueens > 0: return false if friendlyKnights >= 2: return false if friendlyKnights + friendlyBishops >= 2: return false if friendlyKnights >= 1 and (enemyPawns > 0 or enemyRooks > 0 or enemyBishops > 0 or enemyKnights > 0 or enemyQueens > 0): return false if friendlyBishops >= 1 and (enemyKnights > 0 or enemyPawns > 0): return false return true proc checkInsufficientMaterial(self: ChessBoard): bool = ## Checks if the given position has not enough material for either side to ## checkmate the enemy king. Note that the criteria as implemented here are ## not fully compliant with FIDE rules (they just define a draw by insufficient ## material as "[...] the position is such that the opponent cannot checkmate ## the player’s king by any possible series of legal moves.", which is really ## tricky to implement efficiently). For more info see https://www.reddit.com/r/chess/comments/se89db/a_writeup_on_definitions_of_insufficient_material/ if not (self.checkInsufficientMaterialPieceCount(White) and self.checkInsufficientMaterialPieceCount(Black)): return false let whiteBishops = self.countPieces(Piece(kind: Bishop, color: White)) blackBishops = self.countPieces(Piece(kind: Bishop, color: Black)) if blackBishops + whiteBishops >= 2: var darkSquare = 0 lightSquare = 0 for bishop in self.position.bitboards.black.bishops: if bishop.isLightSquare(): lightSquare += 1 else: darkSquare += 1 for bishop in self.position.bitboards.white.bishops: if bishop.isLightSquare(): lightSquare += 1 else: darkSquare += 1 if darkSquare >= 1 and lightSquare >= 1: return false return true proc generateMoves(self: ChessBoard, square: Square): seq[Move] = ## Returns the list of possible legal chess moves for the ## piece in the given square if self.position.halfMoveClock >= 100: # Draw by 50-move rule return @[] # TODO: Check for draw by insufficient material #[ if self.checkInsufficientMaterial(): return @[] ]# let piece = self.grid[square.rank, square.file] case piece.kind: of Queen, Bishop, Rook: return self.generateSlidingMoves(square) of Pawn: return self.generatePawnMoves(square) of King: return self.generateKingMoves(square) of Knight: return self.generateKnightMoves(square) 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, square: Square, color: PieceColor = None): bool = ## Returns whether the given square 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 == square: return true of White: for attack in self.position.attacked.white: if attack.target == square: return true of None: discard proc getAttackers*(self: ChessBoard, square: Square, color: PieceColor): seq[Square] = ## 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 == square: result.add(attack.source) of White: for attack in self.position.attacked.white: if attack.target == square: result.add(attack.source) of None: discard proc getAttacks*(self: ChessBoard, square: Square): Attacked = ## Returns all the squares attacked by the piece in the given ## square let piece = self.grid[square.rank, square.file] case piece.color: of Black: for attack in self.position.attacked.black: if attack.source == square: result.add(attack) of White: for attack in self.position.attacked.white: if attack.source == square: result.add(attack) of None: discard proc getAttackFor*(self: ChessBoard, source, target: Square): tuple[source, target, direction: Square] = ## Returns the first attack from the given source to the ## given target square result = (emptySquare(), emptySquare(), emptySquare()) let piece = self.grid[source] 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.algebraicToSquare()) func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Square], 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, square: Square): seq[Square] = ## Returns all the directions along which the piece in the given ## square is pinned. If the result is non-empty, the piece at ## the given square is only allowed to move along the directions ## returned by this function let piece = self.grid[square.rank, square.file] case piece.color: of None: discard of White: for pin in self.position.pinned.black: if pin.target == square: result.add(pin.direction) of Black: for pin in self.position.pinned.white: if pin.target == square: result.add(pin.direction) proc updatePawnAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares for sq in self.position.pieces.white.pawns: # Pawns are special in how they capture (i.e. the # squares they can regularly move to do not match # the squares they can capture on. Sneaky fucks) self.addAttack((sq, sq + White.topRightDiagonal(), White.topRightDiagonal()), White) self.addAttack((sq, sq + White.topLeftDiagonal(), White.topLeftDiagonal()), White) # We do the same thing for black for sq in self.position.pieces.black.pawns: self.addAttack((sq, sq + Black.topRightDiagonal(), Black.topRightDiagonal()), Black) self.addAttack((sq, sq + Black.topLeftDiagonal(), Black.topLeftDiagonal()), 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) self.addAttack((king, king + White.leftSide(), White.leftSide()), White) self.addAttack((king, king + White.rightSide(), White.rightSide()), White) self.addAttack((king, king + White.bottomSide(), White.bottomSide()), White) self.addAttack((king, king + White.topSide(), White.topSide()), 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) self.addAttack((king, king + Black.leftSide(), Black.leftSide()), Black) self.addAttack((king, king + Black.rightSide(), Black.rightSide()), Black) self.addAttack((king, king + Black.bottomSide(), Black.bottomSide()), Black) self.addAttack((king, king + Black.topSide(), Black.topSide()), 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, square: Square): tuple[attacks: Attacked, pins: Attacked] = ## Internal helper of updateSlidingAttacks var directions: seq[Square] = @[] let piece = self.grid[square.rank, square.file] 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 currentSquare = square otherPiece: Piece # Slide in this direction as long as it's possible while true: currentSquare = currentSquare + direction # End of board reached if not square.isValid(): break otherPiece = self.grid[square] # Target square is attacked (even if a friendly piece # is present, because in this case we're defending # it) result.attacks.add((square, currentSquare, 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 it to the king var otherSquare: Square = square behindPiece: Piece while true: otherSquare = otherSquare + direction if not otherSquare.isValid(): break behindPiece = self.grid[otherSquare] 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((square, currentSquare, direction)) result.pins.add((square, currentSquare, -direction)) if otherPiece.kind == Pawn and square.rank == otherPiece.getStartRank(): # The pinned piece is a pawn which hasn't moved yet: # we allow it to move two squares as well if square.file == square.file: # The pawn can only push two squares if it's being pinned from the # top side (relative to the pawn itself) result.pins.add((square, currentSquare, otherPiece.color.doublePush())) 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((square, 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) self.position.pinned.white.setLen(0) self.position.pinned.black.setLen(0) # Pawns self.updatePawnAttacks() # Sliding pieces self.updateSlidingAttacks() # Knights self.updateKnightAttacks() # Kings self.updateKingAttacks() ]# 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] case piece.color: of White: case piece.kind: of Pawn: self.position.bitboards.white.pawns.uint64.clearBit(square.coordToIndex()) of Bishop: self.position.bitboards.white.bishops.uint64.clearBit(square.coordToIndex()) of Knight: self.position.bitboards.white.knights.uint64.clearBit(square.coordToIndex()) of Rook: self.position.bitboards.white.rooks.uint64.clearBit(square.coordToIndex()) of Queen: self.position.bitboards.white.queens.uint64.clearBit(square.coordToIndex()) of King: self.position.bitboards.white.king.uint64.clearBit(square.coordToIndex()) else: discard of Black: case piece.kind: of Pawn: self.position.bitboards.black.pawns.uint64.clearBit(square.coordToIndex()) of Bishop: self.position.bitboards.black.bishops.uint64.clearBit(square.coordToIndex()) of Knight: self.position.bitboards.black.knights.uint64.clearBit(square.coordToIndex()) of Rook: self.position.bitboards.black.rooks.uint64.clearBit(square.coordToIndex()) of Queen: self.position.bitboards.black.queens.uint64.clearBit(square.coordToIndex()) of King: self.position.bitboards.black.king.uint64.clearBit(square.coordToIndex()) else: discard else: discard proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) = ## Adds the given piece at the given square in the chessboard to ## its respective bitboard case piece.color: of White: case piece.kind: of Pawn: self.position.bitboards.white.pawns.uint64.setBit(square.coordToIndex()) of Bishop: self.position.bitboards.white.bishops.uint64.setBit(square.coordToIndex()) of Knight: self.position.bitboards.white.knights.uint64.setBit(square.coordToIndex()) of Rook: self.position.bitboards.white.rooks.uint64.setBit(square.coordToIndex()) of Queen: self.position.bitboards.white.queens.uint64.setBit(square.coordToIndex()) of King: self.position.bitboards.white.king.uint64.setBit(square.coordToIndex()) else: discard of Black: case piece.kind: of Pawn: self.position.bitboards.black.pawns.uint64.setBit(square.coordToIndex()) of Bishop: self.position.bitboards.black.bishops.uint64.setBit(square.coordToIndex()) of Knight: self.position.bitboards.black.knights.uint64.setBit(square.coordToIndex()) of Rook: self.position.bitboards.black.rooks.uint64.setBit(square.coordToIndex()) of Queen: self.position.bitboards.black.queens.uint64.setBit(square.coordToIndex()) of King: self.position.bitboards.black.king.uint64.setBit(square.coordToIndex()) else: discard else: discard proc removePiece(self: ChessBoard, square: Square, attack: bool = true) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[square] self.grid[square] = emptyPiece() self.removePieceFromBitboard(square) #[if attack: self.updateAttackedSquares()]# proc updateMoveBitboards(self: ChessBoard, move: Move) = ## Updates our bitboard representation after a move: note that this ## does *not* handle captures, en passant, promotions etc. as those ## are already called by helpers such as removePiece() and spawnPiece() var bitboard: uint64 let piece = self.grid[move.startSquare] # TODO: Should we use our helpers or is it faster to branch only once? case piece.color: of White: case piece.kind: of Pawn: self.position.bitboards.white.pawns.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.white.pawns.uint64.clearBit(move.startSquare.coordToIndex()) of Bishop: self.position.bitboards.white.bishops.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.white.bishops.uint64.clearBit(move.startSquare.coordToIndex()) of Knight: self.position.bitboards.white.knights.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.white.knights.uint64.clearBit(move.startSquare.coordToIndex()) of Rook: self.position.bitboards.white.rooks.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.white.rooks.uint64.clearBit(move.startSquare.coordToIndex()) of Queen: self.position.bitboards.white.queens.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.white.queens.uint64.clearBit(move.startSquare.coordToIndex()) of King: self.position.bitboards.white.king.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.white.king.uint64.clearBit(move.startSquare.coordToIndex()) else: discard of Black: case piece.kind: of Pawn: self.position.bitboards.black.pawns.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.black.pawns.uint64.clearBit(move.startSquare.coordToIndex()) of Bishop: self.position.bitboards.black.bishops.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.black.bishops.uint64.clearBit(move.startSquare.coordToIndex()) of Knight: self.position.bitboards.black.knights.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.black.knights.uint64.clearBit(move.startSquare.coordToIndex()) of Rook: self.position.bitboards.black.rooks.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.black.rooks.uint64.clearBit(move.startSquare.coordToIndex()) of Queen: self.position.bitboards.black.queens.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.black.queens.uint64.clearBit(move.startSquare.coordToIndex()) of King: self.position.bitboards.black.king.uint64.setBit(move.targetSquare.coordToIndex()) self.position.bitboards.black.king.uint64.clearBit(move.startSquare.coordToIndex()) else: discard else: discard 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] let targetSquare = self.getPiece(move.targetSquare) if targetSquare.color != None: raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}") # Update positional metadata self.updateMoveBitboards(move) # Empty out the starting square self.grid[move.startSquare] = emptyPiece() # Actually move the piece on the board self.grid[move.targetSquare] = piece #[if attack: self.updateAttackedSquares()]# proc movePiece(self: ChessBoard, startSquare, targetSquare: Square, attack: bool = true) = ## Like the other movePiece(), but with two squares 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] var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable enPassantTarget = emptySquare() # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): halfMoveClock = 0 else: inc(halfMoveClock) if piece.color == Black: inc(fullMoveCount) 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.rank == piece.getStartRank(): if move.startSquare.file == 0: # Queen side castlingAvailable.white.queen = false elif move.startSquare.file == 7: # King side castlingAvailable.white.king = false of Black: if move.startSquare.rank == piece.getStartRank(): if move.startSquare.file == 0: # Queen side castlingAvailable.black.queen = false elif move.startSquare.file == 7: # King side castlingAvailable.black.king = false else: discard # Has a rook been captured? if move.isCapture(): let captured = self.grid[move.targetSquare] if captured.kind == Rook: case captured.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, enPassantSquare: enPassantTarget, bitboards: self.position.bitboards ) # Update position metadata if move.isCastling(): # Move the rook onto the # correct file when castling var square: Square target: Square flags: uint16 if move.getCastlingType() == CastleShort: square = piece.color.kingSideRook() target = shortCastleRook() flags = flags or CastleShort.uint16 else: square = piece.color.queenSideRook() target = longCastleRook() flags = flags or CastleLong.uint16 let rook = self.grid[square.rank, square.file] let move = Move(startSquare: square, targetSquare: square + target, flags: flags) self.movePiece(move, attack=false) if move.isEnPassant(): # Make the en passant pawn disappear self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false) if move.isCapture(): # Get rid of captured pieces self.removePiece(move.targetSquare, attack=false) # Move the piece to its target square and update attack metadata 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) 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 #self.updateAttackedSquares() proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) = ## Internal helper to "spawn" a given piece at the given ## square. Note that this will overwrite whatever piece ## was previously located there: use with caution. Does ## not automatically update the attacked square metadata self.addPieceToBitboard(square, piece) self.grid[square] = piece proc updateBoard*(self: ChessBoard) = ## Updates the internal grid representation ## according to the positional data stored ## in the chessboard for i in 0..63: self.grid[i] = emptyPiece() for sq in self.position.bitboards.white.pawns: self.grid[sq] = Piece(color: White, kind: Pawn) for sq in self.position.bitboards.black.pawns: self.grid[sq] = Piece(color: Black, kind: Pawn) for sq in self.position.bitboards.white.bishops: self.grid[sq] = Piece(color: White, kind: Bishop) for sq in self.position.bitboards.black.bishops: self.grid[sq] = Piece(color: Black, kind: Bishop) for sq in self.position.bitboards.white.knights: self.grid[sq] = Piece(color: White, kind: Knight) for sq in self.position.bitboards.black.knights: self.grid[sq] = Piece(color: Black, kind: Knight) for sq in self.position.bitboards.white.rooks: self.grid[sq] = Piece(color: White, kind: Rook) for sq in self.position.bitboards.black.rooks: self.grid[sq] = Piece(color: Black, kind: Rook) for sq in self.position.bitboards.white.queens: self.grid[sq] = Piece(color: White, kind: Queen) for sq in self.position.bitboards.black.queens: self.grid[sq] = Piece(color: Black, kind: Queen) self.grid[self.position.bitboards.white.king.bitboardToSquare()] = Piece(color: White, kind: King) self.grid[self.position.bitboards.black.king.bitboardToSquare()] = Piece(color: Black, kind: King) proc undoLastMove*(self: ChessBoard) = if self.positions.len() > 0: self.position = self.positions.pop() self.updateBoard() proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = ## Returns whether the given move is legal 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 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 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[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() == emptySquare(): result &= "-" else: result &= self.getEnPassantTarget().squareToAlgebraic() 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 not bulk: if len(moves) == 0 and self.inCheck(): result.checkmates = 1 # TODO: Should we count stalemates/draws? if ply == 0: result.nodes = 1 return elif 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.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}{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.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}, from ({move.startSquare.rank}, {move.startSquare.file}) to ({move.targetSquare.rank}, {move.targetSquare.file})" echo &"Turn: {self.getActiveColor()}" echo &"Piece: {self.grid[move.startSquare].kind}" echo &"Flags: {move.getFlags()}" 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() != emptySquare(): echo self.getEnPassantTarget().squareToAlgebraic() else: echo "None" echo "\n", self.pretty() self.doMove(move) if ply == 1: 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.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}{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": 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: let t = cpuTime() let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes echo &"\nNodes searched (bulk-counting: on): {nodes}" echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n" else: let t = cpuTime() 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 "" echo &"Time taken: {round(cpuTime() - t, 3)} seconds" except ValueError: echo "Error: go: perft: invalid depth" else: echo &"Error: go: unknown subcommand '{command[1]}'" proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} = 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: Square targetSquare: Square flags: uint16 try: startSquare = moveString[0..1].algebraicToSquare() except ValueError: echo &"Error: move: invalid start square ({moveString[0..1]})" return try: targetSquare = moveString[2..3].algebraicToSquare() except ValueError: echo &"Error: move: invalid target square ({moveString[2..3]})" return # Since the user tells us just the source and target square of the move, # we have to figure out all the flags by ourselves (whether it's a double # push, a capture, a promotion, castling, etc.) if board.grid[targetSquare].kind != Empty: flags = flags or Capture.uint16 elif board.grid[startSquare].kind == Pawn and abs(startSquare.rank - targetSquare.rank) == 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 var move = Move(startSquare: startSquare, targetSquare: targetSquare, flags: flags) if board.getPiece(move.startSquare).kind == King and move.startSquare == board.getActiveColor().getKingStartingSquare(): if move.targetSquare == move.startSquare + longCastleKing(): move.flags = move.flags or CastleLong.uint16 elif move.targetSquare == move.startSquare + shortCastleKing(): move.flags = move.flags or CastleShort.uint16 if move.targetSquare == board.getEnPassantTarget(): move.flags = move.flags or EnPassant.uint16 result = board.makeMove(move) if result == emptyMove(): echo &"Error: move: {moveString} is illegal" proc handlePositionCommand(board: var ChessBoard, command: seq[string]) = if len(command) < 2: echo "Error: position: invalid number of arguments" return # Makes sure we don't leave the board in an invalid state if # some error occurs var tempBoard: ChessBoard case command[1]: of "startpos": tempBoard = newDefaultChessboard() if command.len() > 2: let args = command[2].splitWhitespace() if args.len() > 0: var i = 0 while i < args.len(): case args[i]: of "moves": var j = i + 1 while j < args.len(): if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove(): return inc(j) inc(i) board = tempBoard of "fen": if len(command) == 2: echo &"Current position: {board.toFEN()}" return var args = command[2].splitWhitespace() fenString = "" stop = 0 for i, arg in args: if arg in ["moves", ]: break if i > 0: fenString &= " " fenString &= arg inc(stop) args = args[stop..^1] try: tempBoard = newChessboardFromFEN(fenString) except ValueError: echo &"error: position: {getCurrentExceptionMsg()}" return if args.len() > 0: var i = 0 while i < args.len(): case args[i]: of "moves": var j = i + 1 while j < args.len(): if handleMoveCommand(tempBoard, @["move", args[j]]) == emptyMove(): return inc(j) inc(i) board = tempBoard of "print": echo board of "pretty": echo board.pretty() else: echo &"error: position: unknown subcommand '{command[1]}'" return proc handleUCICommand(board: var ChessBoard, command: seq[string]) = echo "id name Nimfish 0.1" echo "id author Nocturn9x & Contributors (see LICENSE)" # TODO echo "uciok" const HELP_TEXT = """Nimfish help menu: - go: Begin a search Subcommands: - perft [options]: Run the performance test at the given depth (in ply) and print the results Options: - bulk: Enable bulk-counting (significantly faster, gives less statistics) - verbose: Enable move debugging (for each and every move, not recommended on large searches) Example: go perft 5 bulk - position: Get/set board position Subcommands: - fen [string]: Set the board to the given fen string if one is provided, or print the current position as a FEN string if no arguments are given - startpos: Set the board to the starting position - pretty: Pretty-print the current position - print: Print the current position using ASCII characters only Options: - moves {moveList}: Perform the given moves (space-separated, all-lowercase) in algebraic notation after the position is loaded. This option only applies to the "startpos" and "fen" subcommands: it is ignored otherwise Examples: - position startpos - position fen "..." moves a2a3 a7a6 - clear: Clear the screen - move : Perform the given move in algebraic notation - castle: Print castling rights for each side - check: Print if the current side to move is in check - undo, u: Undoes the last move. Can be used in succession - turn: Print which side is to move - ep: Print the current en passant target - pretty: Shorthand for "position pretty" - print: Shorthand for "position print" - fen: Shorthand for "position fen" - pos : Shorthand for "position " - get : Get the piece on the given square - uci: enter UCI mode (WIP) """ proc main: int = ## Nimfish's control interface echo "Nimfish by nocturn9x (see LICENSE)" var board = newDefaultChessboard() uciMode = false while true: var cmd: seq[string] cmdStr: string try: if not uciMode: 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 "uci": handleUCICommand(board, cmd) uciMode = true of "clear": echo "\x1Bc" of "help": echo HELP_TEXT of "go": handleGoCommand(board, cmd) of "position", "pos": handlePositionCommand(board, cmd) of "move": handleMoveCommand(board, cmd) of "pretty", "print", "fen": handlePositionCommand(board, @["position", cmd[0]]) of "undo", "u": board.undoLastMove() of "turn": echo &"Active color: {board.getActiveColor()}" of "ep": let target = board.getEnPassantTarget() if target != emptySquare(): echo &"En passant target: {target.squareToAlgebraic()}" else: echo "En passant target: None" of "get": if len(cmd) != 2: echo "error: get: invalid number of arguments" continue try: echo board.getPiece(cmd[1]) except ValueError: echo "error: get: invalid square" continue of "castle": let canCastle = board.canCastle() echo &"Castling rights for {($board.getActiveColor()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" of "check": echo &"{board.getActiveColor()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}" else: echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." except IOError: echo "" return 0 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 squares # 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) b.makeMove(createMove("a2", "a4", @[DoublePush])) let whitePawns = b.getBitboard(Pawn, White) echo whitePawns.pretty() for square in whitePawns: echo square doAssert b.getEnPassantTarget() == makeSquare(5, 0) #[setControlCHook(proc () {.noconv.} = quit(0)) quit(main())]#