CPG/src/Chess/board.nim

1807 lines
72 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
import bitboards
import pieces
import moves
type
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
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
# Positional bitboards for all pieces
pieces: 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]
# Index of the current position
currPos: int
# 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, 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)
# A bunch of getters
func getSideToMove*(self: ChessBoard): PieceColor {.inline.} =
## Returns the currently side 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 makeSquare(7, 4)
of Black:
return makeSquare(0, 4)
else:
discard
func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 7) else: makeSquare(0, 7))
func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 0) else: makeSquare(0, 0))
func longCastleKing(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 2) else: makeSquare(0, 5))
func shortCastleKing(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 6) else: makeSquare(0, 1))
func longCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 3) else: makeSquare(7, 5))
func shortCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(0, 0) else: makeSquare(0, 2))
proc newChessboard: ChessBoard =
## Returns a new, empty chessboard
new(result)
for i in 0..63:
result.grid[i] = nullPiece()
result.position = Position(enPassantSquare: nullSquare(), turn: 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 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.pieces.white.pawns
of Knight:
return self.position.pieces.white.knights
of Bishop:
return self.position.pieces.white.bishops
of Rook:
return self.position.pieces.white.rooks
of Queen:
return self.position.pieces.white.queens
of King:
return self.position.pieces.white.king
else:
discard
of Black:
case kind:
of Pawn:
return self.position.pieces.black.pawns
of Knight:
return self.position.pieces.black.knights
of Bishop:
return self.position.pieces.black.bishops
of Rook:
return self.position.pieces.black.rooks
of Queen:
return self.position.pieces.black.queens
of King:
return self.position.pieces.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 = makeSquare(row, column)
bitIndex = square.int8
# 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.pieces.black.pawns.uint64.uint64.setBit(bitIndex)
of Bishop:
result.position.pieces.black.bishops.uint64.setBit(bitIndex)
of Knight:
result.position.pieces.black.knights.uint64.setBit(bitIndex)
of Rook:
result.position.pieces.black.rooks.uint64.setBit(bitIndex)
of Queen:
result.position.pieces.black.queens.uint64.setBit(bitIndex)
of King:
if result.position.pieces.black.king != 0:
raise newException(ValueError, "invalid position: exactly one king of each color must be present")
result.position.pieces.black.king.uint64.setBit(bitIndex)
else:
discard
of White:
case piece.kind:
of Pawn:
result.position.pieces.white.pawns.uint64.setBit(bitIndex)
of Bishop:
result.position.pieces.white.bishops.uint64.setBit(bitIndex)
of Knight:
result.position.pieces.white.knights.uint64.setBit(bitIndex)
of Rook:
result.position.pieces.white.rooks.uint64.setBit(bitIndex)
of Queen:
result.position.pieces.white.queens.uint64.setBit(bitIndex)
of King:
if result.position.pieces.white.king != 0:
raise newException(ValueError, "invalid position: exactly one king of each color must be present")
result.position.pieces.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].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.updateAttackedSquares()
#[if result.inCheck(result.getSideToMove().opposite):
# Opponent king cannot be captured on the next move
raise newException(ValueError, "invalid position: opponent king can be captured")]#
if result.position.pieces.white.king == 0 or result.position.pieces.black.king == 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.pieces.white.pawns.uint64.countSetBits()
of Bishop:
return self.position.pieces.white.bishops.uint64.countSetBits()
of Knight:
return self.position.pieces.white.knights.uint64.countSetBits()
of Rook:
return self.position.pieces.white.rooks.uint64.countSetBits()
of Queen:
return self.position.pieces.white.queens.uint64.countSetBits()
of King:
return self.position.pieces.white.king.uint64.countSetBits()
else:
raise newException(ValueError, "invalid piece type")
of Black:
case kind:
of Pawn:
return self.position.pieces.black.pawns.uint64.countSetBits()
of Bishop:
return self.position.pieces.black.bishops.uint64.countSetBits()
of Knight:
return self.position.pieces.black.knights.uint64.countSetBits()
of Rook:
return self.position.pieces.black.rooks.uint64.countSetBits()
of Queen:
return self.position.pieces.black.queens.uint64.countSetBits()
of King:
return self.position.pieces.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)
proc getPiece*(self: ChessBoard, square: Square): Piece {.inline.} =
## Gets the piece at the given square
return self.grid[square]
proc getPiece*(self: ChessBoard, square: string): Piece {.inline.} =
## Gets the piece on the given square
## in algebraic notation
return self.getPiece(square.toSquare())
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 getKingSquare(self: ChessBoard, color: PieceColor = None): Square {.inline.} =
## Returns the square of the king for the given
## side (if it is None, the side to move is used)
var color = color
if color == None:
color = self.getSideToMove()
case color:
of White:
return self.position.pieces.white.king.toSquare()
of Black:
return self.position.pieces.black.king.toSquare()
else:
discard
proc getOccupancyFor(self: ChessBoard, color: PieceColor): Bitboard =
## Get the occupancy bitboard for every piece of the given color
case color:
of White:
let b = self.position.pieces.white
return b.pawns or b.knights or b.bishops or b.rooks or b.queens or b.king
of Black:
let b = self.position.pieces.black
return b.pawns or b.knights or b.bishops or b.rooks or b.queens or b.king
else:
# huh?
discard
proc getOccupancy(self: ChessBoard): Bitboard =
## Get the occupancy bitboard for every piece on
## the chessboard
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
proc getPawnAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Returns the attack bitboard for the given square from
## the pawns of the given side
let
sq = square.toBitboard()
pawns = self.getBitboard(Pawn, attacker)
bottomLeft = sq.backwardLeftRelativeTo(attacker)
bottomRight = sq.backwardRightRelativeTo(attacker)
return pawns and (bottomLeft or bottomRight)
proc getKingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Returns the attack bitboard for the given square from
## the king of the given side
result = Bitboard(0)
let
sq = square.toBitboard()
king = self.getBitboard(King, attacker)
if (sq and KING_BITBOARDS[square.uint64]) != 0:
result = result or sq
proc getKnightAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
## Returns the attack bitboard for the given square from
## the knights of the given side
let
sq = square.toBitboard()
knights = self.getBitboard(Knight, attacker)
result = Bitboard(0)
for knight in knights:
if (sq and KNIGHT_BITBOARDS[knight.uint64]) != 0:
result = result or knight.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)
let
squareBitboard = square.toBitboard()
result = result or self.getPawnAttacks(square, attacker)
result = result or self.getKingAttacks(square, attacker)
result = result or self.getKnightAttacks(square, attacker)
proc getCapturablePieces(self: ChessBoard, side: PieceColor): Bitboard {.inline.} =
## Returns the set of pieces of the given color that can
## be captured
# Just a handy helper to filter out the king and avoid code duplication
return self.getOccupancyFor(side) and not self.getBitboard(King, side)
proc generatePawnMovements(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all non-capture
## and non-promotion pawn moves
let
sideToMove = self.getSideToMove()
pawns = self.getBitboard(Pawn, sideToMove)
# We can only move to squares that are *not* occupied by another piece.
# We also cannot move to the last rank, as that will result in a promotion
# and is handled elsewhere
allowedSquares = not (self.getOccupancy() or sideToMove.getLastRank())
# Single push
for square in pawns.forwardRelativeTo(sideToMove) and allowedSquares:
moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square))
# Double push
let rank = if sideToMove == White: getRankMask(6) else: getRankMask(1) # Only pawns on their starting rank can double push
for square in (pawns and rank).doubleForwardRelativeTo(sideToMove) and allowedSquares:
moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush))
proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all capture
## pawn moves
let
sideToMove = self.getSideToMove()
nonSideToMove = sideToMove.opposite()
pawns = self.getBitboard(Pawn, sideToMove)
# We can only capture enemy pieces (except the king)
enemyPieces = self.getCapturablePieces(nonSideToMove)
rightMovement = pawns.forwardRightRelativeTo(sideToMove)
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
epTarget = self.getEnPassantTarget()
let epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0)
# Top right attacks
for square in rightMovement and enemyPieces:
moves.add(createMove(square.toBitboard().backwardLeftRelativeTo(sideToMove), square, Capture))
# Top left attacks
for square in leftMovement and enemyPieces:
moves.add(createMove(square.toBitboard().backwardRightRelativeTo(sideToMove), square, Capture))
# Special case for en passant
let
epLeft = epBitboard and leftMovement
epRight = epBitboard and rightMovement
if epLeft != 0:
moves.add(createMove(epBitboard.forwardLeftRelativeTo(nonSideToMove), epBitboard, EnPassant))
elif epRight != 0:
moves.add(createMove(epBitboard.forwardRightRelativeTo(nonSideToMove), epBitboard, EnPassant))
proc generatePawnPromotions(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all pawn promotion
## moves
let
sideToMove = self.getSideToMove()
pawns = self.getBitboard(Pawn, sideToMove)
occupancy = self.getOccupancy()
for square in pawns.forwardRelativeTo(sideToMove) and not occupancy and sideToMove.getLastRank():
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]:
moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square, promotion))
proc generatePawnMoves(self: ChessBoard, moves: var MoveList) =
## Generates all the legal pawn moves for the side to move
self.generatePawnMovements(moves)
self.generatePawnCaptures(moves)
self.generatePawnPromotions(moves)
proc generateSlidingMoves(self: ChessBoard, moves: var MoveList) =
## Generates all legal sliding moves for the side to move
proc generateKingMoves(self: ChessBoard, moves: var MoveList) =
## Generates all legal king moves for the side to move
let
sideToMove = self.getSideToMove()
king = self.getBitboard(King, sideToMove)
moveIdx = king.toSquare().uint64
allowedSquares = not self.getOccupancy()
nonSideToMove = sideToMove.opposite()
enemyPieces = self.getCapturablePieces(nonSideToMove)
# Regular moves
for square in KING_BITBOARDS[moveIdx] and allowedSquares:
moves.add(createMove(king, square))
# Captures
for square in KING_BITBOARDS[moveIdx] and enemyPieces:
moves.add(createMove(king, square, Capture))
proc generateKnightMoves(self: ChessBoard, moves: var MoveList)=
## Generates all the legal knight moves for the side to move
let
sideToMove = self.getSideToMove()
knights = self.getBitboard(Knight, sideToMove)
allowedSquares = not self.getOccupancy()
nonSideToMove = sideToMove.opposite()
enemyPieces = self.getCapturablePieces(nonSideToMove)
for square in knights:
# Regular moves
for target in KNIGHT_BITBOARDS[square.uint64] and allowedSquares:
moves.add(createMove(square, target))
# Captures
for target in KNIGHT_BITBOARDS[square.uint64] and enemyPieces:
moves.add(createMove(square, target, Capture))
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 players 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.pieces.black.bishops:
if bishop.isLightSquare():
lightSquare += 1
else:
darkSquare += 1
for bishop in self.position.pieces.white.bishops:
if bishop.isLightSquare():
lightSquare += 1
else:
darkSquare += 1
if darkSquare >= 1 and lightSquare >= 1:
return false
return true
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
# TODO: Check for draw by insufficient material
#[
if self.checkInsufficientMaterial():
return @[]
]#
# TODO: Check for repetitions (requires zobrist hashing + table)
self.generatePawnMoves(moves)
self.generateKingMoves(moves)
self.generateKnightMoves(moves)
# TODO: all pieces
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.pieces.white.pawns.uint64.clearBit(square.int8)
of Bishop:
self.position.pieces.white.bishops.uint64.clearBit(square.int8)
of Knight:
self.position.pieces.white.knights.uint64.clearBit(square.int8)
of Rook:
self.position.pieces.white.rooks.uint64.clearBit(square.int8)
of Queen:
self.position.pieces.white.queens.uint64.clearBit(square.int8)
of King:
self.position.pieces.white.king.uint64.clearBit(square.int8)
else:
discard
of Black:
case piece.kind:
of Pawn:
self.position.pieces.black.pawns.uint64.clearBit(square.int8)
of Bishop:
self.position.pieces.black.bishops.uint64.clearBit(square.int8)
of Knight:
self.position.pieces.black.knights.uint64.clearBit(square.int8)
of Rook:
self.position.pieces.black.rooks.uint64.clearBit(square.int8)
of Queen:
self.position.pieces.black.queens.uint64.clearBit(square.int8)
of King:
self.position.pieces.black.king.uint64.clearBit(square.int8)
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.pieces.white.pawns.uint64.setBit(square.int8)
of Bishop:
self.position.pieces.white.bishops.uint64.setBit(square.int8)
of Knight:
self.position.pieces.white.knights.uint64.setBit(square.int8)
of Rook:
self.position.pieces.white.rooks.uint64.setBit(square.int8)
of Queen:
self.position.pieces.white.queens.uint64.setBit(square.int8)
of King:
self.position.pieces.white.king.uint64.setBit(square.int8)
else:
discard
of Black:
case piece.kind:
of Pawn:
self.position.pieces.black.pawns.uint64.setBit(square.int8)
of Bishop:
self.position.pieces.black.bishops.uint64.setBit(square.int8)
of Knight:
self.position.pieces.black.knights.uint64.setBit(square.int8)
of Rook:
self.position.pieces.black.rooks.uint64.setBit(square.int8)
of Queen:
self.position.pieces.black.queens.uint64.setBit(square.int8)
of King:
self.position.pieces.black.king.uint64.setBit(square.int8)
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] = nullPiece()
self.removePieceFromBitboard(square)
#[if attack:
self.updateAttackedSquares()]#
proc updateMovepieces(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.pieces.white.pawns.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.pawns.uint64.clearBit(move.startSquare.int8)
of Bishop:
self.position.pieces.white.bishops.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.bishops.uint64.clearBit(move.startSquare.int8)
of Knight:
self.position.pieces.white.knights.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.knights.uint64.clearBit(move.startSquare.int8)
of Rook:
self.position.pieces.white.rooks.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.rooks.uint64.clearBit(move.startSquare.int8)
of Queen:
self.position.pieces.white.queens.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.queens.uint64.clearBit(move.startSquare.int8)
of King:
self.position.pieces.white.king.uint64.setBit(move.targetSquare.int8)
self.position.pieces.white.king.uint64.clearBit(move.startSquare.int8)
else:
discard
of Black:
case piece.kind:
of Pawn:
self.position.pieces.black.pawns.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.pawns.uint64.clearBit(move.startSquare.int8)
of Bishop:
self.position.pieces.black.bishops.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.bishops.uint64.clearBit(move.startSquare.int8)
of Knight:
self.position.pieces.black.knights.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.knights.uint64.clearBit(move.startSquare.int8)
of Rook:
self.position.pieces.black.rooks.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.rooks.uint64.clearBit(move.startSquare.int8)
of Queen:
self.position.pieces.black.queens.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.queens.uint64.clearBit(move.startSquare.int8)
of King:
self.position.pieces.black.king.uint64.setBit(move.targetSquare.int8)
self.position.pieces.black.king.uint64.clearBit(move.startSquare.int8)
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.updateMovePieces(move)
# Empty out the starting square
self.grid[move.startSquare] = nullPiece()
# 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 = nullSquare()
# 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.toBitboard().backwardRelativeTo(piece.color).toSquare()
# Castling check: have the rooks moved?
if piece.kind == Rook:
case piece.color:
of White:
if rowFromSquare(move.startSquare) == piece.getStartRank():
if colFromSquare(move.startSquare) == 0:
# Queen side
castlingAvailable.white.queen = false
elif colfromSquare(move.startSquare) == 7:
# King side
castlingAvailable.white.king = false
of Black:
if rowFromSquare(move.startSquare) == piece.getStartRank():
if colFromSquare(move.startSquare) == 0:
# Queen side
castlingAvailable.black.queen = false
elif colFromSquare(move.startSquare) == 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.getSideToMove().opposite,
castlingAvailable: castlingAvailable,
enPassantSquare: enPassantTarget,
pieces: self.position.pieces
)
# Update position metadata
if move.isCastling():
# Move the rook onto the
# correct file when castling
var
square: Square
target: Square
flag: MoveFlag
if move.getCastlingType() == CastleShort:
square = piece.color.kingSideRook()
target = shortCastleRook(piece.color)
flag = CastleShort
else:
square = piece.color.queenSideRook()
target = longCastleRook(piece.color)
flag = CastleLong
let rook = self.grid[square]
self.movePiece(createMove(square, target, flag), attack=false)
if move.isEnPassant():
# Make the en passant pawn disappear
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare(), 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] = nullPiece()
for sq in self.position.pieces.white.pawns:
self.grid[sq] = Piece(color: White, kind: Pawn)
for sq in self.position.pieces.black.pawns:
self.grid[sq] = Piece(color: Black, kind: Pawn)
for sq in self.position.pieces.white.bishops:
self.grid[sq] = Piece(color: White, kind: Bishop)
for sq in self.position.pieces.black.bishops:
self.grid[sq] = Piece(color: Black, kind: Bishop)
for sq in self.position.pieces.white.knights:
self.grid[sq] = Piece(color: White, kind: Knight)
for sq in self.position.pieces.black.knights:
self.grid[sq] = Piece(color: Black, kind: Knight)
for sq in self.position.pieces.white.rooks:
self.grid[sq] = Piece(color: White, kind: Rook)
for sq in self.position.pieces.black.rooks:
self.grid[sq] = Piece(color: Black, kind: Rook)
for sq in self.position.pieces.white.queens:
self.grid[sq] = Piece(color: White, kind: Queen)
for sq in self.position.pieces.black.queens:
self.grid[sq] = Piece(color: Black, kind: Queen)
self.grid[self.position.pieces.white.king.toSquare()] = Piece(color: White, kind: King)
self.grid[self.position.pieces.black.king.toSquare()] = Piece(color: Black, kind: King)
proc unmakeMove*(self: ChessBoard) =
## Reverts to the previous board position,
## if one exists
if self.currPos > 0:
dec(self.currPos)
self.position = self.positions[self.currPos]
self.updateBoard()
proc redoMove*(self: ChessBoard) =
## Reverts to the next board position, if one
## exists. Only makes sense after a call to
## unmakeMove
if self.positions.high() > self.currPos:
inc(self.currPos)
self.position = self.positions[self.currPos]
self.updateBoard()
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 =
if piece.color == White:
return char(piece.kind).toUpperAscii()
return char(piece.kind)
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.getSideToMove() == 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() == nullSquare():
result &= "-"
else:
result &= self.getEnPassantTarget().toAlgebraic()
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
var moves = MoveList()
self.generateMoves(moves)
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.toAlgebraic()}{move.targetSquare.toAlgebraic()}{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.getSideToMove())
echo &"Ply (from root): {self.position.plyFromRoot}"
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}, from ({move.startSquare.rank}, {move.startSquare.file}) to ({move.targetSquare.rank}, {move.targetSquare.file})"
echo &"Turn: {self.getSideToMove()}"
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() != nullSquare():
echo self.getEnPassantTarget().toAlgebraic()
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.getSideToMove())
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.unmakeMove()
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.toAlgebraic()}{move.targetSquare.toAlgebraic()}{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: seq[MoveFlag]
try:
startSquare = moveString[0..1].toSquare()
except ValueError:
echo &"Error: move: invalid start square ({moveString[0..1]})"
return
try:
targetSquare = moveString[2..3].toSquare()
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.add(Capture)
elif board.grid[startSquare].kind == Pawn and abs(rowFromSquare(startSquare) - rowFromSquare(targetSquare)) == 2:
flags.add(DoublePush)
if len(moveString) == 5:
# Promotion
case moveString[4]:
of 'b':
flags.add(PromoteToBishop)
of 'n':
flags.add(PromoteToKnight)
of 'q':
flags.add(PromoteToQueen)
of 'r':
flags.add(PromoteToRook)
else:
echo &"Error: move: invalid promotion type"
return
var move = createMove(startSquare, targetSquare, flags)
let piece = board.getPiece(move.startSquare)
if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare():
if move.targetSquare == longCastleKing(piece.color):
move.flags = move.flags or CastleLong.uint16
elif move.targetSquare == shortCastleKing(piece.color):
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 == nullMove():
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]]) == nullMove():
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]]) == nullMove():
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 <depth> [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 <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 <args>: Shorthand for "position <args>"
- get <square>: 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.unmakeMove()
of "turn":
echo &"Active color: {board.getSideToMove()}"
of "ep":
let target = board.getEnPassantTarget()
if target != nullSquare():
echo &"En passant target: {target.toAlgebraic()}"
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.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
of "check":
echo &"{board.getSideToMove()} 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"
proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) =
var i = 0
for square in bitboard:
doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}"
inc(i)
if i != squares.len():
doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})"
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. This is testing the FEN
# parser
# 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)
# Ensure our bitboards match with the board
let
whitePawns = b.getBitboard(Pawn, White)
whiteKnights = b.getBitboard(Knight, White)
whiteBishops = b.getBitboard(Bishop, White)
whiteRooks = b.getBitboard(Rook, White)
whiteQueens = b.getBitboard(Queen, White)
whiteKing = b.getBitboard(King, White)
blackPawns = b.getBitboard(Pawn, Black)
blackKnights = b.getBitboard(Knight, Black)
blackBishops = b.getBitboard(Bishop, Black)
blackRooks = b.getBitboard(Rook, Black)
blackQueens = b.getBitboard(Queen, Black)
blackKing = b.getBitboard(King, Black)
whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)]
whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)]
whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)]
whiteRookSquares = @[makeSquare(7'i8, 0'i8), makeSquare(7, 7)]
whiteQueenSquares = @[makeSquare(7'i8, 3'i8)]
whiteKingSquares = @[makeSquare(7'i8, 4'i8)]
blackPawnSquares = @[makeSquare(1'i8, 0'i8), makeSquare(1, 1), makeSquare(1, 2), makeSquare(1, 3), makeSquare(1, 4), makeSquare(1, 5), makeSquare(1, 6), makeSquare(1, 7)]
blackKnightSquares = @[makeSquare(0'i8, 1'i8), makeSquare(0, 6)]
blackBishopSquares = @[makeSquare(0'i8, 2'i8), makeSquare(0, 5)]
blackRookSquares = @[makeSquare(0'i8, 0'i8), makeSquare(0, 7)]
blackQueenSquares = @[makeSquare(0'i8, 3'i8)]
blackKingSquares = @[makeSquare(0'i8, 4'i8)]
testPieceBitboard(whitePawns, whitePawnSquares)
testPieceBitboard(whiteKnights, whiteKnightSquares)
testPieceBitboard(whiteBishops, whiteBishopSquares)
testPieceBitboard(whiteRooks, whiteRookSquares)
testPieceBitboard(whiteQueens, whiteQueenSquares)
testPieceBitboard(whiteKing, whiteKingSquares)
testPieceBitboard(blackPawns, blackPawnSquares)
testPieceBitboard(blackKnights, blackKnightSquares)
testPieceBitboard(blackBishops, blackBishopSquares)
testPieceBitboard(blackRooks, blackRookSquares)
testPieceBitboard(blackQueens, blackQueenSquares)
testPieceBitboard(blackKing, blackKingSquares)
var m = MoveList()
b.generateMoves(m)
echo &"There are {len(m)} legal moves for {b.getSideToMove()} at {b.toFEN()}: "
for move in m:
echo " - ", move.startSquare, move.targetSquare, " ", move.getFlags()
echo b.pretty()
echo b.getAttacksTo("f3".toSquare(), White)
# setControlCHook(proc () {.noconv.} = quit(0))
# quit(main())