Further improve modularity

This commit is contained in:
Mattia Giambirtone 2024-04-21 11:07:15 +02:00
parent c072576b23
commit 4404ce10b9
4 changed files with 707 additions and 701 deletions

View File

@ -0,0 +1,486 @@
import std/strformat
import std/strutils
import pieces
import magics
import moves
import rays
import bitboards
import position
export pieces, position, bitboards, moves, magics, rays
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 toFEN*(self: Chessboard): string
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)
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 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 `$`*(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 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

View File

@ -1,4 +1,4 @@
import movegen
import board
import std/strformat

View File

@ -12,542 +12,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import std/strutils
import std/strformat
import bitboards
import board
import magics
import pieces
import moves
import position
import rays
import misc
export bitboards, magics, pieces, moves, position, rays
export bitboards, magics, pieces, moves, position, rays, misc, board
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
@ -726,7 +206,126 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) =
# Queens are just handled rooks + bishops
proc isLegal(self: Chessboard, move: Move): bool {.inline.} =
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 isLegal*(self: Chessboard, move: Move): bool {.inline.} =
## Returns whether the given move is legal
var moves = MoveList()
self.generateMoves(moves)
@ -736,183 +335,15 @@ proc isLegal(self: Chessboard, move: Move): bool {.inline.} =
proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} =
## Makes a move on the board
result = move
# Updates checks and pins for the side to move
self.updateChecksAndPins()
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
proc unmakeMove*(self: Chessboard) =
## Reverts to the previous board position,
## if one exists
self.position = self.positions.pop()
self.update()

View File

@ -76,4 +76,93 @@ proc toAlgebraic*(square: Square): string {.inline.} =
return &"{file}{rank}"
proc `$`*(square: Square): string = square.toAlgebraic()
proc `$`*(square: Square): string = square.toAlgebraic()
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 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
func 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()
func 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)