Refactor package structure
This commit is contained in:
parent
fe987576c3
commit
c072576b23
|
@ -1,929 +1,20 @@
|
||||||
# Copyright 2023 Mattia Giambirtone & All Contributors
|
import nimfishpkg/tui
|
||||||
#
|
import nimfishpkg/misc
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
import nimfishpkg/movegen
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import std/strutils
|
|
||||||
import std/strformat
|
|
||||||
|
|
||||||
|
|
||||||
import nimfishpkg/bitboards
|
import nimfishpkg/bitboards
|
||||||
import nimfishpkg/magics
|
|
||||||
import nimfishpkg/pieces
|
|
||||||
import nimfishpkg/moves
|
import nimfishpkg/moves
|
||||||
import nimfishpkg/position
|
import nimfishpkg/pieces
|
||||||
|
import nimfishpkg/magics
|
||||||
import nimfishpkg/rays
|
import nimfishpkg/rays
|
||||||
|
import nimfishpkg/position
|
||||||
|
|
||||||
|
|
||||||
export bitboards, magics, pieces, moves, position
|
export tui, misc, movegen, bitboards, moves, pieces, magics, rays, position
|
||||||
|
|
||||||
|
|
||||||
type
|
|
||||||
|
|
||||||
Chessboard* = ref object
|
|
||||||
## A chessboard
|
|
||||||
|
|
||||||
# The actual board where pieces live
|
|
||||||
grid: array[64, Piece]
|
|
||||||
# The current position
|
|
||||||
position*: Position
|
|
||||||
# List of all previously reached positions
|
|
||||||
positions*: seq[Position]
|
|
||||||
|
|
||||||
|
|
||||||
# A bunch of simple utility functions and forward declarations
|
|
||||||
proc makeMove*(self: Chessboard, move: Move): Move {.discardable.}
|
|
||||||
proc isLegal(self: Chessboard, move: Move): bool {.inline.}
|
|
||||||
proc doMove*(self: Chessboard, move: Move)
|
|
||||||
proc pretty*(self: Chessboard): string
|
|
||||||
proc spawnPiece(self: Chessboard, square: Square, piece: Piece)
|
|
||||||
proc toFEN*(self: Chessboard): string
|
|
||||||
proc unmakeMove*(self: Chessboard)
|
|
||||||
proc movePiece(self: Chessboard, move: Move)
|
|
||||||
proc removePiece(self: Chessboard, square: Square)
|
|
||||||
proc update*(self: Chessboard)
|
|
||||||
func inCheck*(self: Chessboard): bool {.inline.}
|
|
||||||
proc fromChar*(c: char): Piece
|
|
||||||
proc updateChecksAndPins*(self: Chessboard)
|
|
||||||
|
|
||||||
|
|
||||||
func kingSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare())
|
|
||||||
func queenSideRook*(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare())
|
|
||||||
func longCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare())
|
|
||||||
func shortCastleKing*(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare())
|
|
||||||
func longCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "d1".toSquare() else: "d8".toSquare())
|
|
||||||
func shortCastleRook*(color: PieceColor): Square {.inline.} = (if color == White: "f1".toSquare() else: "f8".toSquare())
|
|
||||||
|
|
||||||
|
|
||||||
proc newChessboard: Chessboard =
|
|
||||||
## Returns a new, empty chessboard
|
|
||||||
new(result)
|
|
||||||
for i in 0..63:
|
|
||||||
result.grid[i] = nullPiece()
|
|
||||||
result.position = Position(enPassantSquare: nullSquare(), sideToMove: White)
|
|
||||||
|
|
||||||
|
|
||||||
# Indexing operations
|
|
||||||
func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8]
|
|
||||||
func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece
|
|
||||||
|
|
||||||
|
|
||||||
func getBitboard*(self: Chessboard, kind: PieceKind, color: PieceColor): Bitboard {.inline.} =
|
|
||||||
## Returns the positional bitboard for the given piece kind and color
|
|
||||||
return self.position.getBitboard(kind, color)
|
|
||||||
|
|
||||||
|
|
||||||
func getBitboard*(self: Chessboard, piece: Piece): Bitboard {.inline.} =
|
|
||||||
## Returns the positional bitboard for the given piece type
|
|
||||||
return self.getBitboard(piece.kind, piece.color)
|
|
||||||
|
|
||||||
|
|
||||||
proc newChessboardFromFEN*(fen: string): Chessboard =
|
|
||||||
## Initializes a chessboard with the
|
|
||||||
## position encoded by the given FEN string
|
|
||||||
result = newChessboard()
|
|
||||||
var
|
|
||||||
# Current square in the grid
|
|
||||||
row: int8 = 0
|
|
||||||
column: int8 = 0
|
|
||||||
# Current section in the FEN string
|
|
||||||
section = 0
|
|
||||||
# Current index into the FEN string
|
|
||||||
index = 0
|
|
||||||
# Temporary variable to store a piece
|
|
||||||
piece: Piece
|
|
||||||
# See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation
|
|
||||||
while index <= fen.high():
|
|
||||||
var c = fen[index]
|
|
||||||
if c == ' ':
|
|
||||||
# Next section
|
|
||||||
inc(section)
|
|
||||||
inc(index)
|
|
||||||
continue
|
|
||||||
case section:
|
|
||||||
of 0:
|
|
||||||
# Piece placement data
|
|
||||||
case c.toLowerAscii():
|
|
||||||
# Piece
|
|
||||||
of 'r', 'n', 'b', 'q', 'k', 'p':
|
|
||||||
let square = makeSquare(row, column)
|
|
||||||
piece = c.fromChar()
|
|
||||||
result.position.pieces[piece.color][piece.kind][].setBit(square)
|
|
||||||
result.grid[square] = piece
|
|
||||||
inc(column)
|
|
||||||
of '/':
|
|
||||||
# Next row
|
|
||||||
inc(row)
|
|
||||||
column = 0
|
|
||||||
of '0'..'9':
|
|
||||||
# Skip x columns
|
|
||||||
let x = int(uint8(c) - uint8('0'))
|
|
||||||
if x > 8:
|
|
||||||
raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)")
|
|
||||||
column += int8(x)
|
|
||||||
else:
|
|
||||||
raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'")
|
|
||||||
of 1:
|
|
||||||
# Active color
|
|
||||||
case c:
|
|
||||||
of 'w':
|
|
||||||
result.position.sideToMove = White
|
|
||||||
of 'b':
|
|
||||||
result.position.sideToMove = Black
|
|
||||||
else:
|
|
||||||
raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'")
|
|
||||||
of 2:
|
|
||||||
# Castling availability
|
|
||||||
case c:
|
|
||||||
# TODO
|
|
||||||
of '-':
|
|
||||||
discard
|
|
||||||
of 'K':
|
|
||||||
result.position.castlingAvailability.white.king = true
|
|
||||||
of 'Q':
|
|
||||||
result.position.castlingAvailability.white.queen = true
|
|
||||||
of 'k':
|
|
||||||
result.position.castlingAvailability.black.king = true
|
|
||||||
of 'q':
|
|
||||||
result.position.castlingAvailability.black.queen = true
|
|
||||||
else:
|
|
||||||
raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castlingRights availability section")
|
|
||||||
of 3:
|
|
||||||
# En passant target square
|
|
||||||
case c:
|
|
||||||
of '-':
|
|
||||||
# Field is already uninitialized to the correct state
|
|
||||||
discard
|
|
||||||
else:
|
|
||||||
result.position.enPassantSquare = fen[index..index+1].toSquare()
|
|
||||||
# Square metadata is 2 bytes long
|
|
||||||
inc(index)
|
|
||||||
of 4:
|
|
||||||
# Halfmove clock
|
|
||||||
var s = ""
|
|
||||||
while not fen[index].isSpaceAscii():
|
|
||||||
s.add(fen[index])
|
|
||||||
inc(index)
|
|
||||||
# Backtrack so the space is seen by the
|
|
||||||
# next iteration of the loop
|
|
||||||
dec(index)
|
|
||||||
result.position.halfMoveClock = parseInt(s).int8
|
|
||||||
of 5:
|
|
||||||
# Fullmove number
|
|
||||||
var s = ""
|
|
||||||
while index <= fen.high():
|
|
||||||
s.add(fen[index])
|
|
||||||
inc(index)
|
|
||||||
result.position.fullMoveCount = parseInt(s).int8
|
|
||||||
else:
|
|
||||||
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
|
|
||||||
inc(index)
|
|
||||||
result.updateChecksAndPins()
|
|
||||||
|
|
||||||
|
|
||||||
proc newDefaultChessboard*: Chessboard {.inline.} =
|
|
||||||
## Initializes a chessboard with the
|
|
||||||
## starting position
|
|
||||||
return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
|
||||||
|
|
||||||
|
|
||||||
func countPieces*(self: Chessboard, kind: PieceKind, color: PieceColor): int {.inline.} =
|
|
||||||
## Returns the number of pieces with
|
|
||||||
## the given color and type in the
|
|
||||||
## current position
|
|
||||||
return self.position.pieces[color][kind][].countSquares()
|
|
||||||
|
|
||||||
|
|
||||||
func countPieces*(self: Chessboard, piece: Piece): int {.inline.} =
|
|
||||||
## Returns the number of pieces on the board that
|
|
||||||
## are of the same type and color as the given piece
|
|
||||||
return self.countPieces(piece.kind, piece.color)
|
|
||||||
|
|
||||||
|
|
||||||
func getPiece*(self: Chessboard, square: Square): Piece {.inline.} =
|
|
||||||
## Gets the piece at the given square
|
|
||||||
return self.grid[square]
|
|
||||||
|
|
||||||
|
|
||||||
func getPiece*(self: Chessboard, square: string): Piece {.inline.} =
|
|
||||||
## Gets the piece on the given square
|
|
||||||
## in algebraic notation
|
|
||||||
return self.getPiece(square.toSquare())
|
|
||||||
|
|
||||||
|
|
||||||
func getOccupancyFor(self: Chessboard, color: PieceColor): Bitboard =
|
|
||||||
## Get the occupancy bitboard for every piece of the given color
|
|
||||||
result = Bitboard(0)
|
|
||||||
for b in self.position.pieces[color][]:
|
|
||||||
result = result or b
|
|
||||||
|
|
||||||
|
|
||||||
func getOccupancy(self: Chessboard): Bitboard {.inline.} =
|
|
||||||
## Get the occupancy bitboard for every piece on
|
|
||||||
## the chessboard
|
|
||||||
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
|
|
||||||
|
|
||||||
|
|
||||||
func getPawnAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} =
|
|
||||||
## Returns the locations of the pawns attacking the given square
|
|
||||||
let
|
|
||||||
sq = square.toBitboard()
|
|
||||||
pawns = self.getBitboard(Pawn, attacker)
|
|
||||||
bottomLeft = sq.backwardLeftRelativeTo(attacker)
|
|
||||||
bottomRight = sq.backwardRightRelativeTo(attacker)
|
|
||||||
return pawns and (bottomLeft or bottomRight)
|
|
||||||
|
|
||||||
|
|
||||||
func getKingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard {.inline.} =
|
|
||||||
## Returns the location of the king if it is attacking the given square
|
|
||||||
result = Bitboard(0)
|
|
||||||
let
|
|
||||||
king = self.getBitboard(King, attacker)
|
|
||||||
if (getKingAttacks(square) and king) != 0:
|
|
||||||
result = result or king
|
|
||||||
|
|
||||||
|
|
||||||
func getKnightAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
|
||||||
## Returns the locations of the knights attacking the given square
|
|
||||||
let
|
|
||||||
knights = self.getBitboard(Knight, attacker)
|
|
||||||
result = Bitboard(0)
|
|
||||||
for knight in knights:
|
|
||||||
let knightBB = knight.toBitboard()
|
|
||||||
if (getKnightAttacks(knight) and knightBB) != 0:
|
|
||||||
result = result or knightBB
|
|
||||||
|
|
||||||
|
|
||||||
proc getSlidingAttacks(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
|
||||||
## Returns the locations of the sliding pieces attacking the given square
|
|
||||||
let
|
|
||||||
queens = self.getBitboard(Queen, attacker)
|
|
||||||
rooks = self.getBitboard(Rook, attacker) or queens
|
|
||||||
bishops = self.getBitboard(Bishop, attacker) or queens
|
|
||||||
occupancy = self.getOccupancy()
|
|
||||||
squareBB = square.toBitboard()
|
|
||||||
result = Bitboard(0)
|
|
||||||
for rook in rooks:
|
|
||||||
let
|
|
||||||
blockers = occupancy and Rook.getRelevantBlockers(rook)
|
|
||||||
moves = getRookMoves(rook, blockers)
|
|
||||||
# Attack set intersects our chosen square
|
|
||||||
if (moves and squareBB) != 0:
|
|
||||||
result = result or rook.toBitboard()
|
|
||||||
for bishop in bishops:
|
|
||||||
let
|
|
||||||
blockers = occupancy and Bishop.getRelevantBlockers(bishop)
|
|
||||||
moves = getBishopMoves(bishop, blockers)
|
|
||||||
if (moves and squareBB) != 0:
|
|
||||||
result = result or bishop.toBitboard()
|
|
||||||
|
|
||||||
|
|
||||||
proc getAttacksTo*(self: Chessboard, square: Square, attacker: PieceColor): Bitboard =
|
|
||||||
## Computes the attack bitboard for the given square from
|
|
||||||
## the given side
|
|
||||||
result = Bitboard(0)
|
|
||||||
result = result or self.getPawnAttacks(square, attacker)
|
|
||||||
result = result or self.getKingAttacks(square, attacker)
|
|
||||||
result = result or self.getKnightAttacks(square, attacker)
|
|
||||||
result = result or self.getSlidingAttacks(square, attacker)
|
|
||||||
|
|
||||||
|
|
||||||
proc isOccupancyAttacked*(self: Chessboard, square: Square, occupancy: Bitboard): bool =
|
|
||||||
## Returns whether the given square would be attacked by the
|
|
||||||
## enemy side if the board had the given occupancy. This function
|
|
||||||
## is necessary mostly to make sure sliding attacks can check the
|
|
||||||
## king properly: due to how we generate our attack bitboards, if
|
|
||||||
## the king moved backwards along a ray from a slider we would not
|
|
||||||
## consider it to be in check (because the ray stops at the first
|
|
||||||
## blocker). In order to fix that, in generateKingMoves() we use this
|
|
||||||
## function and pass in the board's occupancy without the moving king so
|
|
||||||
## that we can pick the correct magic bitboard and ray. Also, since this
|
|
||||||
## function doesn't need to generate all the attacks to know whether a
|
|
||||||
## given square is unsafe, it can short circuit at the first attack and
|
|
||||||
## exit early, unlike getAttacksTo
|
|
||||||
let
|
|
||||||
sideToMove = self.position.sideToMove
|
|
||||||
nonSideToMove = sideToMove.opposite()
|
|
||||||
knights = self.getBitboard(Knight, nonSideToMove)
|
|
||||||
|
|
||||||
# Let's do the cheap ones first (the ones which are precomputed)
|
|
||||||
if (getKnightAttacks(square) and knights) != 0:
|
|
||||||
return true
|
|
||||||
|
|
||||||
let king = self.getBitboard(King, nonSideToMove)
|
|
||||||
|
|
||||||
if (getKingAttacks(square) and king) != 0:
|
|
||||||
return true
|
|
||||||
|
|
||||||
let
|
|
||||||
queens = self.getBitboard(Queen, nonSideToMove)
|
|
||||||
bishops = self.getBitboard(Bishop, nonSideToMove) or queens
|
|
||||||
|
|
||||||
if (getBishopMoves(square, occupancy) and bishops) != 0:
|
|
||||||
return true
|
|
||||||
|
|
||||||
let rooks = self.getBitboard(Rook, nonSideToMove) or queens
|
|
||||||
|
|
||||||
if (getRookMoves(square, occupancy) and rooks) != 0:
|
|
||||||
return true
|
|
||||||
|
|
||||||
# TODO: Precompute pawn moves as well?
|
|
||||||
let pawns = self.getBitboard(Pawn, nonSideToMove)
|
|
||||||
|
|
||||||
if (self.getPawnAttacks(square, nonSideToMove) and pawns) != 0:
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
proc updateChecksAndPins*(self: Chessboard) =
|
|
||||||
## Updates internal metadata about checks and
|
|
||||||
## pinned pieces
|
|
||||||
|
|
||||||
# *Ahem*, stolen from https://github.com/Ciekce/voidstar/blob/424ac4624011271c4d1dbd743602c23f6dbda1de/src/position.rs
|
|
||||||
# Can you tell I'm a *great* coder?
|
|
||||||
let
|
|
||||||
sideToMove = self.position.sideToMove
|
|
||||||
nonSideToMove = sideToMove.opposite()
|
|
||||||
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
|
|
||||||
friendlyPieces = self.getOccupancyFor(sideToMove)
|
|
||||||
enemyPieces = self.getOccupancyFor(nonSideToMove)
|
|
||||||
|
|
||||||
# Update checks
|
|
||||||
self.position.checkers = self.getAttacksTo(friendlyKing, nonSideToMove)
|
|
||||||
# Update pins
|
|
||||||
self.position.diagonalPins = Bitboard(0)
|
|
||||||
self.position.orthogonalPins = Bitboard(0)
|
|
||||||
|
|
||||||
let
|
|
||||||
diagonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Bishop, nonSideToMove)
|
|
||||||
orthogonalAttackers = self.getBitboard(Queen, nonSideToMove) or self.getBitboard(Rook, nonSideToMove)
|
|
||||||
canPinDiagonally = diagonalAttackers and getBishopMoves(friendlyKing, enemyPieces)
|
|
||||||
canPinOrthogonally = orthogonalAttackers and getRookMoves(friendlyKing, enemyPieces)
|
|
||||||
|
|
||||||
for piece in canPinDiagonally:
|
|
||||||
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
|
|
||||||
|
|
||||||
# Is the pinning ray obstructed by any of our friendly pieces? If so, the
|
|
||||||
# piece is pinned
|
|
||||||
if (pinningRay and friendlyPieces).countSquares() > 0:
|
|
||||||
self.position.diagonalPins = self.position.diagonalPins or pinningRay
|
|
||||||
|
|
||||||
for piece in canPinOrthogonally:
|
|
||||||
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
|
|
||||||
if (pinningRay and friendlyPieces).countSquares() > 0:
|
|
||||||
self.position.orthogonalPins = self.position.orthogonalPins or pinningRay
|
|
||||||
|
|
||||||
|
|
||||||
func inCheck(self: Chessboard): bool {.inline.} =
|
|
||||||
## Returns if the current side to move is in check
|
|
||||||
return self.position.checkers != 0
|
|
||||||
|
|
||||||
|
|
||||||
proc canCastle*(self: Chessboard, side: PieceColor): tuple[king, queen: bool] =
|
|
||||||
## Returns if the current side to move can castle
|
|
||||||
return (false, false) # TODO
|
|
||||||
|
|
||||||
|
|
||||||
proc removePieceFromBitboard(self: Chessboard, square: Square) =
|
|
||||||
## Removes a piece at the given square in the chessboard from
|
|
||||||
## its respective bitboard
|
|
||||||
let piece = self.grid[square]
|
|
||||||
self.position.pieces[piece.color][piece.kind][].clearBit(square)
|
|
||||||
|
|
||||||
|
|
||||||
proc addPieceToBitboard(self: Chessboard, square: Square, piece: Piece) =
|
|
||||||
## Adds the given piece at the given square in the chessboard to
|
|
||||||
## its respective bitboard
|
|
||||||
self.position.pieces[piece.color][piece.kind][].setBit(square)
|
|
||||||
|
|
||||||
|
|
||||||
proc spawnPiece(self: Chessboard, square: Square, piece: Piece) =
|
|
||||||
## Internal helper to "spawn" a given piece at the given
|
|
||||||
## square
|
|
||||||
when not defined(danger):
|
|
||||||
doAssert self.grid[square].kind == Empty
|
|
||||||
self.addPieceToBitboard(square, piece)
|
|
||||||
self.grid[square] = piece
|
|
||||||
|
|
||||||
|
|
||||||
proc removePiece(self: Chessboard, square: Square) =
|
|
||||||
## Removes a piece from the board, updating necessary
|
|
||||||
## metadata
|
|
||||||
var piece = self.grid[square]
|
|
||||||
when not defined(danger):
|
|
||||||
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
|
||||||
self.removePieceFromBitboard(square)
|
|
||||||
self.grid[square] = nullPiece()
|
|
||||||
|
|
||||||
|
|
||||||
proc movePiece(self: Chessboard, move: Move) =
|
|
||||||
## Internal helper to move a piece from
|
|
||||||
## its current square to a target square
|
|
||||||
let piece = self.grid[move.startSquare]
|
|
||||||
when not defined(danger):
|
|
||||||
let targetSquare = self.getPiece(move.targetSquare)
|
|
||||||
if targetSquare.color != None:
|
|
||||||
raise newException(AccessViolationDefect, &"{piece} at {move.startSquare} attempted to overwrite {targetSquare} at {move.targetSquare}: {move}")
|
|
||||||
# Update positional metadata
|
|
||||||
self.removePiece(move.startSquare)
|
|
||||||
self.spawnPiece(move.targetSquare, piece)
|
|
||||||
|
|
||||||
|
|
||||||
proc doMove*(self: Chessboard, move: Move) =
|
|
||||||
## Internal function called by makeMove after
|
|
||||||
## performing legality checks. Can be used in
|
|
||||||
## performance-critical paths where a move is
|
|
||||||
## already known to be legal (i.e. during search)
|
|
||||||
|
|
||||||
# Record final position for future reference
|
|
||||||
self.positions.add(self.position)
|
|
||||||
|
|
||||||
# Final checks
|
|
||||||
let piece = self.grid[move.startSquare]
|
|
||||||
when not defined(danger):
|
|
||||||
doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}"
|
|
||||||
|
|
||||||
var
|
|
||||||
halfMoveClock = self.position.halfMoveClock
|
|
||||||
fullMoveCount = self.position.fullMoveCount
|
|
||||||
castlingRights = self.position.castlingRights
|
|
||||||
enPassantTarget = nullSquare()
|
|
||||||
# Needed to detect draw by the 50 move rule
|
|
||||||
if piece.kind == Pawn or move.isCapture() or move.isEnPassant():
|
|
||||||
# Number of half-moves since the last reversible half-move
|
|
||||||
halfMoveClock = 0
|
|
||||||
else:
|
|
||||||
inc(halfMoveClock)
|
|
||||||
if piece.color == Black:
|
|
||||||
inc(fullMoveCount)
|
|
||||||
|
|
||||||
if move.isDoublePush():
|
|
||||||
enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()
|
|
||||||
|
|
||||||
# Create new position
|
|
||||||
self.position = Position(plyFromRoot: self.position.plyFromRoot + 1,
|
|
||||||
halfMoveClock: halfMoveClock,
|
|
||||||
fullMoveCount: fullMoveCount,
|
|
||||||
sideToMove: self.position.sideToMove.opposite(),
|
|
||||||
castlingRights: castlingRights,
|
|
||||||
enPassantSquare: enPassantTarget,
|
|
||||||
pieces: self.position.pieces
|
|
||||||
)
|
|
||||||
# Update position metadata
|
|
||||||
|
|
||||||
if move.isEnPassant():
|
|
||||||
# Make the en passant pawn disappear
|
|
||||||
self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare())
|
|
||||||
|
|
||||||
if move.isCapture():
|
|
||||||
# Get rid of captured pieces
|
|
||||||
self.removePiece(move.targetSquare)
|
|
||||||
|
|
||||||
# Move the piece to its target square
|
|
||||||
self.movePiece(move)
|
|
||||||
# TODO: Castling!
|
|
||||||
if move.isPromotion():
|
|
||||||
# Move is a pawn promotion: get rid of the pawn
|
|
||||||
# and spawn a new piece
|
|
||||||
self.removePiece(move.targetSquare)
|
|
||||||
case move.getPromotionType():
|
|
||||||
of PromoteToBishop:
|
|
||||||
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color))
|
|
||||||
of PromoteToKnight:
|
|
||||||
self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color))
|
|
||||||
of PromoteToRook:
|
|
||||||
self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color))
|
|
||||||
of PromoteToQueen:
|
|
||||||
self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color))
|
|
||||||
else:
|
|
||||||
# Unreachable
|
|
||||||
discard
|
|
||||||
# Updates checks and pins for the side to move
|
|
||||||
self.updateChecksAndPins()
|
|
||||||
|
|
||||||
|
|
||||||
proc update*(self: Chessboard) =
|
|
||||||
## Updates the internal grid representation
|
|
||||||
## according to the positional data stored
|
|
||||||
## in the chessboard
|
|
||||||
for i in 0..63:
|
|
||||||
self.grid[i] = nullPiece()
|
|
||||||
for sq in self.position.pieces[White][Pawn][]:
|
|
||||||
self.grid[sq] = Piece(color: White, kind: Pawn)
|
|
||||||
for sq in self.position.pieces[Black][Pawn][]:
|
|
||||||
self.grid[sq] = Piece(color: Black, kind: Pawn)
|
|
||||||
for sq in self.position.pieces[White][Bishop][]:
|
|
||||||
self.grid[sq] = Piece(color: White, kind: Bishop)
|
|
||||||
for sq in self.position.pieces[Black][Bishop][]:
|
|
||||||
self.grid[sq] = Piece(color: Black, kind: Bishop)
|
|
||||||
for sq in self.position.pieces[White][Knight][]:
|
|
||||||
self.grid[sq] = Piece(color: White, kind: Knight)
|
|
||||||
for sq in self.position.pieces[Black][Knight][]:
|
|
||||||
self.grid[sq] = Piece(color: Black, kind: Knight)
|
|
||||||
for sq in self.position.pieces[White][Rook][]:
|
|
||||||
self.grid[sq] = Piece(color: White, kind: Rook)
|
|
||||||
for sq in self.position.pieces[Black][Rook][]:
|
|
||||||
self.grid[sq] = Piece(color: Black, kind: Rook)
|
|
||||||
for sq in self.position.pieces[White][Queen][]:
|
|
||||||
self.grid[sq] = Piece(color: White, kind: Queen)
|
|
||||||
for sq in self.position.pieces[Black][Queen][]:
|
|
||||||
self.grid[sq] = Piece(color: Black, kind: Queen)
|
|
||||||
for sq in self.position.pieces[White][King][]:
|
|
||||||
self.grid[sq] = Piece(color: White, kind: King)
|
|
||||||
for sq in self.position.pieces[Black][King][]:
|
|
||||||
self.grid[sq] = Piece(color: Black, kind: King)
|
|
||||||
|
|
||||||
|
|
||||||
proc unmakeMove*(self: Chessboard) =
|
|
||||||
## Reverts to the previous board position,
|
|
||||||
## if one exists
|
|
||||||
self.position = self.positions.pop()
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
|
|
||||||
proc generatePawnMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
||||||
let
|
|
||||||
sideToMove = self.position.sideToMove
|
|
||||||
pawns = self.getBitboard(Pawn, sideToMove)
|
|
||||||
occupancy = self.getOccupancy()
|
|
||||||
# We can only capture enemy pieces (except the king)
|
|
||||||
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
|
|
||||||
# We can only capture diagonally and forward
|
|
||||||
rightMovement = pawns.forwardRightRelativeTo(sideToMove)
|
|
||||||
leftMovement = pawns.forwardLeftRelativeTo(sideToMove)
|
|
||||||
epTarget = self.position.enPassantSquare
|
|
||||||
checkers = self.position.checkers
|
|
||||||
diagonalPins = self.position.diagonalPins
|
|
||||||
orthogonalPins = self.position.orthogonalPins
|
|
||||||
promotionRank = if sideToMove == White: getRankMask(0) else: getRankMask(7)
|
|
||||||
# The rank where each color's side starts
|
|
||||||
# TODO: Give names to ranks and files so we don't have to assume a
|
|
||||||
# specific board layout when calling get(Rank|File)Mask
|
|
||||||
startingRank = if sideToMove == White: getRankMask(6) else: getRankMask(1)
|
|
||||||
|
|
||||||
var epBitboard = if epTarget != nullSquare(): epTarget.toBitboard() else: Bitboard(0)
|
|
||||||
let epPawn = if epBitboard == 0: Bitboard(0) else: epBitboard.forwardRelativeTo(sideToMove)
|
|
||||||
# If we are in check, en passant is only possible if we'd capture the (only)
|
|
||||||
# checking pawn with it
|
|
||||||
if epBitboard != 0 and self.inCheck() and (epPawn and checkers).countSquares() == 0:
|
|
||||||
epBitboard = Bitboard(0)
|
|
||||||
|
|
||||||
# Single and double pushes
|
|
||||||
let
|
|
||||||
# If a pawn is pinned diagonally, it cannot move
|
|
||||||
pushablePawns = pawns and not diagonalPins
|
|
||||||
# Neither can it move if it's pinned orthogonally
|
|
||||||
singlePushes = pushablePawns.forwardRelativeTo(sideToMove) and not occupancy and not orthogonalPins
|
|
||||||
# Only pawns on their starting rank can double push
|
|
||||||
doublePushes = (pushablePawns and startingRank).doubleForwardRelativeTo(sideToMove) and not occupancy and orthogonalPins
|
|
||||||
|
|
||||||
|
|
||||||
proc generateRookMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
||||||
## Helper of generateSlidingMoves to generate rook moves
|
|
||||||
let
|
|
||||||
sideToMove = self.position.sideToMove
|
|
||||||
occupancy = self.getOccupancy()
|
|
||||||
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
|
|
||||||
rooks = self.getBitboard(Rook, sideToMove)
|
|
||||||
queens = self.getBitboard(Queen, sideToMove)
|
|
||||||
movableRooks = not self.position.diagonalPins and (queens or rooks)
|
|
||||||
pinMask = self.position.orthogonalPins
|
|
||||||
pinnedRooks = movableRooks and pinMask
|
|
||||||
unpinnedRooks = movableRooks and not pinnedRooks
|
|
||||||
for square in pinnedRooks:
|
|
||||||
let
|
|
||||||
blockers = occupancy and Rook.getRelevantBlockers(square)
|
|
||||||
moveset = getRookMoves(square, blockers)
|
|
||||||
for target in moveset and not occupancy and pinMask and mask:
|
|
||||||
moves.add(createMove(square, target))
|
|
||||||
for target in moveset and enemyPieces and pinMask and mask:
|
|
||||||
moves.add(createMove(square, target, Capture))
|
|
||||||
for square in unpinnedRooks:
|
|
||||||
let
|
|
||||||
blockers = occupancy and Rook.getRelevantBlockers(square)
|
|
||||||
moveset = getRookMoves(square, blockers)
|
|
||||||
for target in moveset and not occupancy and mask:
|
|
||||||
moves.add(createMove(square, target))
|
|
||||||
for target in moveset and enemyPieces and mask:
|
|
||||||
moves.add(createMove(square, target, Capture))
|
|
||||||
|
|
||||||
|
|
||||||
proc generateBishopMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
||||||
## Helper of generateSlidingMoves to generate bishop moves
|
|
||||||
let
|
|
||||||
sideToMove = self.position.sideToMove
|
|
||||||
occupancy = self.getOccupancy()
|
|
||||||
enemyPieces = self.getOccupancyFor(sideToMove.opposite())
|
|
||||||
bishops = self.getBitboard(Bishop, sideToMove)
|
|
||||||
queens = self.getBitboard(Queen, sideToMove)
|
|
||||||
movableBishops = not self.position.orthogonalPins and (queens or bishops)
|
|
||||||
pinMask = self.position.diagonalPins
|
|
||||||
pinnedBishops = movableBishops and pinMask
|
|
||||||
unpinnedBishops = movableBishops and not pinnedBishops
|
|
||||||
for square in pinnedBishops:
|
|
||||||
let
|
|
||||||
blockers = occupancy and Bishop.getRelevantBlockers(square)
|
|
||||||
moveset = getBishopMoves(square, blockers)
|
|
||||||
for target in moveset and pinMask and mask:
|
|
||||||
moves.add(createMove(square, target))
|
|
||||||
for target in moveset and enemyPieces and pinMask and mask:
|
|
||||||
moves.add(createMove(square, target, Capture))
|
|
||||||
for square in unpinnedBishops:
|
|
||||||
let
|
|
||||||
blockers = occupancy and Bishop.getRelevantBlockers(square)
|
|
||||||
moveset = getBishopMoves(square, blockers)
|
|
||||||
for target in moveset and mask:
|
|
||||||
moves.add(createMove(square, target))
|
|
||||||
for target in moveset and enemyPieces and mask:
|
|
||||||
moves.add(createMove(square, target, Capture))
|
|
||||||
|
|
||||||
|
|
||||||
proc generateKingMoves(self: Chessboard, moves: var MoveList) =
|
|
||||||
## Generates all legal king moves for the side to move
|
|
||||||
let
|
|
||||||
sideToMove = self.position.sideToMove
|
|
||||||
king = self.getBitboard(King, sideToMove)
|
|
||||||
occupancy = self.getOccupancy()
|
|
||||||
nonSideToMove = sideToMove.opposite()
|
|
||||||
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
|
|
||||||
bitboard = getKingAttacks(king.toSquare())
|
|
||||||
noKingOccupancy = occupancy and not king
|
|
||||||
for square in bitboard and not occupancy:
|
|
||||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
|
||||||
moves.add(createMove(king, square))
|
|
||||||
for square in bitboard and enemyPieces:
|
|
||||||
if not self.isOccupancyAttacked(square, noKingOccupancy):
|
|
||||||
moves.add(createMove(king, square, Capture))
|
|
||||||
|
|
||||||
|
|
||||||
proc generateKnightMoves(self: Chessboard, moves: var MoveList, mask: Bitboard) =
|
|
||||||
## Generates all the legal knight moves for the side to move
|
|
||||||
let
|
|
||||||
sideToMove = self.position.sideToMove
|
|
||||||
knights = self.getBitboard(Knight, sideToMove)
|
|
||||||
nonSideToMove = sideToMove.opposite()
|
|
||||||
pinned = self.position.diagonalPins or self.position.orthogonalPins
|
|
||||||
unpinnedKnights = knights and not pinned
|
|
||||||
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
|
|
||||||
for square in unpinnedKnights:
|
|
||||||
let bitboard = getKnightAttacks(square)
|
|
||||||
for target in bitboard and mask:
|
|
||||||
moves.add(createMove(square, target))
|
|
||||||
for target in bitboard and enemyPieces:
|
|
||||||
moves.add(createMove(square, target, Capture))
|
|
||||||
|
|
||||||
|
|
||||||
proc generateMoves*(self: Chessboard, moves: var MoveList) =
|
|
||||||
## Generates the list of all possible legal moves
|
|
||||||
## in the current position
|
|
||||||
if self.position.halfMoveClock >= 100:
|
|
||||||
# Draw by 50-move rule
|
|
||||||
return
|
|
||||||
let sideToMove = self.position.sideToMove
|
|
||||||
# TODO: Check for draw by insufficient material
|
|
||||||
# TODO: Check for repetitions (requires zobrist hashing + table)
|
|
||||||
self.generateKingMoves(moves)
|
|
||||||
if self.position.checkers.countSquares() > 1:
|
|
||||||
# King is in double check: no need to generate any more
|
|
||||||
# moves
|
|
||||||
return
|
|
||||||
if not self.inCheck():
|
|
||||||
# TODO: Castling
|
|
||||||
discard
|
|
||||||
|
|
||||||
# We pass a mask to our move generators to remove stuff
|
|
||||||
# like our friendly pieces from the set of possible
|
|
||||||
# target squares, as well as to ensure checks are not
|
|
||||||
# ignored
|
|
||||||
|
|
||||||
var mask: Bitboard
|
|
||||||
if not self.inCheck():
|
|
||||||
# Not in check: cannot move over friendly pieces
|
|
||||||
mask = not self.getOccupancyFor(sideToMove)
|
|
||||||
else:
|
|
||||||
# We *are* in check (from a single piece, because the two checks
|
|
||||||
# case was handled above already). If the piece is a slider, we'll
|
|
||||||
# extract the ray from it to our king and add the checking piece to
|
|
||||||
# it, meaning the only legal moves are those that either block the
|
|
||||||
# check or capture the checking piece. For other non-sliding pieces
|
|
||||||
# the ray will be empty so the only legal move will be to capture
|
|
||||||
# the checking piece
|
|
||||||
let checker = self.position.checkers.lowestSquare()
|
|
||||||
mask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checker.toBitboard()
|
|
||||||
|
|
||||||
self.generatePawnMoves(moves, mask)
|
|
||||||
self.generateKnightMoves(moves, mask)
|
|
||||||
self.generateRookMoves(moves, mask)
|
|
||||||
self.generateBishopMoves(moves, mask)
|
|
||||||
# Queens are just handled rooks + bishops
|
|
||||||
|
|
||||||
|
|
||||||
proc isLegal(self: Chessboard, move: Move): bool {.inline.} =
|
|
||||||
## Returns whether the given move is legal
|
|
||||||
var moves = MoveList()
|
|
||||||
self.generateMoves(moves)
|
|
||||||
return move in moves
|
|
||||||
|
|
||||||
|
|
||||||
proc makeMove*(self: Chessboard, move: Move): Move {.discardable.} =
|
|
||||||
## Makes a move on the board
|
|
||||||
result = move
|
|
||||||
if not self.isLegal(move):
|
|
||||||
return nullMove()
|
|
||||||
self.doMove(move)
|
|
||||||
|
|
||||||
|
|
||||||
proc toChar*(piece: Piece): char =
|
|
||||||
case piece.kind:
|
|
||||||
of Bishop:
|
|
||||||
result = 'b'
|
|
||||||
of King:
|
|
||||||
result = 'k'
|
|
||||||
of Knight:
|
|
||||||
result = 'n'
|
|
||||||
of Pawn:
|
|
||||||
result = 'p'
|
|
||||||
of Queen:
|
|
||||||
result = 'q'
|
|
||||||
of Rook:
|
|
||||||
result = 'r'
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
if piece.color == White:
|
|
||||||
result = result.toUpperAscii()
|
|
||||||
|
|
||||||
|
|
||||||
proc fromChar*(c: char): Piece =
|
|
||||||
var
|
|
||||||
kind: PieceKind
|
|
||||||
color = Black
|
|
||||||
case c.toLowerAscii():
|
|
||||||
of 'b':
|
|
||||||
kind = Bishop
|
|
||||||
of 'k':
|
|
||||||
kind = King
|
|
||||||
of 'n':
|
|
||||||
kind = Knight
|
|
||||||
of 'p':
|
|
||||||
kind = Pawn
|
|
||||||
of 'q':
|
|
||||||
kind = Queen
|
|
||||||
of 'r':
|
|
||||||
kind = Rook
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
if c.isUpperAscii():
|
|
||||||
color = White
|
|
||||||
result = Piece(kind: kind, color: color)
|
|
||||||
|
|
||||||
|
|
||||||
proc `$`*(self: Chessboard): string =
|
|
||||||
result &= "- - - - - - - -"
|
|
||||||
var file = 8
|
|
||||||
for i in 0..7:
|
|
||||||
result &= "\n"
|
|
||||||
for j in 0..7:
|
|
||||||
let piece = self.grid[makeSquare(i, j)]
|
|
||||||
if piece.kind == Empty:
|
|
||||||
result &= "x "
|
|
||||||
continue
|
|
||||||
result &= &"{piece.toChar()} "
|
|
||||||
result &= &"{file}"
|
|
||||||
dec(file)
|
|
||||||
result &= "\n- - - - - - - -"
|
|
||||||
result &= "\na b c d e f g h"
|
|
||||||
|
|
||||||
|
|
||||||
proc toPretty*(piece: Piece): string =
|
|
||||||
case piece.color:
|
|
||||||
of White:
|
|
||||||
case piece.kind:
|
|
||||||
of King:
|
|
||||||
return "\U2654"
|
|
||||||
of Queen:
|
|
||||||
return "\U2655"
|
|
||||||
of Rook:
|
|
||||||
return "\U2656"
|
|
||||||
of Bishop:
|
|
||||||
return "\U2657"
|
|
||||||
of Knight:
|
|
||||||
return "\U2658"
|
|
||||||
of Pawn:
|
|
||||||
return "\U2659"
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
of Black:
|
|
||||||
case piece.kind:
|
|
||||||
of King:
|
|
||||||
return "\U265A"
|
|
||||||
of Queen:
|
|
||||||
return "\U265B"
|
|
||||||
of Rook:
|
|
||||||
return "\U265C"
|
|
||||||
of Bishop:
|
|
||||||
return "\U265D"
|
|
||||||
of Knight:
|
|
||||||
return "\U265E"
|
|
||||||
of Pawn:
|
|
||||||
return "\240\159\168\133"
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
else:
|
|
||||||
discard
|
|
||||||
|
|
||||||
|
|
||||||
proc pretty*(self: Chessboard): string =
|
|
||||||
## Returns a colored version of the
|
|
||||||
## board for easier visualization
|
|
||||||
var file = 8
|
|
||||||
for i in 0..7:
|
|
||||||
if i > 0:
|
|
||||||
result &= "\n"
|
|
||||||
for j in 0..7:
|
|
||||||
# Equivalent to (i + j) mod 2
|
|
||||||
# (I'm just evil)
|
|
||||||
if ((i + j) and 1) == 0:
|
|
||||||
result &= "\x1b[39;44;1m"
|
|
||||||
else:
|
|
||||||
result &= "\x1b[39;40;1m"
|
|
||||||
let piece = self.grid[makeSquare(i, j)]
|
|
||||||
if piece.kind == Empty:
|
|
||||||
result &= " \x1b[0m"
|
|
||||||
else:
|
|
||||||
result &= &"{piece.toPretty()} \x1b[0m"
|
|
||||||
result &= &" \x1b[33;1m{file}\x1b[0m"
|
|
||||||
dec(file)
|
|
||||||
|
|
||||||
result &= "\n\x1b[31;1ma b c d e f g h"
|
|
||||||
result &= "\x1b[0m"
|
|
||||||
|
|
||||||
|
|
||||||
proc toFEN*(self: Chessboard): string =
|
|
||||||
## Returns a FEN string of the current
|
|
||||||
## position in the chessboard
|
|
||||||
var skip: int
|
|
||||||
# Piece placement data
|
|
||||||
for i in 0..7:
|
|
||||||
skip = 0
|
|
||||||
for j in 0..7:
|
|
||||||
let piece = self.grid[makeSquare(i, j)]
|
|
||||||
if piece.kind == Empty:
|
|
||||||
inc(skip)
|
|
||||||
elif skip > 0:
|
|
||||||
result &= &"{skip}{piece.toChar()}"
|
|
||||||
skip = 0
|
|
||||||
else:
|
|
||||||
result &= piece.toChar()
|
|
||||||
if skip > 0:
|
|
||||||
result &= $skip
|
|
||||||
if i < 7:
|
|
||||||
result &= "/"
|
|
||||||
result &= " "
|
|
||||||
# Active color
|
|
||||||
result &= (if self.position.sideToMove == White: "w" else: "b")
|
|
||||||
result &= " "
|
|
||||||
# Castling availability
|
|
||||||
let castleWhite = self.position.castlingAvailability.white
|
|
||||||
let castleBlack = self.position.castlingAvailability.black
|
|
||||||
if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
|
|
||||||
result &= "-"
|
|
||||||
else:
|
|
||||||
if castleWhite.king:
|
|
||||||
result &= "K"
|
|
||||||
if castleWhite.queen:
|
|
||||||
result &= "Q"
|
|
||||||
if castleBlack.king:
|
|
||||||
result &= "k"
|
|
||||||
if castleBlack.queen:
|
|
||||||
result &= "q"
|
|
||||||
result &= " "
|
|
||||||
# En passant target
|
|
||||||
if self.position.enPassantSquare == nullSquare():
|
|
||||||
result &= "-"
|
|
||||||
else:
|
|
||||||
result &= self.position.enPassantSquare.toAlgebraic()
|
|
||||||
result &= " "
|
|
||||||
# Halfmove clock
|
|
||||||
result &= $self.position.halfMoveClock
|
|
||||||
result &= " "
|
|
||||||
# Fullmove number
|
|
||||||
result &= $self.position.fullMoveCount
|
|
||||||
|
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
import nimfishpkg/tui
|
|
||||||
import nimfishpkg/misc
|
|
||||||
|
|
||||||
basicTests()
|
basicTests()
|
||||||
|
|
||||||
setControlCHook(proc () {.noconv.} = quit(0))
|
setControlCHook(proc () {.noconv.} = quit(0))
|
||||||
|
|
||||||
quit(commandLoop())
|
quit(commandLoop())
|
|
@ -1,4 +1,4 @@
|
||||||
import ../nimfish
|
import movegen
|
||||||
|
|
||||||
|
|
||||||
import std/strformat
|
import std/strformat
|
||||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
||||||
import ../nimfish
|
import movegen
|
||||||
|
|
||||||
|
|
||||||
import std/strformat
|
import std/strformat
|
||||||
|
|
Loading…
Reference in New Issue