Refactor package structure

This commit is contained in:
Mattia Giambirtone 2024-04-21 10:51:11 +02:00
parent fe987576c3
commit c072576b23
4 changed files with 928 additions and 919 deletions

View File

@ -1,929 +1,20 @@
# 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/tui
import nimfishpkg/misc
import nimfishpkg/movegen
import nimfishpkg/bitboards
import nimfishpkg/magics
import nimfishpkg/pieces
import nimfishpkg/moves
import nimfishpkg/position
import nimfishpkg/pieces
import nimfishpkg/magics
import nimfishpkg/rays
import nimfishpkg/position
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
export tui, misc, movegen, bitboards, moves, pieces, magics, rays, position
when isMainModule:
import nimfishpkg/tui
import nimfishpkg/misc
basicTests()
setControlCHook(proc () {.noconv.} = quit(0))
quit(commandLoop())
quit(commandLoop())

View File

@ -1,4 +1,4 @@
import ../nimfish
import movegen
import std/strformat

View File

@ -0,0 +1,918 @@
# 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 bitboards
import magics
import pieces
import moves
import position
import rays
export bitboards, magics, pieces, moves, position, 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 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

View File

@ -1,4 +1,4 @@
import ../nimfish
import movegen
import std/strformat