2024-04-21 11:11:28 +02:00
|
|
|
# Copyright 2024 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.
|
|
|
|
|
|
|
|
## Implementation of a simple chessboard
|
2024-04-21 11:07:15 +02:00
|
|
|
import std/strformat
|
|
|
|
import std/strutils
|
|
|
|
|
|
|
|
|
|
|
|
import pieces
|
|
|
|
import magics
|
|
|
|
import moves
|
|
|
|
import rays
|
|
|
|
import bitboards
|
|
|
|
import position
|
|
|
|
|
|
|
|
|
|
|
|
export pieces, position, bitboards, moves, magics, rays
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
Chessboard* = ref object
|
|
|
|
## A chessboard
|
|
|
|
|
|
|
|
# The actual board where pieces live
|
|
|
|
grid*: array[64, Piece]
|
|
|
|
# The current position
|
|
|
|
position*: Position
|
|
|
|
# List of all previously reached positions
|
|
|
|
positions*: seq[Position]
|
|
|
|
|
|
|
|
|
|
|
|
# A bunch of simple utility functions and forward declarations
|
|
|
|
proc toFEN*(self: Chessboard): string
|
2024-04-21 15:58:31 +02:00
|
|
|
proc updateChecksAndPins*(self: Chessboard)
|
2024-04-21 11:07:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
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)
|
2024-04-21 15:58:31 +02:00
|
|
|
result.updateChecksAndPins()
|
2024-04-21 11:07:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2024-04-21 15:58:31 +02:00
|
|
|
if (pinningRay and friendlyPieces).countSquares() == 1:
|
2024-04-21 11:07:15 +02:00
|
|
|
self.position.diagonalPins = self.position.diagonalPins or pinningRay
|
|
|
|
|
|
|
|
for piece in canPinOrthogonally:
|
|
|
|
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
|
2024-04-21 15:58:31 +02:00
|
|
|
if (pinningRay and friendlyPieces).countSquares() == 1:
|
|
|
|
self.position.orthogonalPins = self.position.orthogonalPins or pinningRay
|
2024-04-21 11:07:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
func inCheck*(self: Chessboard): bool {.inline.} =
|
|
|
|
## Returns if the current side to move is in check
|
|
|
|
return self.position.checkers != 0
|
|
|
|
|
|
|
|
|
|
|
|
proc canCastle*(self: Chessboard, side: PieceColor): tuple[king, queen: bool] =
|
|
|
|
## Returns if the current side to move can castle
|
|
|
|
return (false, false) # TODO
|
|
|
|
|
|
|
|
|
|
|
|
proc update*(self: Chessboard) =
|
|
|
|
## Updates the internal grid representation
|
|
|
|
## according to the positional data stored
|
|
|
|
## in the chessboard
|
|
|
|
for i in 0..63:
|
|
|
|
self.grid[i] = nullPiece()
|
|
|
|
for sq in self.position.pieces[White][Pawn][]:
|
|
|
|
self.grid[sq] = Piece(color: White, kind: Pawn)
|
|
|
|
for sq in self.position.pieces[Black][Pawn][]:
|
|
|
|
self.grid[sq] = Piece(color: Black, kind: Pawn)
|
|
|
|
for sq in self.position.pieces[White][Bishop][]:
|
|
|
|
self.grid[sq] = Piece(color: White, kind: Bishop)
|
|
|
|
for sq in self.position.pieces[Black][Bishop][]:
|
|
|
|
self.grid[sq] = Piece(color: Black, kind: Bishop)
|
|
|
|
for sq in self.position.pieces[White][Knight][]:
|
|
|
|
self.grid[sq] = Piece(color: White, kind: Knight)
|
|
|
|
for sq in self.position.pieces[Black][Knight][]:
|
|
|
|
self.grid[sq] = Piece(color: Black, kind: Knight)
|
|
|
|
for sq in self.position.pieces[White][Rook][]:
|
|
|
|
self.grid[sq] = Piece(color: White, kind: Rook)
|
|
|
|
for sq in self.position.pieces[Black][Rook][]:
|
|
|
|
self.grid[sq] = Piece(color: Black, kind: Rook)
|
|
|
|
for sq in self.position.pieces[White][Queen][]:
|
|
|
|
self.grid[sq] = Piece(color: White, kind: Queen)
|
|
|
|
for sq in self.position.pieces[Black][Queen][]:
|
|
|
|
self.grid[sq] = Piece(color: Black, kind: Queen)
|
|
|
|
for sq in self.position.pieces[White][King][]:
|
|
|
|
self.grid[sq] = Piece(color: White, kind: King)
|
|
|
|
for sq in self.position.pieces[Black][King][]:
|
|
|
|
self.grid[sq] = Piece(color: Black, kind: King)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc `$`*(self: Chessboard): string =
|
|
|
|
result &= "- - - - - - - -"
|
|
|
|
var file = 8
|
|
|
|
for i in 0..7:
|
|
|
|
result &= "\n"
|
|
|
|
for j in 0..7:
|
|
|
|
let piece = self.grid[makeSquare(i, j)]
|
|
|
|
if piece.kind == Empty:
|
|
|
|
result &= "x "
|
|
|
|
continue
|
|
|
|
result &= &"{piece.toChar()} "
|
|
|
|
result &= &"{file}"
|
|
|
|
dec(file)
|
|
|
|
result &= "\n- - - - - - - -"
|
|
|
|
result &= "\na b c d e f g h"
|
|
|
|
|
|
|
|
|
|
|
|
proc pretty*(self: Chessboard): string =
|
|
|
|
## Returns a colored version of the
|
|
|
|
## board for easier visualization
|
|
|
|
var file = 8
|
|
|
|
for i in 0..7:
|
|
|
|
if i > 0:
|
|
|
|
result &= "\n"
|
|
|
|
for j in 0..7:
|
|
|
|
# Equivalent to (i + j) mod 2
|
|
|
|
# (I'm just evil)
|
|
|
|
if ((i + j) and 1) == 0:
|
|
|
|
result &= "\x1b[39;44;1m"
|
|
|
|
else:
|
|
|
|
result &= "\x1b[39;40;1m"
|
|
|
|
let piece = self.grid[makeSquare(i, j)]
|
|
|
|
if piece.kind == Empty:
|
|
|
|
result &= " \x1b[0m"
|
|
|
|
else:
|
|
|
|
result &= &"{piece.toPretty()} \x1b[0m"
|
|
|
|
result &= &" \x1b[33;1m{file}\x1b[0m"
|
|
|
|
dec(file)
|
|
|
|
|
|
|
|
result &= "\n\x1b[31;1ma b c d e f g h"
|
|
|
|
result &= "\x1b[0m"
|
|
|
|
|
|
|
|
|
|
|
|
proc toFEN*(self: Chessboard): string =
|
|
|
|
## Returns a FEN string of the current
|
|
|
|
## position in the chessboard
|
|
|
|
var skip: int
|
|
|
|
# Piece placement data
|
|
|
|
for i in 0..7:
|
|
|
|
skip = 0
|
|
|
|
for j in 0..7:
|
|
|
|
let piece = self.grid[makeSquare(i, j)]
|
|
|
|
if piece.kind == Empty:
|
|
|
|
inc(skip)
|
|
|
|
elif skip > 0:
|
|
|
|
result &= &"{skip}{piece.toChar()}"
|
|
|
|
skip = 0
|
|
|
|
else:
|
|
|
|
result &= piece.toChar()
|
|
|
|
if skip > 0:
|
|
|
|
result &= $skip
|
|
|
|
if i < 7:
|
|
|
|
result &= "/"
|
|
|
|
result &= " "
|
|
|
|
# Active color
|
|
|
|
result &= (if self.position.sideToMove == White: "w" else: "b")
|
|
|
|
result &= " "
|
|
|
|
# Castling availability
|
|
|
|
let castleWhite = self.position.castlingAvailability.white
|
|
|
|
let castleBlack = self.position.castlingAvailability.black
|
|
|
|
if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen):
|
|
|
|
result &= "-"
|
|
|
|
else:
|
|
|
|
if castleWhite.king:
|
|
|
|
result &= "K"
|
|
|
|
if castleWhite.queen:
|
|
|
|
result &= "Q"
|
|
|
|
if castleBlack.king:
|
|
|
|
result &= "k"
|
|
|
|
if castleBlack.queen:
|
|
|
|
result &= "q"
|
|
|
|
result &= " "
|
|
|
|
# En passant target
|
|
|
|
if self.position.enPassantSquare == nullSquare():
|
|
|
|
result &= "-"
|
|
|
|
else:
|
|
|
|
result &= self.position.enPassantSquare.toAlgebraic()
|
|
|
|
result &= " "
|
|
|
|
# Halfmove clock
|
|
|
|
result &= $self.position.halfMoveClock
|
|
|
|
result &= " "
|
|
|
|
# Fullmove number
|
|
|
|
result &= $self.position.fullMoveCount
|