1985 lines
79 KiB
Nim
1985 lines
79 KiB
Nim
# 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 src/bitboards
|
||
import src/magics
|
||
import src/pieces
|
||
import src/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 inCheck(self: ChessBoard, side: PieceColor): bool
|
||
|
||
|
||
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)
|
||
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 (KING_BITBOARDS[square.uint] and king) != 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 (KNIGHT_BITBOARDS[square.uint] and knight.toBitboard()) != 0:
|
||
result = result or knight.toBitboard()
|
||
|
||
|
||
proc getSlidingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard =
|
||
## Returns the attack bitboard for the given square from
|
||
## the sliding pieces of the given side
|
||
let
|
||
sq = square.toBitboard()
|
||
queens = self.getBitboard(Queen, attacker)
|
||
rooks = self.getBitboard(Rook, attacker)
|
||
bishops = self.getBitboard(Bishop, attacker)
|
||
occupancy = self.getOccupancy()
|
||
result = Bitboard(0)
|
||
for rook in rooks:
|
||
let blockers = Rook.getRelevantBlockers(square)
|
||
if (getRookMoves(square, blockers) and rook.toBitboard()) != 0:
|
||
result = result or rook.toBitboard()
|
||
for bishop in bishops:
|
||
let blockers = Bishop.getRelevantBlockers(square)
|
||
if (getBishopMoves(square, blockers) and bishop.toBitboard()) != 0:
|
||
result = result or bishop.toBitboard()
|
||
for queen in queens:
|
||
let rookBlockers = Rook.getRelevantBlockers(square)
|
||
if (getRookMoves(square, rookBlockers) and queen.toBitboard()) != 0:
|
||
result = result or queen.toBitboard()
|
||
let bishopBlockers = Bishop.getRelevantBlockers(square)
|
||
if (getBishopMoves(square, bishopBlockers) and queen.toBitboard()) != 0:
|
||
result = result or queen.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)
|
||
result = result or self.getSlidingAttacks(square, attacker)
|
||
|
||
|
||
proc inCheck(self: ChessBoard, side: PieceColor): bool =
|
||
## Returns if the current side to move is in check
|
||
return self.getAttacksTo(self.getKingSquare(side), side.opposite()) != 0
|
||
|
||
|
||
proc canCastle(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] =
|
||
## Returns if the current side to move can castle
|
||
return (false, false) # TODO
|
||
|
||
|
||
proc 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 = sideToMove.getFirstRank() # 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 generateRookMovements(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateRookMoves to generate all non-capture
|
||
## rook moves
|
||
let
|
||
sideToMove = self.getSideToMove()
|
||
occupancy = self.getOccupancy()
|
||
friendlyPieces = self.getOccupancyFor(sideToMove)
|
||
rooks = self.getBitboard(Rook, sideToMove)
|
||
for square in rooks:
|
||
let blockers = occupancy and Rook.getRelevantBlockers(square)
|
||
var moveset = getRookMoves(square, blockers)
|
||
# Can't move over our own pieces
|
||
moveset = moveset and not friendlyPieces
|
||
for target in moveset:
|
||
moves.add(createMove(square, target))
|
||
|
||
|
||
proc generateRookCaptures(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateRookMoves to generate all capture
|
||
## rook moves
|
||
let
|
||
sideToMove = self.getSideToMove()
|
||
occupancy = self.getOccupancy()
|
||
enemyPieces = self.getCapturablePieces(sideToMove.opposite())
|
||
rooks = self.getBitboard(Rook, sideToMove)
|
||
for square in rooks:
|
||
let blockers = occupancy and Rook.getRelevantBlockers(square)
|
||
var moveset = getRookMoves(square, blockers)
|
||
# Can only cature enemy pieces
|
||
moveset = moveset and enemyPieces
|
||
for target in moveset:
|
||
moves.add(createMove(square, target, Capture))
|
||
|
||
|
||
proc generateRookMoves(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateSlidingMoves to generate rook moves
|
||
self.generateRookMovements(moves)
|
||
self.generateRookCaptures(moves)
|
||
|
||
|
||
proc generateBishopMovements(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateBishopMoves to generate all non-capture
|
||
## bishop moves
|
||
let
|
||
sideToMove = self.getSideToMove()
|
||
occupancy = self.getOccupancy()
|
||
friendlyPieces = self.getOccupancyFor(sideToMove)
|
||
bishops = self.getBitboard(Bishop, sideToMove)
|
||
for square in bishops:
|
||
let blockers = occupancy and Bishop.getRelevantBlockers(square)
|
||
var moveset = getBishopMoves(square, blockers)
|
||
# Can't move over our own pieces
|
||
moveset = moveset and not friendlyPieces
|
||
for target in moveset:
|
||
moves.add(createMove(square, target))
|
||
|
||
|
||
proc generateBishopCaptures(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateBishopMoves to generate all capture
|
||
## bishop moves
|
||
let
|
||
sideToMove = self.getSideToMove()
|
||
occupancy = self.getOccupancy()
|
||
enemyPieces = self.getCapturablePieces(sideToMove.opposite())
|
||
bishops = self.getBitboard(Bishop, sideToMove)
|
||
for square in bishops:
|
||
let blockers = occupancy and Bishop.getRelevantBlockers(square)
|
||
var moveset = getRookMoves(square, blockers)
|
||
# Can only cature enemy pieces
|
||
moveset = moveset and enemyPieces
|
||
for target in moveset:
|
||
moves.add(createMove(square, target, Capture))
|
||
|
||
|
||
proc generateBishopMoves(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateSlidingMoves to generate bishop moves
|
||
self.generateBishopMovements(moves)
|
||
self.generateBishopCaptures(moves)
|
||
|
||
|
||
proc generateQueenMovements(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateQueenMoves to generate all non-capture
|
||
## bishop moves
|
||
let
|
||
sideToMove = self.getSideToMove()
|
||
occupancy = self.getOccupancy()
|
||
friendlyPieces = self.getOccupancyFor(sideToMove)
|
||
queens = self.getBitboard(Queen, sideToMove)
|
||
for square in queens:
|
||
# A queen is just a rook plus a bishop in terms of move
|
||
# generation
|
||
let
|
||
rookBlockers = Rook.getRelevantBlockers(square) and occupancy
|
||
bishopBlockers = Bishop.getRelevantBlockers(square) and occupancy
|
||
rookMoves = getRookMoves(square, rookBlockers)
|
||
bishopMoves = getBishopMoves(square, bishopBlockers)
|
||
var moveset = rookMoves or bishopMoves
|
||
# Can't move over our own pieces
|
||
moveset = moveset and not friendlyPieces
|
||
for target in moveset:
|
||
moves.add(createMove(square, target))
|
||
|
||
|
||
proc generateQueenCaptures(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateQueenMoves to generate all capture
|
||
## queen moves
|
||
let
|
||
sideToMove = self.getSideToMove()
|
||
occupancy = self.getOccupancy()
|
||
enemyPieces = self.getCapturablePieces(sideToMove.opposite())
|
||
queens = self.getBitboard(Queen, sideToMove)
|
||
for square in queens:
|
||
# A queen is just a rook plus a bishop in terms of move
|
||
# generation
|
||
let
|
||
rookBlockers = Rook.getRelevantBlockers(square) and occupancy
|
||
bishopBlockers = Bishop.getRelevantBlockers(square) and occupancy
|
||
rookMoves = getRookMoves(square, rookBlockers)
|
||
bishopMoves = getBishopMoves(square, bishopBlockers)
|
||
var moveset = rookMoves or bishopMoves
|
||
# Can only capture the enemy pieces
|
||
moveset = moveset and enemyPieces
|
||
for target in moveset:
|
||
moves.add(createMove(square, target, Capture))
|
||
|
||
|
||
proc generateQueenMoves(self: ChessBoard, moves: var MoveList) =
|
||
## Helper of generateSlidingMoves to generate queen moves
|
||
self.generateQueenMovements(moves)
|
||
self.generateQueenCaptures(moves)
|
||
|
||
|
||
proc generateSlidingMoves(self: ChessBoard, moves: var MoveList) =
|
||
## Generates all legal sliding moves for the side to move
|
||
self.generateRookMoves(moves)
|
||
self.generateBishopMoves(moves)
|
||
self.generateQueenMoves(moves)
|
||
|
||
|
||
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 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.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)
|
||
self.generateSlidingMoves(moves)
|
||
|
||
|
||
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:
|
||
discard
|
||
# case piece.color:
|
||
# of White:
|
||
# if rowFromSquare(move.startSquare) == piece.getStartRank():
|
||
# if columnFromSquare(move.startSquare) == 0:
|
||
# # Queen side
|
||
# castlingAvailable.white.queen = false
|
||
# elif columnfromSquare(move.startSquare) == 7:
|
||
# # King side
|
||
# castlingAvailable.white.king = false
|
||
# of Black:
|
||
# if rowFromSquare(move.startSquare) == piece.getStartRank():
|
||
# if columnFromSquare(move.startSquare) == 0:
|
||
# # Queen side
|
||
# castlingAvailable.black.queen = false
|
||
# elif columnFromSquare(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(self.getSideToMove()):
|
||
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()}"
|
||
echo &"Turn: {self.getSideToMove()}"
|
||
echo &"Piece: {self.grid[move.startSquare].kind}"
|
||
echo &"Flags: {move.getFlags()}"
|
||
echo &"In check: {(if self.inCheck(self.getSideToMove()): \"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(self.getSideToMove()):
|
||
# 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(self.getSideToMove()): \"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
|
||
- atk: Print the attack bitboard of the given square for the side to move
|
||
- 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 "atk":
|
||
if len(cmd) != 2:
|
||
echo "error: atk: invalid number of arguments"
|
||
continue
|
||
try:
|
||
echo board.getAttacksTo(cmd[1].toSquare(), board.getSideToMove())
|
||
except ValueError:
|
||
echo "error: atk: invalid square"
|
||
continue
|
||
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(board.getSideToMove())
|
||
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(board.getSideToMove()): \"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)
|
||
|
||
setControlCHook(proc () {.noconv.} = quit(0))
|
||
quit(main())
|