CPG/Chess/nimfish/nimfish.nim

1404 lines
54 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 nimfishpkg/bitboards
import nimfishpkg/magics
import nimfishpkg/pieces
import nimfishpkg/moves
export bitboards, magics, pieces, moves
type
CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
Position* = object
## A chess position
# Castling metadata. Updated on every move
castlingRights: array[64, uint8]
# 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 (half-moves)
fullMoveCount: int8
# En passant target square (see https://en.wikipedia.org/wiki/En_passant)
enPassantSquare*: Square
# The side to move
sideToMove: PieceColor
# Positional bitboards for all pieces
pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]]
# Pinned pieces for each side
pins: tuple[white, black: Bitboard]
# Checking pieces
checkers: tuple[white, black: Bitboard]
ChessBoard* = ref object
## A chess board object
# The actual board where pieces live
grid: array[64, Piece]
# The current position
position: Position
# List of all previously reached positions
positions: seq[Position]
# A bunch of simple utility functions and forward declarations
proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.}
proc isLegal(self: ChessBoard, move: Move): bool {.inline.}
proc doMove*(self: ChessBoard, move: Move)
proc pretty*(self: ChessBoard): string
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece)
proc toFEN*(self: ChessBoard): string
proc unmakeMove*(self: ChessBoard)
proc movePiece(self: ChessBoard, move: Move)
proc removePiece(self: ChessBoard, square: Square)
proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} =
for x in other:
self.add(x)
proc update*(self: ChessBoard)
func setSideToMove*(self: ChessBoard, side: PieceColor) {.inline.} =
self.position.sideToMove = side
# A bunch of getters
func getSideToMove*(self: ChessBoard): PieceColor {.inline.} =
## Returns the currently side to move
return self.position.sideToMove
func getEnPassantTarget*(self: ChessBoard): Square {.inline.} =
## Returns the current en passant target square
return self.position.enPassantSquare
func getPlyFromRoot*(self: ChessBoard): int8 {.inline.} =
## Returns the current distance from the root in plys
return self.position.plyFromRoot
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 "e1".toSquare()
of Black:
return "e8".toSquare()
else:
discard
# FIXME: Check this shit.
func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare())
func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare())
func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare())
func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare())
func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare())
func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare())
proc 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(), sideToMove: White)
# Indexing operations
func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8]
func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece
func 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.sideToMove = White
of 'b':
result.position.sideToMove = Black
else:
raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'")
of 2:
# Castling availability
case c:
# TODO
of '-':
discard
of 'K':
discard
# result.position.castlingRightsAvailable.white.king = true
of 'Q':
discard
# result.position.castlingRightsAvailable.white.queen = true
of 'k':
discard
# result.position.castlingRightsAvailable.black.king = true
of 'q':
discard
# result.position.castlingRightsAvailable.black.queen = true
else:
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section")
of 3:
# En passant target square
case c:
of '-':
# Field is already uninitialized to the correct state
discard
else:
result.position.enPassantSquare = fen[index..index+1].toSquare()
# Square metadata is 2 bytes long
inc(index)
of 4:
# Halfmove clock
var s = ""
while not fen[index].isSpaceAscii():
s.add(fen[index])
inc(index)
# Backtrack so the space is seen by the
# next iteration of the loop
dec(index)
result.position.halfMoveClock = parseInt(s).int8
of 5:
# Fullmove number
var s = ""
while index <= fen.high():
s.add(fen[index])
inc(index)
result.position.fullMoveCount = parseInt(s).int8
else:
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
inc(index)
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 castlingRights 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): Square {.inline.} =
## Returns the square of the king for the given
## side
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 updateCheckers(self: ChessBoard) =
let side = self.getSideToMove()
let checkers = self.getAttacksTo(self.getKingSquare(side), side.opposite())
case side:
of White:
self.position.checkers.white = checkers
of Black:
self.position.checkers.black = checkers
else:
discard
proc inCheck(self: ChessBoard, side: PieceColor): bool =
## Returns if the current side to move is in check
case self.getSideToMove():
of White:
return self.position.checkers.white != 0
of Black:
return self.position.checkers.black != 0
else:
discard
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 = if sideToMove == White: getRankMask(6) else: getRankMask(1) # Only pawns on their starting rank can double push
for square in (pawns and rank).doubleForwardRelativeTo(sideToMove) and allowedSquares:
moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush))
proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) =
## Helper of generatePawnMoves for generating all capture
## pawn moves
let
sideToMove = self.getSideToMove()
nonSideToMove = sideToMove.opposite()
pawns = self.getBitboard(Pawn, sideToMove)
# We can only capture enemy pieces (except the king)
enemyPieces = self.getCapturablePieces(nonSideToMove)
enemyPawns = self.getBitboard(Pawn, nonSideToMove)
rightMovement = pawns.forwardRightRelativeTo(sideToMove)
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
epTarget = self.getEnPassantTarget()
var epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0)
epBitboard = epBitboard and enemyPawns
# 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()
rooks = self.getBitboard(Rook, sideToMove)
for square in rooks:
let blockers = occupancy and Rook.getRelevantBlockers(square)
# Can't move over other pieces (captures are handled elsewhere)
let moveset = getRookMoves(square, blockers) and not occupancy
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()
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 other pieces (captures are handled elsewhere)
moveset = moveset and not occupancy
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 other pieces (captures are handled elsewhere)
moveset = moveset and not occupancy
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 generateMoves*(self: ChessBoard, moves: var MoveList) =
## Generates the list of all possible legal moves
## in the current position
moves.clear()
if self.position.halfMoveClock >= 100:
# Draw by 50-move rule
return
# TODO: Check for draw by insufficient material
# TODO: Check for repetitions (requires zobrist hashing + table)
self.generateKingMoves(moves)
self.generatePawnMoves(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)
of Empty:
doAssert false, &"cannot remove empty white piece from {square}"
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)
of Empty:
doAssert false, &"cannot remove empty black piece from {square}"
else:
doAssert false, &"cannot remove empty piece from colorless square {square}"
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) =
## Removes a piece from the board, updating necessary
## metadata
var piece = self.grid[square]
when not defined(danger):
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
self.removePieceFromBitboard(square)
self.grid[square] = nullPiece()
proc movePiece(self: ChessBoard, move: Move) =
## Internal helper to move a piece. 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]
when not defined(danger):
let targetSquare = self.getPiece(move.targetSquare)
if targetSquare.color != None:
raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}")
# Update positional metadata
self.removePiece(move.startSquare)
self.spawnPiece(move.targetSquare, piece)
proc movePiece(self: ChessBoard, startSquare, targetSquare: Square) =
## Like the other movePiece(), but with two squares
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare))
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]
when not defined(danger):
doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}"
var
halfMoveClock = self.position.halfMoveClock
fullMoveCount = self.position.fullMoveCount
castlingRights = self.position.castlingRights
enPassantTarget = nullSquare()
# Needed to detect draw by the 50 move rule
if piece.kind == Pawn or move.isCapture() or move.isEnPassant():
halfMoveClock = 0
else:
inc(halfMoveClock)
if piece.color == Black:
inc(fullMoveCount)
if move.isDoublePush():
enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()
# Create new position
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
halfMoveClock: halfMoveClock,
fullMoveCount: fullMoveCount,
sideToMove: self.getSideToMove().opposite,
castlingRights: castlingRights,
enPassantSquare: enPassantTarget,
pieces: self.position.pieces
)
# Update position metadata
if move.isEnPassant():
# Make the en passant pawn disappear
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare())
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare)
# Move the piece to its target square
self.movePiece(move)
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.updateCheckers()
proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) =
## Internal helper to "spawn" a given piece at the given
## square
when not defined(danger):
doAssert self.grid[square].kind == Empty
self.addPieceToBitboard(square, piece)
self.grid[square] = piece
proc update*(self: ChessBoard) =
## Updates the internal grid representation
## according to the positional data stored
## in the chessboard
for i in 0..63:
self.grid[i] = nullPiece()
for sq in self.position.pieces.white.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)
for sq in self.position.pieces.white.king:
self.grid[sq] = Piece(color: White, kind: King)
for sq in self.position.pieces.black.king:
self.grid[sq] = Piece(color: Black, kind: King)
proc unmakeMove*(self: ChessBoard) =
## Reverts to the previous board position,
## if one exists
self.position = self.positions.pop()
self.update()
proc 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
result &= "-"
# let castleWhite = self.position.castlingRightsAvailable.white
# let castleBlack = self.position.castlingRightsAvailable.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()
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))
import tui
quit(tui.commandLoop())