2440 lines
99 KiB
Nim
2440 lines
99 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 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 generateMoves(self: ChessBoard, square: Square): seq[Move]
|
||
# proc getAttackers*(self: ChessBoard, square: Square, color: PieceColor): seq[Square]
|
||
# proc getAttackFor*(self: ChessBoard, source, target: Square): tuple[source, target, direction: Square]
|
||
# proc isAttacked*(self: ChessBoard, square: Square, color: PieceColor = None): bool
|
||
proc isLegal(self: ChessBoard, move: Move): bool {.inline.}
|
||
proc doMove(self: ChessBoard, move: Move)
|
||
proc pretty*(self: ChessBoard): string
|
||
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece)
|
||
# proc updateAttackedSquares(self: ChessBoard)
|
||
# proc updateSlidingAttacks(self: ChessBoard)
|
||
# proc getPinnedDirections(self: ChessBoard, square: Square): seq[Square]
|
||
# proc getAttacks*(self: ChessBoard, square: Square): Attacked
|
||
# proc getSlidingAttacks(self: ChessBoard, square: Square): tuple[attacks: Attacked, pins: Attacked]
|
||
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool
|
||
proc toFEN*(self: ChessBoard): string
|
||
proc 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)
|
||
|
||
|
||
# Due to our board layout, directions of movement are reversed for white and black, so
|
||
# we need these helpers to avoid going mad with integer tuples and minus signs everywhere
|
||
func topLeftDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (-1, -1) else: (1, 1))
|
||
func topRightDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (-1, 1) else: (1, -1))
|
||
func bottomLeftDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (1, -1) else: (-1, 1))
|
||
func bottomRightDiagonal(color: PieceColor): Square {.inline.} = (if color == White: (1, 1) else: (-1, -1))
|
||
func leftSide(color: PieceColor): Square {.inline.} = (if color == White: (0, -1) else: (0, 1))
|
||
func rightSide(color: PieceColor): Square {.inline.} = (if color == White: (0, 1) else: (0, -1))
|
||
func topSide(color: PieceColor): Square {.inline.} = (if color == White: (-1, 0) else: (1, 0))
|
||
func bottomSide(color: PieceColor): Square {.inline.} = (if color == White: (1, 0) else: (-1, 0))
|
||
func doublePush(color: PieceColor): Square {.inline.} = (if color == White: (-2, 0) else: (2, 0))
|
||
func longCastleKing: Square {.inline.} = (0, -2)
|
||
func shortCastleKing: Square {.inline.} = (0, 2)
|
||
func longCastleRook: Square {.inline.} = (0, 3)
|
||
func shortCastleRook: Square {.inline.} = (0, -2)
|
||
func bottomLeftKnightMove(color: PieceColor, long: bool = true): Square {.inline.} =
|
||
if color == White:
|
||
if long:
|
||
return (2, -1)
|
||
else:
|
||
return (1, -2)
|
||
elif color == Black:
|
||
if long:
|
||
return (-2, 1)
|
||
else:
|
||
return (1, -2)
|
||
|
||
|
||
func bottomRightKnightMove(color: PieceColor, long: bool = true): Square {.inline.} =
|
||
if color == White:
|
||
if long:
|
||
return (2, 1)
|
||
else:
|
||
return (1, 2)
|
||
elif color == Black:
|
||
if long:
|
||
return (-2, -1)
|
||
else:
|
||
return (-1, -2)
|
||
|
||
|
||
func topLeftKnightMove(color: PieceColor, long: bool = true): Square {.inline.} =
|
||
if color == White:
|
||
if long:
|
||
return (-2, -1)
|
||
else:
|
||
return (-1, -2)
|
||
elif color == Black:
|
||
if long:
|
||
return (2, 1)
|
||
else:
|
||
return (1, 2)
|
||
|
||
|
||
func topRightKnightMove(color: PieceColor, long: bool = true): Square {.inline.} =
|
||
if color == White:
|
||
if long:
|
||
return (-2, 1)
|
||
else:
|
||
return (-1, 2)
|
||
elif color == Black:
|
||
if long:
|
||
return (2, -1)
|
||
else:
|
||
return (-1, 2)
|
||
|
||
# These return absolute squares rather than relative direction offsets
|
||
func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: (7, 7) else: (0, 7))
|
||
func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: (7, 0) else: (0, 0))
|
||
|
||
|
||
|
||
# A bunch of getters
|
||
func getActiveColor*(self: ChessBoard): PieceColor {.inline.} =
|
||
## Returns the currently active color
|
||
## (turn of who has to move)
|
||
return self.position.turn
|
||
|
||
|
||
func getEnPassantTarget*(self: ChessBoard): Square {.inline.} =
|
||
## Returns the current en passant target square
|
||
return self.position.enPassantSquare
|
||
|
||
|
||
func getMoveCount*(self: ChessBoard): int {.inline.} =
|
||
## Returns the number of full moves that
|
||
## have been played
|
||
return self.position.fullMoveCount
|
||
|
||
|
||
func getHalfMoveCount*(self: ChessBoard): int {.inline.} =
|
||
## Returns the current number of half-moves
|
||
## since the last irreversible move
|
||
return self.position.halfMoveClock
|
||
|
||
|
||
func getStartRank(piece: Piece): int {.inline.} =
|
||
## Retrieves the starting row of
|
||
## the given piece inside our 8x8
|
||
## grid
|
||
case piece.color:
|
||
of None:
|
||
return -1
|
||
of White:
|
||
case piece.kind:
|
||
of Pawn:
|
||
return 6
|
||
else:
|
||
return 7
|
||
of Black:
|
||
case piece.kind:
|
||
of Pawn:
|
||
return 1
|
||
else:
|
||
return 0
|
||
|
||
|
||
func getKingStartingSquare(color: PieceColor): Square {.inline.} =
|
||
## Retrieves the starting square of the king
|
||
## for the given color
|
||
case color:
|
||
of White:
|
||
return (7, 4)
|
||
of Black:
|
||
return (0, 4)
|
||
else:
|
||
discard
|
||
|
||
|
||
func getLastRank(color: PieceColor): int {.inline.} =
|
||
## Retrieves the square of the last
|
||
## rank relative to the given color
|
||
case color:
|
||
of White:
|
||
return 0
|
||
of Black:
|
||
return 7
|
||
else:
|
||
return -1
|
||
|
||
|
||
proc newChessboard: ChessBoard =
|
||
## Returns a new, empty chessboard
|
||
new(result)
|
||
for i in 0..63:
|
||
result.grid[i] = nullPiece()
|
||
result.position = Position(enPassantSquare: nullSquare(), turn: White)
|
||
|
||
# Indexing operations
|
||
func `[]`(self: array[64, Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)]
|
||
proc `[]=`(self: var array[64, Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece
|
||
func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.rank, square.file]
|
||
func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.rank, square.file] = piece
|
||
|
||
|
||
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 = (row, column)
|
||
bitIndex = square.coordToIndex()
|
||
# We know for a fact these values are in our
|
||
# enumeration, so all is good
|
||
{.warning[HoleEnumConv]:off.}
|
||
piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black)
|
||
case piece.color:
|
||
of Black:
|
||
case piece.kind:
|
||
of Pawn:
|
||
result.position.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 != Bitboard(0'u64):
|
||
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].algebraicToSquare()
|
||
# Square metadata is 2 bytes long
|
||
inc(index)
|
||
of 4:
|
||
# Halfmove clock
|
||
var s = ""
|
||
while not fen[index].isSpaceAscii():
|
||
s.add(fen[index])
|
||
inc(index)
|
||
# Backtrack so the space is seen by the
|
||
# next iteration of the loop
|
||
dec(index)
|
||
result.position.halfMoveClock = parseInt(s).int8
|
||
of 5:
|
||
# Fullmove number
|
||
var s = ""
|
||
while index <= fen.high():
|
||
s.add(fen[index])
|
||
inc(index)
|
||
result.position.fullMoveCount = parseInt(s).int8
|
||
else:
|
||
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
|
||
inc(index)
|
||
# result.updateAttackedSquares()
|
||
#[if result.inCheck(result.getActiveColor().opposite):
|
||
# Opponent king cannot be captured on the next move
|
||
raise newException(ValueError, "invalid position: opponent king can be captured")]#
|
||
if result.position.pieces.white.king == Bitboard(0) or result.position.pieces.black.king == Bitboard(0):
|
||
# Both kings must be on the board
|
||
raise newException(ValueError, "invalid position: exactly one king of each color must be present")
|
||
|
||
|
||
proc newDefaultChessboard*: ChessBoard {.inline.} =
|
||
## Initializes a chessboard with the
|
||
## starting position
|
||
return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
||
|
||
|
||
proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int =
|
||
## Returns the number of pieces with
|
||
## the given color and type in the
|
||
## current position
|
||
case color:
|
||
of White:
|
||
case kind:
|
||
of Pawn:
|
||
return self.position.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)
|
||
|
||
|
||
func getPiece*(self: ChessBoard, square: Square): Piece {.inline.} =
|
||
## Gets the piece at the given square
|
||
return self.grid[square.rank, square.file]
|
||
|
||
|
||
func getPiece*(self: ChessBoard, square: string): Piece {.inline.} =
|
||
## Gets the piece on the given square
|
||
## in algebraic notation
|
||
return self.getPiece(square.algebraicToSquare())
|
||
|
||
|
||
func isPromotion*(move: Move): bool {.inline.} =
|
||
## Returns whether the given move is a
|
||
## pawn promotion
|
||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
|
||
if (move.flags and promotion.uint16) != 0:
|
||
return true
|
||
|
||
|
||
func getPromotionType*(move: Move): MoveFlag {.inline.} =
|
||
## Returns the promotion type of the given move.
|
||
## The return value of this function is only valid
|
||
## if isPromotion() returns true
|
||
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
|
||
if (move.flags and promotion.uint16) != 0:
|
||
return promotion
|
||
|
||
|
||
func isCapture*(move: Move): bool {.inline.} =
|
||
## Returns whether the given move is a
|
||
## cature
|
||
result = (move.flags and Capture.uint16) == Capture.uint16
|
||
|
||
|
||
func isCastling*(move: Move): bool {.inline.} =
|
||
## Returns whether the given move is a
|
||
## castle
|
||
for flag in [CastleLong, CastleShort]:
|
||
if (move.flags and flag.uint16) != 0:
|
||
return true
|
||
|
||
|
||
func getCastlingType*(move: Move): MoveFlag {.inline.} =
|
||
## Returns the castling type of the given move.
|
||
## The return value of this function is only valid
|
||
## if isCastling() returns true
|
||
for flag in [CastleLong, CastleShort]:
|
||
if (move.flags and flag.uint16) != 0:
|
||
return flag
|
||
|
||
|
||
func isEnPassant*(move: Move): bool {.inline.} =
|
||
## Returns whether the given move is an
|
||
## en passant capture
|
||
result = (move.flags and EnPassant.uint16) != 0
|
||
|
||
|
||
func isDoublePush*(move: Move): bool {.inline.} =
|
||
## Returns whether the given move is a
|
||
## double pawn push
|
||
result = (move.flags and DoublePush.uint16) != 0
|
||
|
||
|
||
func getFlags*(move: Move): seq[MoveFlag] =
|
||
## Gets all the flags of this move
|
||
for flag in [EnPassant, Capture, DoublePush, CastleLong, CastleShort,
|
||
PromoteToBishop, PromoteToKnight, PromoteToQueen,
|
||
PromoteToRook]:
|
||
if (move.flags and flag.uint16) == flag.uint16:
|
||
result.add(flag)
|
||
if result.len() == 0:
|
||
result.add(Default)
|
||
|
||
|
||
func getKing(self: ChessBoard, color: PieceColor = None): Square {.inline.} =
|
||
## Returns the square of the king for the given
|
||
## color (if it is None, the active color is used)
|
||
var color = color
|
||
if color == None:
|
||
color = self.getActiveColor()
|
||
case color:
|
||
of White:
|
||
return self.position.pieces.white.king.uint64.countTrailingZeroBits().indexToCoord()
|
||
of Black:
|
||
return self.position.pieces.black.king.uint64.countTrailingZeroBits().indexToCoord()
|
||
else:
|
||
discard
|
||
|
||
# TODO
|
||
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = false
|
||
proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} = (false, false)
|
||
|
||
#[
|
||
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
|
||
## Returns whether the given color's
|
||
## king is in check. If the color is
|
||
## set to None, checks are checked
|
||
## for the active color's king
|
||
var color = color
|
||
if color == None:
|
||
color = self.getActiveColor()
|
||
#[case color:
|
||
of White:
|
||
result = self.isAttacked(self.position.pieces.white.king, Black)
|
||
of Black:
|
||
result = self.isAttacked(self.position.pieces.black.king, White)
|
||
else:
|
||
# Unreachable
|
||
discard
|
||
]#
|
||
|
||
|
||
proc inDoubleCheck*(self: ChessBoard, color: PieceColor = None): bool =
|
||
## Returns whether the given color's
|
||
## king is in double check. If the color is
|
||
## set to None, checks are checked
|
||
## for the active color's king
|
||
var color = color
|
||
if color == None:
|
||
color = self.getActiveColor()
|
||
case color:
|
||
of White:
|
||
result = self.getAttackers(self.position.pieces.white.king, Black).len() > 1
|
||
of Black:
|
||
result = self.getAttackers(self.position.pieces.black.king, White).len() > 1
|
||
else:
|
||
# Unreachable
|
||
discard
|
||
|
||
|
||
proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} =
|
||
## Returns the sides on which castling is allowed
|
||
## for the given color. If the color is None, the
|
||
## currently active color is used
|
||
var color = color
|
||
if color == None:
|
||
color = self.getActiveColor()
|
||
# Check if castling rights are still available for moving side
|
||
case color:
|
||
of White:
|
||
result.king = self.position.castlingAvailable.white.king
|
||
result.queen = self.position.castlingAvailable.white.queen
|
||
of Black:
|
||
result.king = self.position.castlingAvailable.black.king
|
||
result.queen = self.position.castlingAvailable.black.queen
|
||
of None:
|
||
# Unreachable
|
||
discard
|
||
|
||
# Some of these checks may seem redundant, but we
|
||
# perform them because they're less expensive
|
||
|
||
# King is not on its starting square
|
||
if self.getKing(color) != getKingStartingSquare(color):
|
||
return (false, false)
|
||
if self.inCheck(color):
|
||
# King can not castle out of check
|
||
return (false, false)
|
||
if result.king or result.queen:
|
||
var
|
||
square: Square
|
||
queenSide: Square
|
||
kingSide: Square
|
||
# If the path between the king and rook on a given side is blocked, or any of the
|
||
# squares where the king would move to are attacked by the opponent, then castling
|
||
# is temporarily prohibited on that side
|
||
case color:
|
||
of White:
|
||
square = self.position.pieces.white.king.bitboardToSquare()
|
||
queenSide = color.leftSide()
|
||
kingSide = color.rightSide()
|
||
of Black:
|
||
square = self.position.pieces.black.king.bitboardToSquare()
|
||
queenSide = color.rightSide()
|
||
kingSide = color.leftSide()
|
||
of None:
|
||
# Unreachable
|
||
discard
|
||
|
||
# We only need to check for attacked squares up until
|
||
# the point where the king arrives on the target castling
|
||
# square, but we _also_ need to make sure the path is free
|
||
# of obstacles for the rook to move past the king. This variable
|
||
# becomes false once the king has arrived on its target square so
|
||
# that we don't prevent castling when it would otherwise be allowed
|
||
# (for an example see r3k2r/p1pNqpb1/bn2pnp1/3P4/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 1)
|
||
var checkAttacks = true
|
||
|
||
if result.king:
|
||
# Short castle
|
||
var
|
||
currentSquare = square
|
||
otherPiece: Piece
|
||
while true:
|
||
currentSquare = currentSquare + kingSide
|
||
|
||
if currentSquare == color.kingSideRook():
|
||
break
|
||
|
||
otherPiece = self.grid[currentSquare.rank, currentSquare.file]
|
||
|
||
if otherPiece.color != None:
|
||
result.king = false
|
||
break
|
||
|
||
if checkAttacks and self.isAttacked(currentSquare, color.opposite()):
|
||
result.king = false
|
||
break
|
||
|
||
# King has arrived at the target square: we no longer
|
||
# need to check whether subsequent squares are free from
|
||
# attacks
|
||
if currentSquare == shortCastleKing() + square:
|
||
checkAttacks = false
|
||
|
||
if result.queen:
|
||
checkAttacks = true
|
||
# Long castle
|
||
var
|
||
currentSquare = square
|
||
otherPiece: Piece
|
||
while true:
|
||
currentSquare = currentSquare + queenSide
|
||
|
||
if currentSquare == color.queenSideRook():
|
||
break
|
||
|
||
otherPiece = self.grid[currentSquare.rank, currentSquare.file]
|
||
|
||
if otherPiece.color != None:
|
||
result.queen = false
|
||
break
|
||
|
||
if checkAttacks and self.isAttacked(currentSquare, color.opposite()):
|
||
result.queen = false
|
||
break
|
||
|
||
if currentSquare == longCastleKing() + square:
|
||
checkAttacks = false
|
||
|
||
|
||
proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Square] =
|
||
## Returns the squares that need to be covered to
|
||
## resolve the current check (including capturing
|
||
## the checking piece). In case of double check, an
|
||
## empty list is returned (as the king must move).
|
||
## Note that this function does not handle the special
|
||
## case of a friendly pawn being able to capture an enemy
|
||
## pawn that is checking our friendly king via en passant:
|
||
## that is handled internally by generatePawnMoves
|
||
var king: Square
|
||
case color:
|
||
of White:
|
||
king = self.position.pieces.white.king
|
||
of Black:
|
||
king = self.position.pieces.black.king
|
||
else:
|
||
return
|
||
|
||
let attackers: seq[Square] = self.getAttackers(king, color.opposite())
|
||
if attackers.len() > 1:
|
||
# Double checks require to move the king
|
||
return @[]
|
||
let
|
||
attacker = attackers[0]
|
||
attackerPiece = self.grid[attacker]
|
||
|
||
var attack = self.getAttackFor(attacker, king)
|
||
# Capturing the piece resolves the check
|
||
result.add(attacker)
|
||
|
||
# Blocking the attack is also a viable strategy
|
||
# (unless the check is from a knight or a pawn,
|
||
# in which case either the king has to move or
|
||
# that piece has to be captured, but this is
|
||
# already implicitly handled by the loop below)
|
||
var square = attacker
|
||
while square != king:
|
||
square = square + attack.direction
|
||
if not square.isValid():
|
||
break
|
||
result.add(square)
|
||
]#
|
||
|
||
proc generatePawnMoves(self: ChessBoard, square: Square): seq[Move] =
|
||
## Generates the possible moves for the pawn in the given
|
||
## square
|
||
|
||
#[var
|
||
piece = self.grid[square.rank, square.file]
|
||
directions: seq[Square] = @[]
|
||
assert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
|
||
# Pawns can move forward one square
|
||
let forward = square + piece.color.topSide()
|
||
# Only if the square is empty though
|
||
if forward.isValid() and self.grid[forward].color == None:
|
||
directions.add(piece.color.topSide())
|
||
# If the pawn is on its first rank, it can push two squares
|
||
if square.rank == piece.getStartRank():
|
||
let double = square + piece.color.doublePush()
|
||
# Check that both squares are empty
|
||
if double.isValid() and self.grid[forward].color == None and self.grid[double].color == None:
|
||
directions.add(piece.color.doublePush())
|
||
let
|
||
enPassantTarget = self.getEnPassantTarget()
|
||
enPassantPawn = enPassantTarget + piece.color.opposite().topSide()
|
||
topLeft = piece.color.topLeftDiagonal()
|
||
topRight = piece.color.topRightDiagonal()
|
||
var enPassantLegal = false
|
||
# They can also move one square on either of their
|
||
# forward diagonals, but only for captures and en passant
|
||
for diagonal in [topRight, topLeft]:
|
||
let target = square + diagonal
|
||
if target.isValid():
|
||
let otherPiece = self.grid[target]
|
||
if target == enPassantTarget and self.grid[enPassantPawn].color == piece.color.opposite():
|
||
# En passant may be possible
|
||
let targetPawn = self.grid[enPassantPawn]
|
||
# Simulate the move and see if the king ends up in check
|
||
self.removePiece(enPassantPawn, attack=false)
|
||
self.removePiece(square, attack=false)
|
||
self.spawnPiece(target, piece)
|
||
self.updateAttackedSquares()
|
||
if not self.inCheck(piece.color):
|
||
# King is not in check after en passant: move is legal
|
||
directions.add(diagonal)
|
||
enPassantLegal = true
|
||
# Reset what we just did and reupdate the attack metadata
|
||
self.removePiece(target, attack=false)
|
||
self.spawnPiece(square, piece)
|
||
self.spawnPiece(enPassantPawn, targetPawn)
|
||
self.updateAttackedSquares()
|
||
elif otherPiece.color == piece.color.opposite() and otherPiece.kind != King: # Can't capture the king!
|
||
# A capture may be possible
|
||
directions.add(diagonal)
|
||
# Check for pins
|
||
let pinned = self.getPinnedDirections(square)
|
||
if pinned.len() > 0:
|
||
var newDirections: seq[Square] = @[]
|
||
for direction in directions:
|
||
if direction in pinned:
|
||
newDirections.add(direction)
|
||
directions = newDirections
|
||
let checked = self.inCheck()
|
||
var resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
||
# If the check comes from a pawn and en passant is legal and would capture it,
|
||
# we add that to the list of possible check resolutions
|
||
if checked and enPassantLegal:
|
||
let attackingPawn = self.getAttackFor(enPassantPawn, self.getKing(piece.color))
|
||
if attackingPawn.source == enPassantPawn:
|
||
resolutions.add(enPassantTarget)
|
||
var targetPiece: Piece
|
||
for direction in directions:
|
||
let target = square + direction
|
||
if checked and target notin resolutions:
|
||
continue
|
||
targetPiece = self.grid[target]
|
||
var flags: uint16 = Default.uint16
|
||
if targetPiece.color != None:
|
||
flags = flags or Capture.uint16
|
||
elif abs(square.rank - target.rank) == 2:
|
||
flags = flags or DoublePush.uint16
|
||
elif target == self.getEnPassantTarget():
|
||
flags = flags or EnPassant.uint16
|
||
if target.rank == piece.color.getLastRank():
|
||
# Pawn reached the other side of the board: generate all potential piece promotions
|
||
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
|
||
result.add(Move(startSquare: square, targetSquare: target, flags: promotionType.uint16 or flags))
|
||
continue
|
||
result.add(Move(startSquare: square, targetSquare: target, flags: flags))
|
||
]#
|
||
|
||
|
||
proc generateSlidingMoves(self: ChessBoard, square: Square): seq[Move] =
|
||
## Generates moves for the sliding piece in the given square
|
||
#[
|
||
let piece = self.grid[square.rank, square.file]
|
||
assert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}"
|
||
var directions: seq[Square] = @[]
|
||
|
||
# Only check in the right directions for the chosen piece
|
||
if piece.kind in [Bishop, Queen]:
|
||
directions.add(piece.color.topLeftDiagonal())
|
||
directions.add(piece.color.topRightDiagonal())
|
||
directions.add(piece.color.bottomLeftDiagonal())
|
||
directions.add(piece.color.bottomRightDiagonal())
|
||
if piece.kind in [Queen, Rook]:
|
||
directions.add(piece.color.topSide())
|
||
directions.add(piece.color.bottomSide())
|
||
directions.add(piece.color.rightSide())
|
||
directions.add(piece.color.leftSide())
|
||
let pinned = self.getPinnedDirections(square)
|
||
if pinned.len() > 0:
|
||
var newDirections: seq[Square] = @[]
|
||
for direction in directions:
|
||
if direction in pinned:
|
||
newDirections.add(direction)
|
||
directions = newDirections
|
||
let
|
||
checked = self.inCheck()
|
||
resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
||
for direction in directions:
|
||
# Slide in this direction as long as it's possible
|
||
var
|
||
square: Square = square
|
||
otherPiece: Piece
|
||
while true:
|
||
square = square + direction
|
||
# End of board reached
|
||
if not square.isValid():
|
||
break
|
||
otherPiece = self.grid[square]
|
||
# A friendly piece is in the way
|
||
if otherPiece.color == piece.color:
|
||
break
|
||
if checked and square notin resolutions:
|
||
# We don't always break out of the loop because
|
||
# we might resolve the check later
|
||
if otherPiece.color == None:
|
||
# We can still move in this direction, so maybe
|
||
# the check can be resolved later
|
||
continue
|
||
else:
|
||
# Our movement is blocked, switch to next direction
|
||
break
|
||
if otherPiece.color == piece.color.opposite:
|
||
# Target square contains an enemy piece: capture
|
||
# it and stop going any further
|
||
if otherPiece.kind != King:
|
||
# Can't capture the king
|
||
result.add(Move(startSquare: square, targetSquare: square, flags: Capture.uint16))
|
||
break
|
||
# Target square is empty, keep going
|
||
result.add(Move(startSquare: square, targetSquare: square))
|
||
]#
|
||
|
||
|
||
proc generateKingMoves(self: ChessBoard, square: Square): seq[Move] =
|
||
## Generates moves for the king in the given square
|
||
#[
|
||
var
|
||
piece = self.grid[square.rank, square.file]
|
||
assert piece.kind == King, &"generateKingMoves called on a {piece.kind}"
|
||
var directions: seq[Square] = @[piece.color.topLeftDiagonal(),
|
||
piece.color.topRightDiagonal(),
|
||
piece.color.bottomRightDiagonal(),
|
||
piece.color.bottomLeftDiagonal(),
|
||
piece.color.topSide(),
|
||
piece.color.bottomSide(),
|
||
piece.color.leftSide(),
|
||
piece.color.rightSide()]
|
||
# Castling
|
||
let canCastle = self.canCastle(piece.color)
|
||
if canCastle.queen:
|
||
directions.add(longCastleKing())
|
||
if canCastle.king:
|
||
directions.add(shortCastleKing())
|
||
var flag = Default
|
||
for direction in directions:
|
||
# Step in this direction once
|
||
let square: Square = square + direction
|
||
# End of board reached
|
||
if not square.isValid():
|
||
continue
|
||
if self.isAttacked(square, piece.color.opposite()):
|
||
continue
|
||
if direction == longCastleKing():
|
||
flag = CastleLong
|
||
elif direction == shortCastleKing():
|
||
flag = CastleShort
|
||
else:
|
||
flag = Default
|
||
let otherPiece = self.grid[square]
|
||
if otherPiece.color == piece.color.opposite():
|
||
flag = Capture
|
||
# A friendly piece is in the way, move onto the next direction
|
||
if otherPiece.color == piece.color:
|
||
continue
|
||
# Target square is empty or contains an enemy piece:
|
||
# All good for us!
|
||
result.add(Move(startSquare: square, targetSquare: square, flags: flag.uint16))
|
||
]#
|
||
|
||
|
||
proc generateKnightMoves(self: ChessBoard, square: Square): seq[Move] =
|
||
## Generates moves for the knight in the given square
|
||
#[
|
||
var
|
||
piece = self.grid[square.rank, square.file]
|
||
assert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}"
|
||
var directions: seq[Square] = @[piece.color.bottomLeftKnightMove(),
|
||
piece.color.bottomRightKnightMove(),
|
||
piece.color.topLeftKnightMove(),
|
||
piece.color.topRightKnightMove(),
|
||
piece.color.bottomLeftKnightMove(long=false),
|
||
piece.color.bottomRightKnightMove(long=false),
|
||
piece.color.topLeftKnightMove(long=false),
|
||
piece.color.topRightKnightMove(long=false)]
|
||
let pinned = self.getPinnedDirections(square)
|
||
if pinned.len() > 0:
|
||
# Knight is pinned: can't move!
|
||
return @[]
|
||
let checked = self.inCheck()
|
||
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
|
||
for direction in directions:
|
||
# Jump to this square
|
||
let square: Square = square + direction
|
||
# End of board reached
|
||
if not square.isValid():
|
||
continue
|
||
let otherPiece = self.grid[square]
|
||
# A friendly piece or the opponent king is is in the way
|
||
if otherPiece.color == piece.color or otherPiece.kind == King:
|
||
continue
|
||
if checked and square notin resolutions:
|
||
continue
|
||
if otherPiece.color != None:
|
||
# Target square contains an enemy piece: capture
|
||
# it
|
||
result.add(Move(startSquare: square, targetSquare: square, flags: Capture.uint16))
|
||
else:
|
||
# Target square is empty
|
||
result.add(Move(startSquare: square, targetSquare: square))
|
||
]#
|
||
|
||
|
||
proc checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool =
|
||
## Helper function for checkInsufficientMaterial
|
||
let
|
||
friendlyPawns = self.countPieces(Piece(kind: Pawn, color: color))
|
||
friendlyRooks = self.countPieces(Piece(kind: Rook, color: color))
|
||
friendlyQueens = self.countPieces(Piece(kind: Queen, color: color))
|
||
friendlyKnights = self.countPieces(Piece(kind: Knight, color: color))
|
||
friendlyBishops = self.countPieces(Piece(kind: Bishop, color: color))
|
||
enemyPawns = self.countPieces(Piece(kind: Pawn, color: color.opposite()))
|
||
enemyRooks = self.countPieces(Piece(kind: Rook, color: color.opposite()))
|
||
enemyQueens = self.countPieces(Piece(kind: Queen, color: color.opposite()))
|
||
enemyKnights = self.countPieces(Piece(kind: Knight, color: color.opposite()))
|
||
enemyBishops = self.countPieces(Piece(kind: Bishop, color: color.opposite()))
|
||
if friendlyPawns > 0 or friendlyRooks > 0 or friendlyQueens > 0:
|
||
return false
|
||
if friendlyKnights >= 2:
|
||
return false
|
||
if friendlyKnights + friendlyBishops >= 2:
|
||
return false
|
||
if friendlyKnights >= 1 and (enemyPawns > 0 or enemyRooks > 0 or enemyBishops > 0 or enemyKnights > 0 or enemyQueens > 0):
|
||
return false
|
||
if friendlyBishops >= 1 and (enemyKnights > 0 or enemyPawns > 0):
|
||
return false
|
||
return true
|
||
|
||
|
||
proc checkInsufficientMaterial(self: ChessBoard): bool =
|
||
## Checks if the given position has not enough material for either side to
|
||
## checkmate the enemy king. Note that the criteria as implemented here are
|
||
## not fully compliant with FIDE rules (they just define a draw by insufficient
|
||
## material as "[...] the position is such that the opponent cannot checkmate
|
||
## the player’s king by any possible series of legal moves.", which is really
|
||
## tricky to implement efficiently). For more info see https://www.reddit.com/r/chess/comments/se89db/a_writeup_on_definitions_of_insufficient_material/
|
||
if not (self.checkInsufficientMaterialPieceCount(White) and self.checkInsufficientMaterialPieceCount(Black)):
|
||
return false
|
||
let
|
||
whiteBishops = self.countPieces(Piece(kind: Bishop, color: White))
|
||
blackBishops = self.countPieces(Piece(kind: Bishop, color: Black))
|
||
if blackBishops + whiteBishops >= 2:
|
||
var
|
||
darkSquare = 0
|
||
lightSquare = 0
|
||
for bishop in self.position.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, square: Square): seq[Move] =
|
||
## Returns the list of possible legal chess moves for the
|
||
## piece in the given square
|
||
if self.position.halfMoveClock >= 100:
|
||
# Draw by 50-move rule
|
||
return @[]
|
||
# TODO: Check for draw by insufficient material
|
||
#[
|
||
if self.checkInsufficientMaterial():
|
||
return @[]
|
||
]#
|
||
let piece = self.grid[square.rank, square.file]
|
||
case piece.kind:
|
||
of Queen, Bishop, Rook:
|
||
return self.generateSlidingMoves(square)
|
||
of Pawn:
|
||
return self.generatePawnMoves(square)
|
||
of King:
|
||
return self.generateKingMoves(square)
|
||
of Knight:
|
||
return self.generateKnightMoves(square)
|
||
else:
|
||
return @[]
|
||
|
||
|
||
proc generateAllMoves*(self: ChessBoard): seq[Move] =
|
||
## Returns the list of all possible legal moves
|
||
## in the current position
|
||
for i in 0..7:
|
||
for j in 0..7:
|
||
if self.grid[i, j].color == self.getActiveColor():
|
||
for move in self.generateMoves((int8(i), int8(j))):
|
||
result.add(move)
|
||
|
||
#[
|
||
proc isAttacked*(self: ChessBoard, square: Square, color: PieceColor = None): bool =
|
||
## Returns whether the given square is attacked
|
||
## by the given color
|
||
var color = color
|
||
if color == None:
|
||
color = self.getActiveColor().opposite()
|
||
case color:
|
||
of Black:
|
||
for attack in self.position.attacked.black:
|
||
if attack.target == square:
|
||
return true
|
||
of White:
|
||
for attack in self.position.attacked.white:
|
||
if attack.target == square:
|
||
return true
|
||
of None:
|
||
discard
|
||
|
||
|
||
proc getAttackers*(self: ChessBoard, square: Square, color: PieceColor): seq[Square] =
|
||
## Returns all the attackers of the given color
|
||
## for the given square
|
||
case color:
|
||
of Black:
|
||
for attack in self.position.attacked.black:
|
||
if attack.target == square:
|
||
result.add(attack.source)
|
||
of White:
|
||
for attack in self.position.attacked.white:
|
||
if attack.target == square:
|
||
result.add(attack.source)
|
||
of None:
|
||
discard
|
||
|
||
|
||
proc getAttacks*(self: ChessBoard, square: Square): Attacked =
|
||
## Returns all the squares attacked by the piece in the given
|
||
## square
|
||
let piece = self.grid[square.rank, square.file]
|
||
case piece.color:
|
||
of Black:
|
||
for attack in self.position.attacked.black:
|
||
if attack.source == square:
|
||
result.add(attack)
|
||
of White:
|
||
for attack in self.position.attacked.white:
|
||
if attack.source == square:
|
||
result.add(attack)
|
||
of None:
|
||
discard
|
||
|
||
|
||
proc getAttackFor*(self: ChessBoard, source, target: Square): tuple[source, target, direction: Square] =
|
||
## Returns the first attack from the given source to the
|
||
## given target square
|
||
result = (nullSquare(), nullSquare(), nullSquare())
|
||
let piece = self.grid[source]
|
||
case piece.color:
|
||
of Black:
|
||
for attack in self.position.attacked.black:
|
||
if attack.target == target and attack.source == source:
|
||
return attack
|
||
of White:
|
||
for attack in self.position.attacked.white:
|
||
if attack.target == target and attack.source == source:
|
||
return attack
|
||
of None:
|
||
discard
|
||
|
||
|
||
proc isAttacked*(self: ChessBoard, square: string): bool =
|
||
## Returns whether the given square is attacked
|
||
## by the current
|
||
return self.isAttacked(square.algebraicToSquare())
|
||
|
||
|
||
func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Square], color: PieceColor) {.inline.} =
|
||
if attack.source.isValid() and attack.target.isValid():
|
||
case color:
|
||
of White:
|
||
self.position.attacked.white.add(attack)
|
||
of Black:
|
||
self.position.attacked.black.add(attack)
|
||
else:
|
||
discard
|
||
|
||
|
||
proc getPinnedDirections(self: ChessBoard, square: Square): seq[Square] =
|
||
## Returns all the directions along which the piece in the given
|
||
## square is pinned. If the result is non-empty, the piece at
|
||
## the given square is only allowed to move along the directions
|
||
## returned by this function
|
||
let piece = self.grid[square.rank, square.file]
|
||
case piece.color:
|
||
of None:
|
||
discard
|
||
of White:
|
||
for pin in self.position.pinned.black:
|
||
if pin.target == square:
|
||
result.add(pin.direction)
|
||
of Black:
|
||
for pin in self.position.pinned.white:
|
||
if pin.target == square:
|
||
result.add(pin.direction)
|
||
|
||
|
||
proc updatePawnAttacks(self: ChessBoard) =
|
||
## Internal helper of updateAttackedSquares
|
||
for sq in self.position.pieces.white.pawns:
|
||
# Pawns are special in how they capture (i.e. the
|
||
# squares they can regularly move to do not match
|
||
# the squares they can capture on. Sneaky fucks)
|
||
self.addAttack((sq, sq + White.topRightDiagonal(), White.topRightDiagonal()), White)
|
||
self.addAttack((sq, sq + White.topLeftDiagonal(), White.topLeftDiagonal()), White)
|
||
# We do the same thing for black
|
||
for sq in self.position.pieces.black.pawns:
|
||
self.addAttack((sq, sq + Black.topRightDiagonal(), Black.topRightDiagonal()), Black)
|
||
self.addAttack((sq, sq + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black)
|
||
|
||
|
||
proc updateKingAttacks(self: ChessBoard) =
|
||
## Internal helper of updateAttackedSquares
|
||
var king = self.position.pieces.white.king
|
||
self.addAttack((king, king + White.topRightDiagonal(), White.topRightDiagonal()), White)
|
||
self.addAttack((king, king + White.topLeftDiagonal(), White.topLeftDiagonal()), White)
|
||
self.addAttack((king, king + White.bottomLeftDiagonal(), White.bottomLeftDiagonal()), White)
|
||
self.addAttack((king, king + White.bottomRightDiagonal(), White.bottomRightDiagonal()), White)
|
||
self.addAttack((king, king + White.leftSide(), White.leftSide()), White)
|
||
self.addAttack((king, king + White.rightSide(), White.rightSide()), White)
|
||
self.addAttack((king, king + White.bottomSide(), White.bottomSide()), White)
|
||
self.addAttack((king, king + White.topSide(), White.topSide()), White)
|
||
|
||
king = self.position.pieces.black.king
|
||
self.addAttack((king, king + Black.topRightDiagonal(), Black.topRightDiagonal()), Black)
|
||
self.addAttack((king, king + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black)
|
||
self.addAttack((king, king + Black.bottomLeftDiagonal(), Black.bottomLeftDiagonal()), Black)
|
||
self.addAttack((king, king + Black.bottomRightDiagonal(), Black.bottomRightDiagonal()), Black)
|
||
self.addAttack((king, king + Black.leftSide(), Black.leftSide()), Black)
|
||
self.addAttack((king, king + Black.rightSide(), Black.rightSide()), Black)
|
||
self.addAttack((king, king + Black.bottomSide(), Black.bottomSide()), Black)
|
||
self.addAttack((king, king + Black.topSide(), Black.topSide()), Black)
|
||
|
||
|
||
proc updateKnightAttacks(self: ChessBoard) =
|
||
## Internal helper of updateAttackedSquares
|
||
for loc in self.position.pieces.white.knights:
|
||
self.addAttack((loc, loc + White.topLeftKnightMove(), White.topLeftKnightMove()), White)
|
||
self.addAttack((loc, loc + White.topRightKnightMove(), White.topRightKnightMove()), White)
|
||
self.addAttack((loc, loc + White.bottomLeftKnightMove(), White.bottomLeftKnightMove()), White)
|
||
self.addAttack((loc, loc + White.bottomRightKnightMove(), White.bottomRightKnightMove()), White)
|
||
self.addAttack((loc, loc + White.topLeftKnightMove(long=false), White.topLeftKnightMove(long=false)), White)
|
||
self.addAttack((loc, loc + White.topRightKnightMove(long=false), White.topRightKnightMove(long=false)), White)
|
||
self.addAttack((loc, loc + White.bottomLeftKnightMove(long=false), White.bottomLeftKnightMove(long=false)), White)
|
||
self.addAttack((loc, loc + White.bottomRightKnightMove(long=false), White.bottomRightKnightMove(long=false)), White)
|
||
|
||
for loc in self.position.pieces.black.knights:
|
||
self.addAttack((loc, loc + Black.topLeftKnightMove(), Black.topLeftKnightMove()), Black)
|
||
self.addAttack((loc, loc + Black.topRightKnightMove(), Black.topRightKnightMove()), Black)
|
||
self.addAttack((loc, loc + Black.bottomLeftKnightMove(), Black.bottomLeftKnightMove()), Black)
|
||
self.addAttack((loc, loc + Black.bottomRightKnightMove(), Black.bottomRightKnightMove()), Black)
|
||
self.addAttack((loc, loc + Black.topLeftKnightMove(long=false), Black.topLeftKnightMove(long=false)), Black)
|
||
self.addAttack((loc, loc + Black.topRightKnightMove(long=false), Black.topRightKnightMove(long=false)), Black)
|
||
self.addAttack((loc, loc + Black.bottomLeftKnightMove(long=false), Black.bottomLeftKnightMove(long=false)), Black)
|
||
self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false), Black.bottomRightKnightMove(long=false)), Black)
|
||
|
||
|
||
proc getSlidingAttacks(self: ChessBoard, square: Square): tuple[attacks: Attacked, pins: Attacked] =
|
||
## Internal helper of updateSlidingAttacks
|
||
var
|
||
directions: seq[Square] = @[]
|
||
let piece = self.grid[square.rank, square.file]
|
||
if piece.kind in [Bishop, Queen]:
|
||
directions.add(piece.color.topLeftDiagonal())
|
||
directions.add(piece.color.topRightDiagonal())
|
||
directions.add(piece.color.bottomLeftDiagonal())
|
||
directions.add(piece.color.bottomRightDiagonal())
|
||
|
||
if piece.kind in [Queen, Rook]:
|
||
directions.add(piece.color.topSide())
|
||
directions.add(piece.color.bottomSide())
|
||
directions.add(piece.color.rightSide())
|
||
directions.add(piece.color.leftSide())
|
||
|
||
for direction in directions:
|
||
var
|
||
currentSquare = square
|
||
otherPiece: Piece
|
||
# Slide in this direction as long as it's possible
|
||
while true:
|
||
currentSquare = currentSquare + direction
|
||
# End of board reached
|
||
if not square.isValid():
|
||
break
|
||
otherPiece = self.grid[square]
|
||
# Target square is attacked (even if a friendly piece
|
||
# is present, because in this case we're defending
|
||
# it)
|
||
result.attacks.add((square, currentSquare, direction))
|
||
# Empty square, keep going
|
||
if otherPiece.color == None:
|
||
continue
|
||
if otherPiece.color == piece.color.opposite():
|
||
if otherPiece.kind != King:
|
||
# We found an enemy piece that is not
|
||
# the enemy king. We don't break out
|
||
# immediately because we first want
|
||
# to check if we've pinned it to the king
|
||
var
|
||
otherSquare: Square = square
|
||
behindPiece: Piece
|
||
while true:
|
||
otherSquare = otherSquare + direction
|
||
if not otherSquare.isValid():
|
||
break
|
||
behindPiece = self.grid[otherSquare]
|
||
if behindPiece.color == None:
|
||
continue
|
||
if behindPiece.color == piece.color.opposite and behindPiece.kind == King:
|
||
# The enemy king is behind this enemy piece: pin it along
|
||
# this axis in both directions
|
||
result.pins.add((square, currentSquare, direction))
|
||
result.pins.add((square, currentSquare, -direction))
|
||
if otherPiece.kind == Pawn and square.rank == otherPiece.getStartRank():
|
||
# The pinned piece is a pawn which hasn't moved yet:
|
||
# we allow it to move two squares as well
|
||
if square.file == square.file:
|
||
# The pawn can only push two squares if it's being pinned from the
|
||
# top side (relative to the pawn itself)
|
||
result.pins.add((square, currentSquare, otherPiece.color.doublePush()))
|
||
else:
|
||
break
|
||
else:
|
||
# Enemy king is here: ensure it cannot move backwards by
|
||
# attacking the square behind it (if one exists and is
|
||
# valid)
|
||
let target = square + direction
|
||
if target.isValid():
|
||
result.attacks.add((square, target, direction))
|
||
break
|
||
|
||
|
||
proc updateSlidingAttacks(self: ChessBoard) =
|
||
## Internal helper of updateAttackedSquares
|
||
var data: tuple[attacks: Attacked, pins: Attacked]
|
||
for loc in self.position.pieces.white.bishops:
|
||
data = self.getSlidingAttacks(loc)
|
||
self.position.attacked.white.extend(data.attacks)
|
||
self.position.pinned.white.extend(data.pins)
|
||
for loc in self.position.pieces.white.rooks:
|
||
data = self.getSlidingAttacks(loc)
|
||
self.position.attacked.white.extend(data.attacks)
|
||
self.position.pinned.white.extend(data.pins)
|
||
for loc in self.position.pieces.white.queens:
|
||
data = self.getSlidingAttacks(loc)
|
||
self.position.attacked.white.extend(data.attacks)
|
||
self.position.pinned.white.extend(data.pins)
|
||
for loc in self.position.pieces.black.bishops:
|
||
data = self.getSlidingAttacks(loc)
|
||
self.position.attacked.black.extend(data.attacks)
|
||
self.position.pinned.black.extend(data.pins)
|
||
for loc in self.position.pieces.black.rooks:
|
||
data = self.getSlidingAttacks(loc)
|
||
self.position.attacked.black.extend(data.attacks)
|
||
self.position.pinned.black.extend(data.pins)
|
||
for loc in self.position.pieces.black.queens:
|
||
data = self.getSlidingAttacks(loc)
|
||
self.position.attacked.black.extend(data.attacks)
|
||
self.position.pinned.black.extend(data.pins)
|
||
|
||
|
||
proc updateAttackedSquares(self: ChessBoard) =
|
||
## Updates internal metadata about which squares
|
||
## are attacked
|
||
self.position.attacked.white.setLen(0)
|
||
self.position.attacked.black.setLen(0)
|
||
self.position.pinned.white.setLen(0)
|
||
self.position.pinned.black.setLen(0)
|
||
# Pawns
|
||
self.updatePawnAttacks()
|
||
# Sliding pieces
|
||
self.updateSlidingAttacks()
|
||
# Knights
|
||
self.updateKnightAttacks()
|
||
# Kings
|
||
self.updateKingAttacks()
|
||
]#
|
||
|
||
proc removePieceFromBitboard(self: ChessBoard, square: Square) =
|
||
## Removes a piece at the given square in the chessboard from
|
||
## its respective bitboard
|
||
let piece = self.grid[square]
|
||
case piece.color:
|
||
of White:
|
||
case piece.kind:
|
||
of Pawn:
|
||
self.position.pieces.white.pawns.uint64.clearBit(square.coordToIndex())
|
||
of Bishop:
|
||
self.position.pieces.white.bishops.uint64.clearBit(square.coordToIndex())
|
||
of Knight:
|
||
self.position.pieces.white.knights.uint64.clearBit(square.coordToIndex())
|
||
of Rook:
|
||
self.position.pieces.white.rooks.uint64.clearBit(square.coordToIndex())
|
||
of Queen:
|
||
self.position.pieces.white.queens.uint64.clearBit(square.coordToIndex())
|
||
of King:
|
||
self.position.pieces.white.king.uint64.clearBit(square.coordToIndex())
|
||
else:
|
||
discard
|
||
of Black:
|
||
case piece.kind:
|
||
of Pawn:
|
||
self.position.pieces.black.pawns.uint64.clearBit(square.coordToIndex())
|
||
of Bishop:
|
||
self.position.pieces.black.bishops.uint64.clearBit(square.coordToIndex())
|
||
of Knight:
|
||
self.position.pieces.black.knights.uint64.clearBit(square.coordToIndex())
|
||
of Rook:
|
||
self.position.pieces.black.rooks.uint64.clearBit(square.coordToIndex())
|
||
of Queen:
|
||
self.position.pieces.black.queens.uint64.clearBit(square.coordToIndex())
|
||
of King:
|
||
self.position.pieces.black.king.uint64.clearBit(square.coordToIndex())
|
||
else:
|
||
discard
|
||
else:
|
||
discard
|
||
|
||
|
||
proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) =
|
||
## Adds the given piece at the given square in the chessboard to
|
||
## its respective bitboard
|
||
case piece.color:
|
||
of White:
|
||
case piece.kind:
|
||
of Pawn:
|
||
self.position.pieces.white.pawns.uint64.setBit(square.coordToIndex())
|
||
of Bishop:
|
||
self.position.pieces.white.bishops.uint64.setBit(square.coordToIndex())
|
||
of Knight:
|
||
self.position.pieces.white.knights.uint64.setBit(square.coordToIndex())
|
||
of Rook:
|
||
self.position.pieces.white.rooks.uint64.setBit(square.coordToIndex())
|
||
of Queen:
|
||
self.position.pieces.white.queens.uint64.setBit(square.coordToIndex())
|
||
of King:
|
||
self.position.pieces.white.king.uint64.setBit(square.coordToIndex())
|
||
else:
|
||
discard
|
||
of Black:
|
||
case piece.kind:
|
||
of Pawn:
|
||
self.position.pieces.black.pawns.uint64.setBit(square.coordToIndex())
|
||
of Bishop:
|
||
self.position.pieces.black.bishops.uint64.setBit(square.coordToIndex())
|
||
of Knight:
|
||
self.position.pieces.black.knights.uint64.setBit(square.coordToIndex())
|
||
of Rook:
|
||
self.position.pieces.black.rooks.uint64.setBit(square.coordToIndex())
|
||
of Queen:
|
||
self.position.pieces.black.queens.uint64.setBit(square.coordToIndex())
|
||
of King:
|
||
self.position.pieces.black.king.uint64.setBit(square.coordToIndex())
|
||
else:
|
||
discard
|
||
else:
|
||
discard
|
||
|
||
|
||
proc removePiece(self: ChessBoard, square: Square, attack: bool = true) =
|
||
## Removes a piece from the board, updating necessary
|
||
## metadata
|
||
var piece = self.grid[square]
|
||
self.grid[square] = 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.coordToIndex())
|
||
self.position.pieces.white.pawns.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Bishop:
|
||
self.position.pieces.white.bishops.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.white.bishops.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Knight:
|
||
self.position.pieces.white.knights.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.white.knights.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Rook:
|
||
self.position.pieces.white.rooks.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.white.rooks.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Queen:
|
||
self.position.pieces.white.queens.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.white.queens.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of King:
|
||
self.position.pieces.white.king.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.white.king.uint64.clearBit(move.startSquare.coordToIndex())
|
||
else:
|
||
discard
|
||
of Black:
|
||
case piece.kind:
|
||
of Pawn:
|
||
self.position.pieces.black.pawns.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.black.pawns.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Bishop:
|
||
self.position.pieces.black.bishops.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.black.bishops.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Knight:
|
||
self.position.pieces.black.knights.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.black.knights.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Rook:
|
||
self.position.pieces.black.rooks.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.black.rooks.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of Queen:
|
||
self.position.pieces.black.queens.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.black.queens.uint64.clearBit(move.startSquare.coordToIndex())
|
||
of King:
|
||
self.position.pieces.black.king.uint64.setBit(move.targetSquare.coordToIndex())
|
||
self.position.pieces.black.king.uint64.clearBit(move.startSquare.coordToIndex())
|
||
else:
|
||
discard
|
||
else:
|
||
discard
|
||
|
||
|
||
proc movePiece(self: ChessBoard, move: Move, attack: bool = true) =
|
||
## Internal helper to move a piece. If attack
|
||
## is set to false, then this function does
|
||
## not update attacked squares metadata, just
|
||
## positional info and the grid itself
|
||
let piece = self.grid[move.startSquare]
|
||
let targetSquare = self.getPiece(move.targetSquare)
|
||
if targetSquare.color != None:
|
||
raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}")
|
||
# Update positional metadata
|
||
self.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 + piece.color.bottomSide()
|
||
|
||
# Castling check: have the rooks moved?
|
||
if piece.kind == Rook:
|
||
case piece.color:
|
||
of White:
|
||
if move.startSquare.rank == piece.getStartRank():
|
||
if move.startSquare.file == 0:
|
||
# Queen side
|
||
castlingAvailable.white.queen = false
|
||
elif move.startSquare.file == 7:
|
||
# King side
|
||
castlingAvailable.white.king = false
|
||
of Black:
|
||
if move.startSquare.rank == piece.getStartRank():
|
||
if move.startSquare.file == 0:
|
||
# Queen side
|
||
castlingAvailable.black.queen = false
|
||
elif move.startSquare.file == 7:
|
||
# King side
|
||
castlingAvailable.black.king = false
|
||
else:
|
||
discard
|
||
# Has a rook been captured?
|
||
if move.isCapture():
|
||
let captured = self.grid[move.targetSquare]
|
||
if captured.kind == Rook:
|
||
case captured.color:
|
||
of White:
|
||
if move.targetSquare == captured.color.queenSideRook():
|
||
# Queen side
|
||
castlingAvailable.white.queen = false
|
||
elif move.targetSquare == captured.color.kingSideRook():
|
||
# King side
|
||
castlingAvailable.white.king = false
|
||
of Black:
|
||
if move.targetSquare == captured.color.queenSideRook():
|
||
# Queen side
|
||
castlingAvailable.black.queen = false
|
||
elif move.targetSquare == captured.color.kingSideRook():
|
||
# King side
|
||
castlingAvailable.black.king = false
|
||
else:
|
||
# Unreachable
|
||
discard
|
||
# Has the king moved?
|
||
if piece.kind == King or move.isCastling():
|
||
# Revoke all castling rights for the moving king
|
||
case piece.color:
|
||
of White:
|
||
castlingAvailable.white.king = false
|
||
castlingAvailable.white.queen = false
|
||
of Black:
|
||
castlingAvailable.black.king = false
|
||
castlingAvailable.black.queen = false
|
||
else:
|
||
discard
|
||
|
||
# Create new position
|
||
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
||
halfMoveClock: halfMoveClock,
|
||
fullMoveCount: fullMoveCount,
|
||
turn: self.getActiveColor().opposite,
|
||
castlingAvailable: castlingAvailable,
|
||
enPassantSquare: enPassantTarget,
|
||
pieces: self.position.pieces
|
||
)
|
||
# Update position metadata
|
||
|
||
if move.isCastling():
|
||
# Move the rook onto the
|
||
# correct file when castling
|
||
var
|
||
square: Square
|
||
target: Square
|
||
flags: uint16
|
||
if move.getCastlingType() == CastleShort:
|
||
square = piece.color.kingSideRook()
|
||
target = shortCastleRook()
|
||
flags = flags or CastleShort.uint16
|
||
else:
|
||
square = piece.color.queenSideRook()
|
||
target = longCastleRook()
|
||
flags = flags or CastleLong.uint16
|
||
let rook = self.grid[square.rank, square.file]
|
||
let move = Move(startSquare: square, targetSquare: square + target, flags: flags)
|
||
self.movePiece(move, attack=false)
|
||
|
||
if move.isEnPassant():
|
||
# Make the en passant pawn disappear
|
||
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
|
||
|
||
if move.isCapture():
|
||
# Get rid of captured pieces
|
||
self.removePiece(move.targetSquare, attack=false)
|
||
# Move the piece to its target square and update attack metadata
|
||
self.movePiece(move, attack=false)
|
||
if move.isPromotion():
|
||
# Move is a pawn promotion: get rid of the pawn
|
||
# and spawn a new piece
|
||
self.removePiece(move.targetSquare)
|
||
case move.getPromotionType():
|
||
of PromoteToBishop:
|
||
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color))
|
||
of PromoteToKnight:
|
||
self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color))
|
||
of PromoteToRook:
|
||
self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color))
|
||
of PromoteToQueen:
|
||
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
|
||
else:
|
||
# Unreachable
|
||
discard
|
||
#self.updateAttackedSquares()
|
||
|
||
|
||
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) =
|
||
## Internal helper to "spawn" a given piece at the given
|
||
## square. Note that this will overwrite whatever piece
|
||
## was previously located there: use with caution. Does
|
||
## not automatically update the attacked square metadata
|
||
self.addPieceToBitboard(square, piece)
|
||
self.grid[square] = piece
|
||
|
||
|
||
proc updateBoard*(self: ChessBoard) =
|
||
## Updates the internal grid representation
|
||
## according to the positional data stored
|
||
## in the chessboard
|
||
for i in 0..63:
|
||
self.grid[i] = 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.bitboardToSquare()] = Piece(color: White, kind: King)
|
||
self.grid[self.position.pieces.black.king.bitboardToSquare()] = 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
|
||
return move in self.generateMoves(move.startSquare)
|
||
|
||
|
||
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
|
||
## Makes a move on the board
|
||
result = move
|
||
if not self.isLegal(move):
|
||
return 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 &= "- - - - - - - -"
|
||
for i in 0..7:
|
||
result &= "\n"
|
||
for j in 0..7:
|
||
let piece = self.grid[i, j]
|
||
if piece.kind == Empty:
|
||
result &= "x "
|
||
continue
|
||
result &= &"{piece.toChar()} "
|
||
result &= &"{fileToColumn(i + 1) + 1}"
|
||
result &= "\n- - - - - - - -"
|
||
result &= "\na b c d e f g h"
|
||
|
||
|
||
proc toPretty*(piece: Piece): string =
|
||
case piece.color:
|
||
of White:
|
||
case piece.kind:
|
||
of King:
|
||
return "\U2654"
|
||
of Queen:
|
||
return "\U2655"
|
||
of Rook:
|
||
return "\U2656"
|
||
of Bishop:
|
||
return "\U2657"
|
||
of Knight:
|
||
return "\U2658"
|
||
of Pawn:
|
||
return "\U2659"
|
||
else:
|
||
discard
|
||
of Black:
|
||
case piece.kind:
|
||
of King:
|
||
return "\U265A"
|
||
of Queen:
|
||
return "\U265B"
|
||
of Rook:
|
||
return "\U265C"
|
||
of Bishop:
|
||
return "\U265D"
|
||
of Knight:
|
||
return "\U265E"
|
||
of Pawn:
|
||
return "\240\159\168\133"
|
||
else:
|
||
discard
|
||
else:
|
||
discard
|
||
|
||
|
||
proc pretty*(self: ChessBoard): string =
|
||
## Returns a colored version of the
|
||
## board for easier visualization
|
||
for i in 0..7:
|
||
if i > 0:
|
||
result &= "\n"
|
||
for j in 0..7:
|
||
# Equivalent to (i + j) mod 2
|
||
# (I'm just evil)
|
||
if ((i + j) and 1) == 0:
|
||
result &= "\x1b[39;44;1m"
|
||
else:
|
||
result &= "\x1b[39;40;1m"
|
||
let piece = self.grid[i, j]
|
||
if piece.kind == Empty:
|
||
result &= " \x1b[0m"
|
||
else:
|
||
result &= &"{piece.toPretty()} \x1b[0m"
|
||
result &= &" \x1b[33;1m{fileToColumn(i + 1) + 1}\x1b[0m"
|
||
|
||
result &= "\n\x1b[31;1ma b c d e f g h"
|
||
result &= "\x1b[0m"
|
||
|
||
|
||
|
||
proc toFEN*(self: ChessBoard): string =
|
||
## Returns a FEN string of the current
|
||
## position in the chessboard
|
||
var skip: int
|
||
# Piece placement data
|
||
for i in 0..7:
|
||
skip = 0
|
||
for j in 0..7:
|
||
let piece = self.grid[i, j]
|
||
if piece.kind == Empty:
|
||
inc(skip)
|
||
elif skip > 0:
|
||
result &= &"{skip}{piece.toChar()}"
|
||
skip = 0
|
||
else:
|
||
result &= piece.toChar()
|
||
if skip > 0:
|
||
result &= $skip
|
||
if i < 7:
|
||
result &= "/"
|
||
result &= " "
|
||
# Active color
|
||
result &= (if self.getActiveColor() == White: "w" else: "b")
|
||
result &= " "
|
||
# Castling availability
|
||
let castleWhite = self.position.castlingAvailable.white
|
||
let castleBlack = self.position.castlingAvailable.black
|
||
if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
|
||
result &= "-"
|
||
else:
|
||
if castleWhite.king:
|
||
result &= "K"
|
||
if castleWhite.queen:
|
||
result &= "Q"
|
||
if castleBlack.king:
|
||
result &= "k"
|
||
if castleBlack.queen:
|
||
result &= "q"
|
||
result &= " "
|
||
# En passant target
|
||
if self.getEnPassantTarget() == nullSquare():
|
||
result &= "-"
|
||
else:
|
||
result &= self.getEnPassantTarget().squareToAlgebraic()
|
||
result &= " "
|
||
# Halfmove clock
|
||
result &= $self.getHalfMoveCount()
|
||
result &= " "
|
||
# Fullmove number
|
||
result &= $self.getMoveCount()
|
||
|
||
|
||
proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
|
||
## Counts (and debugs) the number of legal positions reached after
|
||
## the given number of ply
|
||
|
||
let moves = self.generateAllMoves()
|
||
if not bulk:
|
||
if len(moves) == 0 and self.inCheck():
|
||
result.checkmates = 1
|
||
# TODO: Should we count stalemates/draws?
|
||
if ply == 0:
|
||
result.nodes = 1
|
||
return
|
||
elif ply == 1 and bulk:
|
||
if divide:
|
||
var postfix = ""
|
||
for move in moves:
|
||
case move.getPromotionType():
|
||
of PromoteToBishop:
|
||
postfix = "b"
|
||
of PromoteToKnight:
|
||
postfix = "n"
|
||
of PromoteToRook:
|
||
postfix = "r"
|
||
of PromoteToQueen:
|
||
postfix = "q"
|
||
else:
|
||
postfix = ""
|
||
echo &"{move.startSquare.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}{postfix}: 1"
|
||
if verbose:
|
||
echo ""
|
||
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
|
||
|
||
for move in moves:
|
||
if verbose:
|
||
let canCastle = self.canCastle(self.getActiveColor())
|
||
echo &"Ply (from root): {self.position.plyFromRoot}"
|
||
echo &"Move: {move.startSquare.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}, from ({move.startSquare.rank}, {move.startSquare.file}) to ({move.targetSquare.rank}, {move.targetSquare.file})"
|
||
echo &"Turn: {self.getActiveColor()}"
|
||
echo &"Piece: {self.grid[move.startSquare].kind}"
|
||
echo &"Flags: {move.getFlags()}"
|
||
echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}"
|
||
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||
echo &"Position before move: {self.toFEN()}"
|
||
stdout.write("En Passant target: ")
|
||
if self.getEnPassantTarget() != nullSquare():
|
||
echo self.getEnPassantTarget().squareToAlgebraic()
|
||
else:
|
||
echo "None"
|
||
echo "\n", self.pretty()
|
||
self.doMove(move)
|
||
if ply == 1:
|
||
if move.isCapture():
|
||
inc(result.captures)
|
||
if move.isCastling():
|
||
inc(result.castles)
|
||
if move.isPromotion():
|
||
inc(result.promotions)
|
||
if move.isEnPassant():
|
||
inc(result.enPassant)
|
||
if self.inCheck():
|
||
# Opponent king is in check
|
||
inc(result.checks)
|
||
if verbose:
|
||
let canCastle = self.canCastle(self.getActiveColor())
|
||
echo "\n"
|
||
echo &"Opponent in check: {(if self.inCheck(): \"yes\" else: \"no\")}"
|
||
echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||
echo &"Position after move: {self.toFEN()}"
|
||
echo "\n", self.pretty()
|
||
stdout.write("nextpos>> ")
|
||
try:
|
||
discard readLine(stdin)
|
||
except IOError:
|
||
discard
|
||
except EOFError:
|
||
discard
|
||
let next = self.perft(ply - 1, verbose, bulk=bulk)
|
||
self.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.squareToAlgebraic()}{move.targetSquare.squareToAlgebraic()}{postfix}: {next.nodes}"
|
||
if verbose:
|
||
echo ""
|
||
result.nodes += next.nodes
|
||
result.captures += next.captures
|
||
result.checks += next.checks
|
||
result.promotions += next.promotions
|
||
result.castles += next.castles
|
||
result.enPassant += next.enPassant
|
||
result.checkmates += next.checkmates
|
||
|
||
|
||
proc handleGoCommand(board: ChessBoard, command: seq[string]) =
|
||
if len(command) < 2:
|
||
echo &"Error: go: invalid number of arguments"
|
||
return
|
||
case command[1]:
|
||
of "perft":
|
||
if len(command) == 2:
|
||
echo &"Error: go: perft: invalid number of arguments"
|
||
return
|
||
var
|
||
args = command[2].splitWhitespace()
|
||
bulk = false
|
||
verbose = false
|
||
if args.len() > 1:
|
||
var ok = true
|
||
for arg in args[1..^1]:
|
||
case arg:
|
||
of "bulk":
|
||
bulk = true
|
||
of "verbose":
|
||
verbose = true
|
||
else:
|
||
echo &"Error: go: perft: invalid argument '{args[1]}'"
|
||
ok = false
|
||
break
|
||
if not ok:
|
||
return
|
||
try:
|
||
let ply = parseInt(args[0])
|
||
if bulk:
|
||
let t = cpuTime()
|
||
let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes
|
||
echo &"\nNodes searched (bulk-counting: on): {nodes}"
|
||
echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n"
|
||
else:
|
||
let t = cpuTime()
|
||
let data = board.perft(ply, divide=true, verbose=verbose)
|
||
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
|
||
echo &" - Captures: {data.captures}"
|
||
echo &" - Checks: {data.checks}"
|
||
echo &" - E.P: {data.enPassant}"
|
||
echo &" - Checkmates: {data.checkmates}"
|
||
echo &" - Castles: {data.castles}"
|
||
echo &" - Promotions: {data.promotions}"
|
||
echo ""
|
||
echo &"Time taken: {round(cpuTime() - t, 3)} seconds"
|
||
except ValueError:
|
||
echo "Error: go: perft: invalid depth"
|
||
else:
|
||
echo &"Error: go: unknown subcommand '{command[1]}'"
|
||
|
||
|
||
proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} =
|
||
if len(command) != 2:
|
||
echo &"Error: move: invalid number of arguments"
|
||
return
|
||
let moveString = command[1]
|
||
if len(moveString) notin 4..5:
|
||
echo &"Error: move: invalid move syntax"
|
||
return
|
||
var
|
||
startSquare: Square
|
||
targetSquare: Square
|
||
flags: seq[MoveFlag]
|
||
|
||
try:
|
||
startSquare = moveString[0..1].algebraicToSquare()
|
||
except ValueError:
|
||
echo &"Error: move: invalid start square ({moveString[0..1]})"
|
||
return
|
||
try:
|
||
targetSquare = moveString[2..3].algebraicToSquare()
|
||
except ValueError:
|
||
echo &"Error: move: invalid target square ({moveString[2..3]})"
|
||
return
|
||
|
||
# Since the user tells us just the source and target square of the move,
|
||
# we have to figure out all the flags by ourselves (whether it's a double
|
||
# push, a capture, a promotion, castling, etc.)
|
||
|
||
if board.grid[targetSquare].kind != Empty:
|
||
flags.add(Capture)
|
||
|
||
elif board.grid[startSquare].kind == Pawn and abs(startSquare.rank - targetSquare.rank) == 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)
|
||
if board.getPiece(move.startSquare).kind == King and move.startSquare == board.getActiveColor().getKingStartingSquare():
|
||
if move.targetSquare == move.startSquare + longCastleKing():
|
||
move.flags = move.flags or CastleLong.uint16
|
||
elif move.targetSquare == move.startSquare + shortCastleKing():
|
||
move.flags = move.flags or CastleShort.uint16
|
||
if move.targetSquare == board.getEnPassantTarget():
|
||
move.flags = move.flags or EnPassant.uint16
|
||
result = board.makeMove(move)
|
||
if result == 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.getActiveColor()}"
|
||
of "ep":
|
||
let target = board.getEnPassantTarget()
|
||
if target != nullSquare():
|
||
echo &"En passant target: {target.squareToAlgebraic()}"
|
||
else:
|
||
echo "En passant target: None"
|
||
of "get":
|
||
if len(cmd) != 2:
|
||
echo "error: get: invalid number of arguments"
|
||
continue
|
||
try:
|
||
echo board.getPiece(cmd[1])
|
||
except ValueError:
|
||
echo "error: get: invalid square"
|
||
continue
|
||
of "castle":
|
||
let canCastle = board.canCastle()
|
||
echo &"Castling rights for {($board.getActiveColor()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
|
||
of "check":
|
||
echo &"{board.getActiveColor()} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
|
||
else:
|
||
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
|
||
except IOError:
|
||
echo ""
|
||
return 0
|
||
except EOFError:
|
||
echo ""
|
||
return 0
|
||
|
||
|
||
when isMainModule:
|
||
|
||
|
||
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
|
||
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
|
||
|
||
proc testPieceCount(board: ChessBoard, kind: PieceKind, color: PieceColor, count: int) =
|
||
let pieces = board.countPieces(kind, color)
|
||
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
|
||
|
||
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 = @[(6'i8, 0'i8), (6, 1), (6, 2), (6, 3), (6, 4), (6, 5), (6, 6), (6, 7)]
|
||
whiteKnightSquares = @[(7'i8, 1'i8), (7, 6)]
|
||
whiteBishopSquares = @[(7'i8, 2'i8), (7, 5)]
|
||
whiteRookSquares = @[(7'i8, 0'i8), (7, 7)]
|
||
whiteQueenSquares = @[(7'i8, 3'i8)]
|
||
whiteKingSquares = @[(7'i8, 4'i8)]
|
||
blackPawnSquares = @[(1'i8, 0'i8), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7)]
|
||
blackKnightSquares = @[(0'i8, 1'i8), (0, 6)]
|
||
blackBishopSquares = @[(0'i8, 2'i8), (0, 5)]
|
||
blackRookSquares = @[(0'i8, 0'i8), (0, 7)]
|
||
blackQueenSquares = @[(0'i8, 3'i8)]
|
||
blackKingSquares = @[(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()) |