930 lines
35 KiB
Nim
930 lines
35 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 nimfishpkg/bitboards
|
|
import nimfishpkg/magics
|
|
import nimfishpkg/pieces
|
|
import nimfishpkg/moves
|
|
import nimfishpkg/position
|
|
import nimfishpkg/rays
|
|
|
|
|
|
export bitboards, magics, pieces, moves, position
|
|
|
|
|
|
type
|
|
|
|
Chessboard* = ref object
|
|
## A chessboard
|
|
|
|
# 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 update*(self: Chessboard)
|
|
func inCheck*(self: Chessboard): bool {.inline.}
|
|
proc fromChar*(c: char): Piece
|
|
proc updateChecksAndPins*(self: Chessboard)
|
|
|
|
|
|
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 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 getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} =
|
|
## Returns the positional bitboard for the given piece kind and color
|
|
return self.position.getBitboard(kind, color)
|
|
|
|
|
|
func getBitboard*(self: Chessboard, piece: Piece): Bitboard {.inline.} =
|
|
## 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
|
|
# 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 = makeSquare(row, column)
|
|
piece = c.fromChar()
|
|
result.position.pieces[piece.color][piece.kind][].setBit(square)
|
|
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':
|
|
result.position.castlingAvailability.white.king = true
|
|
of 'Q':
|
|
result.position.castlingAvailability.white.queen = true
|
|
of 'k':
|
|
result.position.castlingAvailability.black.king = true
|
|
of 'q':
|
|
result.position.castlingAvailability.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)
|
|
result.updateChecksAndPins()
|
|
|
|
|
|
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")
|
|
|
|
|
|
func countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.inline.} =
|
|
## Returns the number of pieces with
|
|
## the given color and type in the
|
|
## current position
|
|
return self.position.pieces[color][kind][].countSquares()
|
|
|
|
|
|
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]
|
|
|
|
|
|
func getPiece*(self: Chessboard, square: string): Piece {.inline.} =
|
|
## Gets the piece on the given square
|
|
## in algebraic notation
|
|
return self.getPiece(square.toSquare())
|
|
|
|
|
|
func getOccupancyFor(self: Chessboard, color: PieceColor): Bitboard =
|
|
## Get the occupancy bitboard for every piece of the given color
|
|
result = Bitboard(0)
|
|
for b in self.position.pieces[color][]:
|
|
result = result or b
|
|
|
|
|
|
func getOccupancy(self: Chessboard): Bitboard {.inline.} =
|
|
## Get the occupancy bitboard for every piece on
|
|
## the chessboard
|
|
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
|
|
|
|
|
|
func getPawnAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} =
|
|
## Returns the locations of the pawns attacking the given square
|
|
let
|
|
sq = square.toBitboard()
|
|
pawns = self.getBitboard(Pawn, attacker)
|
|
bottomLeft = sq.backwardLeftRelativeTo(attacker)
|
|
bottomRight = sq.backwardRightRelativeTo(attacker)
|
|
return pawns and (bottomLeft or bottomRight)
|
|
|
|
|
|
func getKingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} =
|
|
## Returns the location of the king if it is attacking the given square
|
|
result = Bitboard(0)
|
|
let
|
|
king = self.getBitboard(King, attacker)
|
|
if (getKingAttacks(square) and king) != 0:
|
|
result = result or king
|
|
|
|
|
|
func getKnightAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
|
## Returns the locations of the knights attacking the given square
|
|
let
|
|
knights = self.getBitboard(Knight, attacker)
|
|
result = Bitboard(0)
|
|
for knight in knights:
|
|
let knightBB = knight.toBitboard()
|
|
if (getKnightAttacks(knight) and knightBB) != 0:
|
|
result = result or knightBB
|
|
|
|
|
|
proc getSlidingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
|
## Returns the locations of the sliding pieces attacking the given square
|
|
let
|
|
queens = self.getBitboard(Queen, attacker)
|
|
rooks = self.getBitboard(Rook, attacker) or queens
|
|
bishops = self.getBitboard(Bishop, attacker) or queens
|
|
occupancy = self.getOccupancy()
|
|
squareBB = square.toBitboard()
|
|
result = Bitboard(0)
|
|
for rook in rooks:
|
|
let
|
|
blockers = occupancy and Rook.getRelevantBlockers(rook)
|
|
moves = getRookMoves(rook, blockers)
|
|
# Attack set intersects our chosen square
|
|
if (moves and squareBB) != 0:
|
|
result = result or rook.toBitboard()
|
|
for bishop in bishops:
|
|
let
|
|
blockers = occupancy and Bishop.getRelevantBlockers(bishop)
|
|
moves = getBishopMoves(bishop, blockers)
|
|
if (moves and squareBB) != 0:
|
|
result = result or bishop.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)
|
|
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 isOccupancyAttacked*(self: Chessboard, square: Square, occupancy: Bitboard): bool =
|
|
## Returns whether the given square would be attacked by the
|
|
## enemy side if the board had the given occupancy. This function
|
|
## is necessary mostly to make sure sliding attacks can check the
|
|
## king properly: due to how we generate our attack bitboards, if
|
|
## the king moved backwards along a ray from a slider we would not
|
|
## consider it to be in check (because the ray stops at the first
|
|
## blocker). In order to fix that, in generateKingMoves() we use this
|
|
## function and pass in the board's occupancy without the moving king so
|
|
## that we can pick the correct magic bitboard and ray. Also, since this
|
|
## function doesn't need to generate all the attacks to know whether a
|
|
## given square is unsafe, it can short circuit at the first attack and
|
|
## exit early, unlike getAttacksTo
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
nonSideToMove = sideToMove.opposite()
|
|
knights = self.getBitboard(Knight, nonSideToMove)
|
|
|
|
# Let's do the cheap ones first (the ones which are precomputed)
|
|
if (getKnightAttacks(square) and knights) != 0:
|
|
return true
|
|
|
|
let king = self.getBitboard(King, nonSideToMove)
|
|
|
|
if (getKingAttacks(square) and king) != 0:
|
|
return true
|
|
|
|
let
|
|
queens = self.getBitboard(Queen, nonSideToMove)
|
|
bishops = self.getBitboard(Bishop, nonSideToMove) or queens
|
|
|
|
if (getBishopMoves(square, occupancy) and bishops) != 0:
|
|
return true
|
|
|
|
let rooks = self.getBitboard(Rook, nonSideToMove) or queens
|
|
|
|
if (getRookMoves(square, occupancy) and rooks) != 0:
|
|
return true
|
|
|
|
# TODO: Precompute pawn moves as well?
|
|
let pawns = self.getBitboard(Pawn, nonSideToMove)
|
|
|
|
if (self.getPawnAttacks(square, nonSideToMove) and pawns) != 0:
|
|
return true
|
|
|
|
|
|
proc updateChecksAndPins*(self: Chessboard) =
|
|
## Updates internal metadata about checks and
|
|
## pinned pieces
|
|
|
|
# *Ahem*, stolen from https://github.com/Ciekce/voidstar/blob/424ac4624011271c4d1dbd743602c23f6dbda1de/src/position.rs
|
|
# Can you tell I'm a *great* coder?
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
nonSideToMove = sideToMove.opposite()
|
|
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
|
|
friendlyPieces = self.getOccupancyFor(sideToMove)
|
|
enemyPieces = self.getOccupancyFor(nonSideToMove)
|
|
|
|
# Update checks
|
|
self.position.checkers = self.getAttacksTo(friendlyKing, nonSideToMove)
|
|
# Update pins
|
|
self.position.diagonalPins = Bitboard(0)
|
|
self.position.orthogonalPins = Bitboard(0)
|
|
|
|
let
|
|
diagonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Bishop, nonSideToMove)
|
|
orthogonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Rook, nonSideToMove)
|
|
canPinDiagonally = diagonalAttackers and getBishopMoves(friendlyKing, enemyPieces)
|
|
canPinOrthogonally = orthogonalAttackers and getRookMoves(friendlyKing, enemyPieces)
|
|
|
|
for piece in canPinDiagonally:
|
|
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
|
|
|
|
# Is the pinning ray obstructed by any of our friendly pieces? If so, the
|
|
# piece is pinned
|
|
if (pinningRay and friendlyPieces).countSquares() > 0:
|
|
self.position.diagonalPins = self.position.diagonalPins or pinningRay
|
|
|
|
for piece in canPinOrthogonally:
|
|
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
|
|
if (pinningRay and friendlyPieces).countSquares() > 0:
|
|
self.position.orthogonalPins = self.position.orthogonalPins or pinningRay
|
|
|
|
|
|
func inCheck(self: Chessboard): bool {.inline.} =
|
|
## Returns if the current side to move is in check
|
|
return self.position.checkers != 0
|
|
|
|
|
|
proc canCastle*(self: Chessboard, side: PieceColor): tuple[king, queen: bool] =
|
|
## Returns if the current side to move can castle
|
|
return (false, false) # TODO
|
|
|
|
|
|
proc removePieceFromBitboard(self: Chessboard, square: Square) =
|
|
## Removes a piece at the given square in the chessboard from
|
|
## its respective bitboard
|
|
let piece = self.grid[square]
|
|
self.position.pieces[piece.color][piece.kind][].clearBit(square)
|
|
|
|
|
|
proc addPieceToBitboard(self: Chessboard, square: Square, piece: Piece) =
|
|
## Adds the given piece at the given square in the chessboard to
|
|
## its respective bitboard
|
|
self.position.pieces[piece.color][piece.kind][].setBit(square)
|
|
|
|
|
|
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 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 from
|
|
## its current square to a target square
|
|
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 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 (i.e. during search)
|
|
|
|
# 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():
|
|
# Number of half-moves since the last reversible half-move
|
|
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.position.sideToMove.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)
|
|
# TODO: Castling!
|
|
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
|
|
# Updates checks and pins for the side to move
|
|
self.updateChecksAndPins()
|
|
|
|
|
|
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][Pawn][]:
|
|
self.grid[sq] = Piece(color: White, kind: Pawn)
|
|
for sq in self.position.pieces[Black][Pawn][]:
|
|
self.grid[sq] = Piece(color: Black, kind: Pawn)
|
|
for sq in self.position.pieces[White][Bishop][]:
|
|
self.grid[sq] = Piece(color: White, kind: Bishop)
|
|
for sq in self.position.pieces[Black][Bishop][]:
|
|
self.grid[sq] = Piece(color: Black, kind: Bishop)
|
|
for sq in self.position.pieces[White][Knight][]:
|
|
self.grid[sq] = Piece(color: White, kind: Knight)
|
|
for sq in self.position.pieces[Black][Knight][]:
|
|
self.grid[sq] = Piece(color: Black, kind: Knight)
|
|
for sq in self.position.pieces[White][Rook][]:
|
|
self.grid[sq] = Piece(color: White, kind: Rook)
|
|
for sq in self.position.pieces[Black][Rook][]:
|
|
self.grid[sq] = Piece(color: Black, kind: Rook)
|
|
for sq in self.position.pieces[White][Queen][]:
|
|
self.grid[sq] = Piece(color: White, kind: Queen)
|
|
for sq in self.position.pieces[Black][Queen][]:
|
|
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 generatePawnMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
pawns = self.getBitboard(Pawn, sideToMove)
|
|
occupancy = self.getOccupancy()
|
|
# We can only capture enemy pieces (except the king)
|
|
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
|
|
# We can only capture diagonally and forward
|
|
rightMovement = pawns.forwardRightRelativeTo(sideToMove)
|
|
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
|
|
epTarget = self.position.enPassantSquare
|
|
checkers = self.position.checkers
|
|
diagonalPins = self.position.diagonalPins
|
|
orthogonalPins = self.position.orthogonalPins
|
|
promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7)
|
|
# The rank where each color's side starts
|
|
# TODO: Give names to ranks and files so we don't have to assume a
|
|
# specific board layout when calling get(Rank|File)Mask
|
|
startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1)
|
|
|
|
var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0)
|
|
let epPawn = if epBitboard == 0: Bitboard(0) else: epBitboard.forwardRelativeTo(sideToMove)
|
|
# If we are in check, en passant is only possible if we'd capture the (only)
|
|
# checking pawn with it
|
|
if epBitboard != 0 and self.inCheck() and (epPawn and checkers).countSquares() == 0:
|
|
epBitboard = Bitboard(0)
|
|
|
|
# Single and double pushes
|
|
let
|
|
# If a pawn is pinned diagonally, it cannot move
|
|
pushablePawns = pawns and not diagonalPins
|
|
# Neither can it move if it's pinned orthogonally
|
|
singlePushes = pushablePawns.forwardRelativeTo(sideToMove) and not occupancy and not orthogonalPins
|
|
# Only pawns on their starting rank can double push
|
|
doublePushes = (pushablePawns and startingRank).doubleForwardRelativeTo(sideToMove) and not occupancy and orthogonalPins
|
|
|
|
|
|
proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
## Helper of generateSlidingMoves to generate rook moves
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
occupancy = self.getOccupancy()
|
|
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
|
|
rooks = self.getBitboard(Rook, sideToMove)
|
|
queens = self.getBitboard(Queen, sideToMove)
|
|
movableRooks = not self.position.diagonalPins and (queens or rooks)
|
|
pinMask = self.position.orthogonalPins
|
|
pinnedRooks = movableRooks and pinMask
|
|
unpinnedRooks = movableRooks and not pinnedRooks
|
|
for square in pinnedRooks:
|
|
let
|
|
blockers = occupancy and Rook.getRelevantBlockers(square)
|
|
moveset = getRookMoves(square, blockers)
|
|
for target in moveset and not occupancy and pinMask and mask:
|
|
moves.add(createMove(square, target))
|
|
for target in moveset and enemyPieces and pinMask and mask:
|
|
moves.add(createMove(square, target, Capture))
|
|
for square in unpinnedRooks:
|
|
let
|
|
blockers = occupancy and Rook.getRelevantBlockers(square)
|
|
moveset = getRookMoves(square, blockers)
|
|
for target in moveset and not occupancy and mask:
|
|
moves.add(createMove(square, target))
|
|
for target in moveset and enemyPieces and mask:
|
|
moves.add(createMove(square, target, Capture))
|
|
|
|
|
|
proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
## Helper of generateSlidingMoves to generate bishop moves
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
occupancy = self.getOccupancy()
|
|
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
|
|
bishops = self.getBitboard(Bishop, sideToMove)
|
|
queens = self.getBitboard(Queen, sideToMove)
|
|
movableBishops = not self.position.orthogonalPins and (queens or bishops)
|
|
pinMask = self.position.diagonalPins
|
|
pinnedBishops = movableBishops and pinMask
|
|
unpinnedBishops = movableBishops and not pinnedBishops
|
|
for square in pinnedBishops:
|
|
let
|
|
blockers = occupancy and Bishop.getRelevantBlockers(square)
|
|
moveset = getBishopMoves(square, blockers)
|
|
for target in moveset and pinMask and mask:
|
|
moves.add(createMove(square, target))
|
|
for target in moveset and enemyPieces and pinMask and mask:
|
|
moves.add(createMove(square, target, Capture))
|
|
for square in unpinnedBishops:
|
|
let
|
|
blockers = occupancy and Bishop.getRelevantBlockers(square)
|
|
moveset = getBishopMoves(square, blockers)
|
|
for target in moveset and mask:
|
|
moves.add(createMove(square, target))
|
|
for target in moveset and enemyPieces and mask:
|
|
moves.add(createMove(square, target, Capture))
|
|
|
|
|
|
proc generateKingMoves(self: Chessboard, moves: var MoveList) =
|
|
## Generates all legal king moves for the side to move
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
king = self.getBitboard(King, sideToMove)
|
|
occupancy = self.getOccupancy()
|
|
nonSideToMove = sideToMove.opposite()
|
|
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
|
|
bitboard = getKingAttacks(king.toSquare())
|
|
noKingOccupancy = occupancy and not king
|
|
for square in bitboard and not occupancy:
|
|
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
|
moves.add(createMove(king, square))
|
|
for square in bitboard and enemyPieces:
|
|
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
|
moves.add(createMove(king, square, Capture))
|
|
|
|
|
|
proc generateKnightMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
## Generates all the legal knight moves for the side to move
|
|
let
|
|
sideToMove = self.position.sideToMove
|
|
knights = self.getBitboard(Knight, sideToMove)
|
|
nonSideToMove = sideToMove.opposite()
|
|
pinned = self.position.diagonalPins or self.position.orthogonalPins
|
|
unpinnedKnights = knights and not pinned
|
|
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
|
|
for square in unpinnedKnights:
|
|
let bitboard = getKnightAttacks(square)
|
|
for target in bitboard and mask:
|
|
moves.add(createMove(square, target))
|
|
for target in bitboard 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
|
|
if self.position.halfMoveClock >= 100:
|
|
# Draw by 50-move rule
|
|
return
|
|
let sideToMove = self.position.sideToMove
|
|
# TODO: Check for draw by insufficient material
|
|
# TODO: Check for repetitions (requires zobrist hashing + table)
|
|
self.generateKingMoves(moves)
|
|
if self.position.checkers.countSquares() > 1:
|
|
# King is in double check: no need to generate any more
|
|
# moves
|
|
return
|
|
if not self.inCheck():
|
|
# TODO: Castling
|
|
discard
|
|
|
|
# We pass a mask to our move generators to remove stuff
|
|
# like our friendly pieces from the set of possible
|
|
# target squares, as well as to ensure checks are not
|
|
# ignored
|
|
|
|
var mask: Bitboard
|
|
if not self.inCheck():
|
|
# Not in check: cannot move over friendly pieces
|
|
mask = not self.getOccupancyFor(sideToMove)
|
|
else:
|
|
# We *are* in check (from a single piece, because the two checks
|
|
# case was handled above already). If the piece is a slider, we'll
|
|
# extract the ray from it to our king and add the checking piece to
|
|
# it, meaning the only legal moves are those that either block the
|
|
# check or capture the checking piece. For other non-sliding pieces
|
|
# the ray will be empty so the only legal move will be to capture
|
|
# the checking piece
|
|
let checker = self.position.checkers.lowestSquare()
|
|
mask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard()
|
|
|
|
self.generatePawnMoves(moves, mask)
|
|
self.generateKnightMoves(moves, mask)
|
|
self.generateRookMoves(moves, mask)
|
|
self.generateBishopMoves(moves, mask)
|
|
# Queens are just handled rooks + bishops
|
|
|
|
|
|
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 =
|
|
case piece.kind:
|
|
of Bishop:
|
|
result = 'b'
|
|
of King:
|
|
result = 'k'
|
|
of Knight:
|
|
result = 'n'
|
|
of Pawn:
|
|
result = 'p'
|
|
of Queen:
|
|
result = 'q'
|
|
of Rook:
|
|
result = 'r'
|
|
else:
|
|
discard
|
|
if piece.color == White:
|
|
result = result.toUpperAscii()
|
|
|
|
|
|
proc fromChar*(c: char): Piece =
|
|
var
|
|
kind: PieceKind
|
|
color = Black
|
|
case c.toLowerAscii():
|
|
of 'b':
|
|
kind = Bishop
|
|
of 'k':
|
|
kind = King
|
|
of 'n':
|
|
kind = Knight
|
|
of 'p':
|
|
kind = Pawn
|
|
of 'q':
|
|
kind = Queen
|
|
of 'r':
|
|
kind = Rook
|
|
else:
|
|
discard
|
|
if c.isUpperAscii():
|
|
color = White
|
|
result = Piece(kind: kind, color: color)
|
|
|
|
|
|
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.position.sideToMove == White: "w" else: "b")
|
|
result &= " "
|
|
# Castling availability
|
|
let castleWhite = self.position.castlingAvailability.white
|
|
let castleBlack = self.position.castlingAvailability.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.position.enPassantSquare == nullSquare():
|
|
result &= "-"
|
|
else:
|
|
result &= self.position.enPassantSquare.toAlgebraic()
|
|
result &= " "
|
|
# Halfmove clock
|
|
result &= $self.position.halfMoveClock
|
|
result &= " "
|
|
# Fullmove number
|
|
result &= $self.position.fullMoveCount
|
|
|
|
|
|
when isMainModule:
|
|
import nimfishpkg/tui
|
|
import nimfishpkg/misc
|
|
|
|
basicTests()
|
|
|
|
setControlCHook(proc () {.noconv.} = quit(0))
|
|
|
|
quit(commandLoop())
|