Refactor chessboard code and move most logic to Position

This commit is contained in:
Mattia Giambirtone 2024-05-01 19:46:28 +02:00
parent 877b6f4b06
commit da5eac94d6
10 changed files with 674 additions and 661 deletions

View File

@ -262,9 +262,9 @@ func shortKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard
# We precompute as much stuff as possible: lookup tables are fast!
func computeKingBitboards: array[64, Bitboard] {.compileTime.} =
func computeKingBitboards: array[Square(0)..Square(63), Bitboard] {.compileTime.} =
## Precomputes all the movement bitboards for the king
for i in 0'u64..63:
for i in Square(0)..Square(63):
let king = i.toBitboard()
# It doesn't really matter which side we generate
# the move for, they're identical for both
@ -284,9 +284,9 @@ func computeKingBitboards: array[64, Bitboard] {.compileTime.} =
result[i] = movements
func computeKnightBitboards: array[64, Bitboard] {.compileTime.} =
func computeKnightBitboards: array[Square(0)..Square(63), Bitboard] {.compileTime.} =
## Precomputes all the movement bitboards for knights
for i in 0'u64..63:
for i in Square(0)..Square(63):
let knight = i.toBitboard()
# It doesn't really matter which side we generate
# the move for, they're identical for both
@ -302,28 +302,19 @@ func computeKnightBitboards: array[64, Bitboard] {.compileTime.} =
result[i] = movements
func computePawnAttacks(color: PieceColor): array[64, Bitboard] {.compileTime.} =
func computePawnAttacks(color: PieceColor): array[Square(0)..Square(63), Bitboard] {.compileTime.} =
## Precomputes all the attack bitboards for pawns
## of the given color
for i in 0'u64..63:
let
pawn = i.toBitboard()
square = Square(i)
file = fileFromSquare(square)
var movements = Bitboard(0)
if file in 1..7:
movements = movements or pawn.forwardLeftRelativeTo(color)
if file in 0..6:
movements = movements or pawn.forwardRightRelativeTo(color)
movements = movements and not pawn
result[i] = movements
for i in Square(0)..Square(63):
let pawn = i.toBitboard()
result[i] = pawn.backwardLeftRelativeTo(color) or pawn.backwardRightRelativeTo(color)
const
KING_BITBOARDS = computeKingBitboards()
KNIGHT_BITBOARDS = computeKnightBitboards()
PAWN_ATTACKS = [computePawnAttacks(White), computePawnAttacks(Black)]
PAWN_ATTACKS: array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), Bitboard]] = [computePawnAttacks(White), computePawnAttacks(Black)]
func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square.int]
func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square.int]
func getPawnAttacks*(color: PieceColor, square: Square): Bitboard {.inline.} = PAWN_ATTACKS[color.int][square.int]
func getKingAttacks*(square: Square): Bitboard {.inline.} = KING_BITBOARDS[square]
func getKnightAttacks*(square: Square): Bitboard {.inline.} = KNIGHT_BITBOARDS[square]
func getPawnAttacks*(color: PieceColor, square: Square): Bitboard {.inline.} = PAWN_ATTACKS[color][square]

View File

@ -13,8 +13,6 @@
# limitations under the License.
## Implementation of a simple chessboard
import std/strformat
import std/strutils
import pieces
@ -41,10 +39,7 @@ type
positions*: seq[Position]
# A bunch of simple utility functions and forward declarations
proc toFEN*(self: Chessboard): string
proc updateChecksAndPins*(self: var Chessboard)
proc hash*(self: var Chessboard)
proc newChessboard*: Chessboard =
@ -54,118 +49,11 @@ proc newChessboard*: Chessboard =
result.position.mailbox[i] = nullPiece()
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.position.mailbox[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).uint16
of 5:
# Fullmove number
var s = ""
while index <= fen.high():
s.add(fen[index])
inc(index)
result.position.fullMoveCount = parseInt(s).uint16
else:
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
inc(index)
result.updateChecksAndPins()
result.hash()
result.position = loadFEN(fen)
proc newDefaultChessboard*: Chessboard {.inline.} =
@ -174,375 +62,29 @@ proc newDefaultChessboard*: Chessboard {.inline.} =
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 getPiece*(self: Chessboard, square: Square): Piece {.inline.} =
## Gets the piece at the given square
return self.position.mailbox[square]
func getPiece*(self: Chessboard, square: string): Piece {.inline.} =
## Gets the piece on the given square
## in algebraic notation
return self.getPiece(square.toSquare())
proc removePieceFromBitboard*(self: var Chessboard, square: Square) =
## Removes a piece at the given square in the chessboard from
## its respective bitboard
let piece = self.getPiece(square)
self.position.pieces[piece.color][piece.kind].clearBit(square)
proc addPieceToBitboard*(self: var 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: var Chessboard, square: Square, piece: Piece) =
## Internal helper to "spawn" a given piece at the given
## square
when not defined(danger):
doAssert self.getPiece(square).kind == Empty
self.addPieceToBitboard(square, piece)
self.position.mailbox[square] = piece
proc removePiece*(self: var Chessboard, square: Square) =
## Removes a piece from the board, updating necessary
## metadata
when not defined(danger):
let piece = self.getPiece(square)
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
self.removePieceFromBitboard(square)
self.position.mailbox[square] = nullPiece()
proc movePiece*(self: var Chessboard, move: Move) =
## Internal helper to move a piece from
## its current square to a target square
let piece = self.getPiece(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 movePiece*(self: var Chessboard, startSquare, targetSquare: Square) =
self.movePiece(createMove(startSquare, targetSquare))
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 getOccupancyFor*(self: Chessboard, color: PieceColor): Bitboard {.inline.} =
## Get the occupancy bitboard for every piece of the given color
result = self.position.getOccupancyFor(color)
func getOccupancy*(self: Chessboard): Bitboard {.inline.} =
## Get the occupancy bitboard for every piece on
## the chessboard
result = self.position.getOccupancy()
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)
squareBB = square.toBitboard()
if (getKingAttacks(square) and squareBB) != 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)
squareBB = square.toBitboard()
result = Bitboard(0)
for knight in knights:
if (getKnightAttacks(knight) and squareBB) != 0:
result = result or knight.toBitboard()
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: var 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() == 1:
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() == 1:
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
return self.position.inCheck()
proc canCastle*(self: Chessboard): tuple[queen, king: bool] =
proc canCastle*(self: Chessboard): tuple[queen, king: bool] {.inline.} =
## Returns if the current side to move can castle
if self.inCheck():
return (false, false)
let
sideToMove = self.position.sideToMove
occupancy = self.getOccupancy()
result = self.position.castlingAvailability[sideToMove]
if result.king:
result.king = (kingSideCastleRay(sideToMove) and occupancy) == 0
if result.queen:
result.queen = (queenSideCastleRay(sideToMove) and occupancy) == 0
if result.king:
# There are no pieces in between our friendly king and
# rook: check for attacks
let
king = self.getBitboard(King, sideToMove).toSquare()
for square in getRayBetween(king, sideToMove.kingSideRook()):
if self.isOccupancyAttacked(square, occupancy):
result.king = false
break
if result.queen:
let
king: Square = self.getBitboard(King, sideToMove).toSquare()
# The king always moves two squares, but the queen side rook moves
# 3 squares. We only need to check for attacks on the squares where
# the king moves to and not any further. We subtract 3 instead of 2
# because getRayBetween ignores the start and target squares in the
# ray it returns so we have to extend it by one
destination = makeSquare(rankFromSquare(king), fileFromSquare(king) - 3)
for square in getRayBetween(king, destination):
if self.isOccupancyAttacked(square, occupancy):
result.queen = false
break
return self.position.canCastle()
proc `$`*(self: Chessboard): string =
result &= "- - - - - - - -"
var file = 8
for i in 0..7:
result &= "\n"
for j in 0..7:
let piece = self.position.mailbox[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 `$`*(self: Chessboard): string = $self.position
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.position.mailbox[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"
## current position for easier visualization
return self.position.pretty()
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.position.mailbox[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
return self.position.toFEN()
proc drawByRepetition*(self: var Chessboard): bool =
@ -559,29 +101,3 @@ proc drawByRepetition*(self: var Chessboard): bool =
return true
dec(i)
proc hash*(self: var Chessboard) =
## Computes the zobrist hash of the current
## position. This only needs to be called when
## a position is loaded the first time, as all
## subsequent hashes are updated incrementally
## at every call to doMove()
self.position.zobristKey = ZobristKey(0)
if self.position.sideToMove == Black:
self.position.zobristKey = self.position.zobristKey xor getBlackToMoveKey()
for sq in self.getOccupancy():
self.position.zobristKey = self.position.zobristKey xor self.getPiece(sq).getKey(sq)
if self.position.castlingAvailability[White].king:
self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(White)
if self.position.castlingAvailability[White].queen:
self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(White)
if self.position.castlingAvailability[Black].king:
self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(Black)
if self.position.castlingAvailability[Black].queen:
self.position.zobristKey = self.position.zobristKey xor getQueenSideCastlingKey(Black)
if self.position.enPassantSquare != nullSquare():
self.position.zobristKey = self.position.zobristKey xor getEnPassantKey(fileFromSquare(self.position.enPassantSquare))

View File

@ -21,7 +21,7 @@ type
# Stolen from https://www.chessprogramming.org/PeSTO's_Evaluation_Function
const
PAWN_MIDDLEGAME_SCORES: array[64, Score] = [
PAWN_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [
0, 0, 0, 0, 0, 0, 0, 0,
98, 134, 61, 95, 68, 126, 34, -11,
-6, 7, 26, 31, 65, 56, 25, -20,
@ -32,7 +32,7 @@ const
0, 0, 0, 0, 0, 0, 0, 0,
]
PAWN_ENDGAME_SCORES: array[64, Score] = [
PAWN_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [
0, 0, 0, 0, 0, 0, 0, 0,
178, 173, 158, 134, 147, 132, 165, 187,
94, 100, 85, 67, 56, 53, 82, 84,
@ -43,7 +43,7 @@ const
0, 0, 0, 0, 0, 0, 0, 0,
]
KNIGHT_MIDDLEGAME_SCORES: array[64, Score] = [
KNIGHT_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [
-167, -89, -34, -49, 61, -97, -15, -107,
-73, -41, 72, 36, 23, 62, 7, -17,
-47, 60, 37, 65, 84, 129, 73, 44,
@ -54,7 +54,7 @@ const
-105, -21, -58, -33, -17, -28, -19, -23,
]
KNIGHT_ENDGAME_SCORES: array[64, Score] = [
KNIGHT_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [
-58, -38, -13, -28, -31, -27, -63, -99,
-25, -8, -25, -2, -9, -25, -24, -52,
-24, -20, 10, 9, -1, -9, -19, -41,
@ -65,7 +65,7 @@ const
-29, -51, -23, -15, -22, -18, -50, -64,
]
BISHOP_MIDDLEGAME_SCORES: array[64, Score] = [
BISHOP_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [
-29, 4, -82, -37, -25, -42, 7, -8,
-26, 16, -18, -13, 30, 59, 18, -47,
-16, 37, 43, 40, 35, 50, 37, -2,
@ -76,7 +76,7 @@ const
-33, -3, -14, -21, -13, -12, -39, -21,
]
BISHOP_ENDGAME_SCORES: array[64, Score] = [
BISHOP_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [
-14, -21, -11, -8, -7, -9, -17, -24,
-8, -4, 7, -12, -3, -13, -4, -14,
2, -8, 0, -1, -2, 6, 0, 4,
@ -87,7 +87,7 @@ const
-23, -9, -23, -5, -9, -16, -5, -17,
]
ROOK_MIDDLEGAME_SCORES: array[64, Score] = [
ROOK_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [
32, 42, 32, 51, 63, 9, 31, 43,
27, 32, 58, 62, 80, 67, 26, 44,
-5, 19, 26, 36, 17, 45, 61, 16,
@ -98,7 +98,7 @@ const
-19, -13, 1, 17, 16, 7, -37, -26,
]
ROOK_ENDGAME_SCORES: array[64, Score] = [
ROOK_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [
13, 10, 18, 15, 12, 12, 8, 5,
11, 13, 13, 11, -3, 3, 8, 3,
7, 7, 7, 5, 4, -3, -5, -3,
@ -109,7 +109,7 @@ const
-9, 2, 3, -1, -5, -13, 4, -20,
]
QUEEN_MIDDLEGAME_SCORES: array[64, Score] = [
QUEEN_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [
-28, 0, 29, 12, 59, 44, 43, 45,
-24, -39, -5, 1, -16, 57, 28, 54,
-13, -17, 7, 8, 29, 56, 47, 57,
@ -120,7 +120,7 @@ const
-1, -18, -9, 10, -15, -25, -31, -50,
]
QUEEN_ENDGAME_SCORES: array[64, Score] = [
QUEEN_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [
-9, 22, 22, 27, 27, 19, 10, 20,
-17, 20, 32, 41, 58, 25, 30, 0,
-20, 6, 9, 49, 47, 35, 19, 9,
@ -131,7 +131,7 @@ const
-33, -28, -22, -43, -5, -32, -20, -41,
]
KING_MIDDLEGAME_SCORES: array[64, Score] = [
KING_MIDDLEGAME_SCORES: array[Square(0)..Square(63), Score] = [
-65, 23, 16, -15, -56, -34, 2, 13,
29, -1, -20, -7, -8, -4, -38, -29,
-9, 24, 2, -16, -20, 6, 22, -22,
@ -142,7 +142,7 @@ const
-15, 36, 12, -54, 8, -28, 24, 14,
]
KING_ENDGAME_SCORES: array[64, Score] = [
KING_ENDGAME_SCORES: array[Square(0)..Square(63), Score] = [
-74, -35, -18, -18, -11, 15, 4, -17,
-12, 17, 14, 17, 17, 38, 23, 11,
10, 17, 23, 15, 20, 45, 44, 13,
@ -154,10 +154,10 @@ const
]
# Bishop, King, Knight, Pawn, Queen, Rook
MIDDLEGAME_WEIGHTS: array[6, Score] = [365, 0, 337, 82, 1025, 477]
ENDGAME_WEIGHTS: array[6, Score] = [297, 0, 281, 94, 936, 512]
MIDDLEGAME_WEIGHTS: array[PieceKind.Bishop..PieceKind.Rook, Score] = [365, 0, 337, 82, 1025, 477]
ENDGAME_WEIGHTS: array[PieceKind.Bishop..PieceKind.Rook, Score] = [297, 0, 281, 94, 936, 512]
MIDDLEGAME_PSQ_TABLES: array[6, array[64, Score]] = [
MIDDLEGAME_PSQ_TABLES: array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]] = [
BISHOP_MIDDLEGAME_SCORES,
KING_MIDDLEGAME_SCORES,
KNIGHT_MIDDLEGAME_SCORES,
@ -166,7 +166,7 @@ const
ROOK_MIDDLEGAME_SCORES
]
ENDGAME_PSQ_TABLES: array[6, array[64, Score]] = [
ENDGAME_PSQ_TABLES: array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]] = [
BISHOP_ENDGAME_SCORES,
KING_ENDGAME_SCORES,
KNIGHT_ENDGAME_SCORES,
@ -180,18 +180,18 @@ const
var
MIDDLEGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]]
ENDGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]]
MIDDLEGAME_VALUE_TABLES: array[PieceColor.White..PieceColor.Black, array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]]]
ENDGAME_VALUE_TABLES: array[PieceColor.White..PieceColor.Black, array[PieceKind.Bishop..PieceKind.Rook, array[Square(0)..Square(63), Score]]]
proc initializeTables =
for kind in [Bishop, King, Knight, Pawn, Queen, Rook]:
for sq in 0..63:
MIDDLEGAME_VALUE_TABLES[White.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq]
ENDGAME_VALUE_TABLES[White.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq]
MIDDLEGAME_VALUE_TABLES[Black.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq xor 56]
ENDGAME_VALUE_TABLES[Black.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq xor 56]
for sq in Square(0)..Square(63):
MIDDLEGAME_VALUE_TABLES[White][kind][sq] = MIDDLEGAME_WEIGHTS[kind] + MIDDLEGAME_PSQ_TABLES[kind][sq]
ENDGAME_VALUE_TABLES[White][kind][sq] = ENDGAME_WEIGHTS[kind] + ENDGAME_PSQ_TABLES[kind][sq]
MIDDLEGAME_VALUE_TABLES[Black][kind][sq] = MIDDLEGAME_WEIGHTS[kind] + MIDDLEGAME_PSQ_TABLES[kind][sq xor 56]
ENDGAME_VALUE_TABLES[Black][kind][sq] = ENDGAME_WEIGHTS[kind] + ENDGAME_PSQ_TABLES[kind][sq xor 56]
initializeTables()
@ -202,12 +202,12 @@ func highestEval*: Score {.inline.} = Score(25_000)
func mateScore*: Score {.inline.} = lowestEval() + 1
func getGamePhase(board: Chessboard): int {.inline.} =
func getGamePhase(position: Position): int {.inline.} =
## Computes the game phase according to
## how many pieces are left on the board
result = 0
for sq in board.getOccupancy():
case board.getPiece(sq).kind:
for sq in position.getOccupancy():
case position.getPiece(sq).kind:
of Bishop, Knight:
inc(result)
of Queen:
@ -221,26 +221,26 @@ func getGamePhase(board: Chessboard): int {.inline.} =
result = min(24, result)
proc getPieceScore*(board: Chessboard, square: Square): Score =
proc getPieceScore*(position: Position, square: Square): Score =
## Returns the value of the piece located at
## the given square
let
piece = board.getPiece(square)
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
middleGamePhase = board.getGamePhase()
piece = position.getPiece(square)
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][square]
endGameScore = ENDGAME_VALUE_TABLES[piece.color][piece.kind][square]
middleGamePhase = position.getGamePhase()
endGamePhase = 24 - middleGamePhase
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
proc getPieceScore*(board: Chessboard, piece: Piece, square: Square): Score =
proc getPieceScore*(position: Position, piece: Piece, square: Square): Score =
## Returns the value the given piece would have if it
## were at the given square
let
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int]
middleGamePhase = board.getGamePhase()
middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][square]
endGameScore = ENDGAME_VALUE_TABLES[piece.color][piece.kind][square]
middleGamePhase = position.getGamePhase()
endGamePhase = 24 - middleGamePhase
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
@ -250,33 +250,33 @@ proc evaluateMaterial(board: ChessBoard): Score =
## Returns a material and position evaluation
## for the current side to move
let
middleGamePhase = board.getGamePhase()
middleGamePhase = board.position.getGamePhase()
endGamePhase = 24 - middleGamePhase
var
# White, Black
middleGameScores: array[2, Score] = [0, 0]
endGameScores: array[2, Score] = [0, 0]
middleGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0]
endGameScores: array[PieceColor.White..PieceColor.Black, Score] = [0, 0]
for sq in board.getOccupancy():
let piece = board.getPiece(sq)
middleGameScores[piece.color.int] += MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int]
endGameScores[piece.color.int] += ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int]
for sq in board.position.getOccupancy():
let piece = board.position.getPiece(sq)
middleGameScores[piece.color] += MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][sq]
endGameScores[piece.color] += ENDGAME_VALUE_TABLES[piece.color][piece.kind][sq]
let
sideToMove = board.position.sideToMove
nonSideToMove = sideToMove.opposite()
middleGameScore = middleGameScores[sideToMove.int] - middleGameScores[nonSideToMove.int]
endGameScore = endGameScores[sideToMove.int] - endGameScores[nonSideToMove.int]
middleGameScore = middleGameScores[sideToMove] - middleGameScores[nonSideToMove]
endGameScore = endGameScores[sideToMove] - endGameScores[nonSideToMove]
result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24)
proc evaluatePawnStructure(board: Chessboard): Score {.used.} =
proc evaluatePawnStructure(position: Position): Score {.used.} =
## Evaluates the pawn structure of the current
## position for the side to move
let
sideToMove = board.position.sideToMove
friendlyPawns = board.getOccupancyFor(sideToMove)
sideToMove = position.sideToMove
friendlyPawns = position.getOccupancyFor(sideToMove)
# Doubled pawns are a bad idea
var doubledPawns = 0
@ -299,9 +299,9 @@ proc evaluatePawnStructure(board: Chessboard): Score {.used.} =
# stronger
var
strongPawnIncrement = Score(0)
for pawn in board.getBitboard(Pawn, White):
if board.getPawnAttacks(pawn, White) != 0:
strongPawnIncrement += board.getPieceScore(pawn) div Score(4)
for pawn in position.getBitboard(Pawn, White):
if position.getPawnAttacks(pawn, White) != 0:
strongPawnIncrement += position.getPieceScore(pawn) div Score(4)
return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns] + strongPawnIncrement

View File

@ -36,10 +36,10 @@ proc generatePawnMoves(self: var Chessboard, moves: var MoveList, destinationMas
let
sideToMove = self.position.sideToMove
nonSideToMove = sideToMove.opposite()
pawns = self.getBitboard(Pawn, sideToMove)
occupancy = self.getOccupancy()
pawns = self.position.getBitboard(Pawn, sideToMove)
occupancy = self.position.getOccupancy()
# We can only capture enemy pieces (except the king)
enemyPieces = self.getOccupancyFor(nonSideToMove)
enemyPieces = self.position.getOccupancyFor(nonSideToMove)
epTarget = self.position.enPassantSquare
diagonalPins = self.position.diagonalPins
orthogonalPins = self.position.orthogonalPins
@ -48,7 +48,7 @@ proc generatePawnMoves(self: var Chessboard, moves: var MoveList, destinationMas
# 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)
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
friendlyKing = self.position.getBitboard(King, sideToMove).toSquare()
# Single and double pushes
@ -146,33 +146,33 @@ proc generatePawnMoves(self: var Chessboard, moves: var MoveList, destinationMas
# king is in check after en passant when it actually isn't
# (see pos fen rnbqkbnr/pppp1ppp/8/2P5/K7/8/PPPP1PPP/RNBQ1BNR b kq - 0 1 moves b7b5 c5b6)
let epPawnSquare = epPawn.toSquare()
let epPiece = self.getPiece(epPawnSquare)
self.removePiece(epPawnSquare)
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
let epPiece = self.position.getPiece(epPawnSquare)
self.position.removePiece(epPawnSquare)
if not self.position.isOccupancyAttacked(friendlyKing, newOccupancy):
# En passant does not create a check on the king: all good
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
self.spawnPiece(epPawnSquare, epPiece)
self.position.spawnPiece(epPawnSquare, epPiece)
if epRight != 0:
# Note that this isn't going to be the same pawn from the previous if block!
let
friendlyPawn = epBitboard.backwardLeftRelativeTo(sideToMove)
newOccupancy = occupancy and not epPawn and not friendlyPawn or epBitboard
let epPawnSquare = epPawn.toSquare()
let epPiece = self.getPiece(epPawnSquare)
self.removePiece(epPawnSquare)
if not self.isOccupancyAttacked(friendlyKing, newOccupancy):
let epPiece = self.position.getPiece(epPawnSquare)
self.position.removePiece(epPawnSquare)
if not self.position.isOccupancyAttacked(friendlyKing, newOccupancy):
# En passant does not create a check on the king: all good
moves.add(createMove(friendlyPawn, epBitboard, EnPassant))
self.spawnPiece(epPawnSquare, epPiece)
self.position.spawnPiece(epPawnSquare, epPiece)
proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
let
sideToMove = self.position.sideToMove
occupancy = self.getOccupancy()
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
rooks = self.getBitboard(Rook, sideToMove)
queens = self.getBitboard(Queen, sideToMove)
occupancy = self.position.getOccupancy()
enemyPieces = self.position.getOccupancyFor(sideToMove.opposite()) and not self.position.getBitboard(King, sideToMove.opposite())
rooks = self.position.getBitboard(Rook, sideToMove)
queens = self.position.getBitboard(Queen, sideToMove)
movableRooks = not self.position.diagonalPins and (queens or rooks)
pinMask = self.position.orthogonalPins
pinnedRooks = movableRooks and pinMask
@ -200,10 +200,10 @@ proc generateRookMoves(self: Chessboard, moves: var MoveList, destinationMask: B
proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
let
sideToMove = self.position.sideToMove
occupancy = self.getOccupancy()
enemyPieces = self.getOccupancyFor(sideToMove.opposite()) and not self.getBitboard(King, sideToMove.opposite())
bishops = self.getBitboard(Bishop, sideToMove)
queens = self.getBitboard(Queen, sideToMove)
occupancy = self.position.getOccupancy()
enemyPieces = self.position.getOccupancyFor(sideToMove.opposite()) and not self.position.getBitboard(King, sideToMove.opposite())
bishops = self.position.getBitboard(Bishop, sideToMove)
queens = self.position.getBitboard(Queen, sideToMove)
movableBishops = not self.position.orthogonalPins and (queens or bishops)
pinMask = self.position.diagonalPins
pinnedBishops = movableBishops and pinMask
@ -229,29 +229,29 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask:
proc generateKingMoves(self: Chessboard, moves: var MoveList, capturesOnly=false) =
let
sideToMove = self.position.sideToMove
king = self.getBitboard(King, sideToMove)
occupancy = self.getOccupancy()
king = self.position.getBitboard(King, sideToMove)
occupancy = self.position.getOccupancy()
nonSideToMove = sideToMove.opposite()
enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove)
enemyPieces = self.position.getOccupancyFor(nonSideToMove) and not self.position.getBitboard(King, nonSideToMove)
bitboard = getKingAttacks(king.toSquare())
noKingOccupancy = occupancy and not king
if not capturesOnly:
for square in bitboard and not occupancy:
if not self.isOccupancyAttacked(square, noKingOccupancy):
if not self.position.isOccupancyAttacked(square, noKingOccupancy):
moves.add(createMove(king, square))
for square in bitboard and enemyPieces:
if not self.isOccupancyAttacked(square, noKingOccupancy):
if not self.position.isOccupancyAttacked(square, noKingOccupancy):
moves.add(createMove(king, square, Capture))
proc generateKnightMoves(self: Chessboard, moves: var MoveList, destinationMask: Bitboard) =
let
sideToMove = self.position.sideToMove
knights = self.getBitboard(Knight, sideToMove)
knights = self.position.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)
enemyPieces = self.position.getOccupancyFor(nonSideToMove) and not self.position.getBitboard(King, nonSideToMove)
for square in unpinnedKnights:
let bitboard = getKnightAttacks(square)
for target in bitboard and destinationMask and not enemyPieces:
@ -264,8 +264,8 @@ proc generateCastling(self: Chessboard, moves: var MoveList) =
let
sideToMove = self.position.sideToMove
castlingRights = self.canCastle()
kingSquare = self.getBitboard(King, sideToMove).toSquare()
kingPiece = self.getPiece(kingSquare)
kingSquare = self.position.getBitboard(King, sideToMove).toSquare()
kingPiece = self.position.getPiece(kingSquare)
if castlingRights.king:
moves.add(createMove(kingSquare, kingPiece.kingSideCastling(), Castle))
if castlingRights.queen:
@ -296,7 +296,7 @@ proc generateMoves*(self: var Chessboard, moves: var MoveList, capturesOnly: boo
var destinationMask: Bitboard
if not self.inCheck():
# Not in check: cannot move over friendly pieces
destinationMask = not self.getOccupancyFor(sideToMove)
destinationMask = not self.position.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
@ -309,8 +309,8 @@ proc generateMoves*(self: var Chessboard, moves: var MoveList, capturesOnly: boo
checker = self.position.checkers.lowestSquare()
checkerBB = checker.toBitboard()
# epTarget = self.position.enPassantSquare
# checkerPiece = self.getPiece(checker)
destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB
# checkerPiece = self.position.getPiece(checker)
destinationMask = getRayBetween(checker, self.position.getBitboard(King, sideToMove).toSquare()) or checkerBB
# TODO: This doesn't really work. I've addressed the issue for now, but it's kinda ugly. Find a better
# solution
# if checkerPiece.kind == Pawn and checkerBB.backwardRelativeTo(checkerPiece.color).toSquare() == epTarget:
@ -322,7 +322,7 @@ proc generateMoves*(self: var Chessboard, moves: var MoveList, capturesOnly: boo
if capturesOnly:
# Note: This does not cover en passant (which is good because it's a capture,
# but the "fix" stands on flimsy ground)
destinationMask = destinationMask and self.getOccupancyFor(nonSideToMove)
destinationMask = destinationMask and self.position.getOccupancyFor(nonSideToMove)
self.generatePawnMoves(moves, destinationMask)
self.generateKnightMoves(moves, destinationMask)
self.generateRookMoves(moves, destinationMask)
@ -340,7 +340,7 @@ proc doMove*(self: var Chessboard, move: Move) =
self.positions.add(self.position)
# Final checks
let piece = self.getPiece(move.startSquare)
let piece = self.position.getPiece(move.startSquare)
when not defined(danger):
doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}"
@ -383,8 +383,8 @@ proc doMove*(self: var Chessboard, move: Move) =
if move.isEnPassant():
# Make the en passant pawn disappear
let epPawnSquare = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare()
self.position.zobristKey = self.position.zobristKey xor self.getPiece(epPawnSquare).getKey(epPawnSquare)
self.removePiece(epPawnSquare)
self.position.zobristKey = self.position.zobristKey xor self.position.getPiece(epPawnSquare).getKey(epPawnSquare)
self.position.removePiece(epPawnSquare)
if move.isCastling() or piece.kind == King:
# If the king has moved, all castling rights for the side to
@ -399,15 +399,15 @@ proc doMove*(self: var Chessboard, move: Move) =
if move.targetSquare == piece.kingSideCastling():
source = piece.color.kingSideRook()
rook = self.getPiece(source)
rook = self.position.getPiece(source)
target = rook.kingSideCastling()
elif move.targetSquare == piece.queenSideCastling():
source = piece.color.queenSideRook()
rook = self.getPiece(source)
rook = self.position.getPiece(source)
target = rook.queenSideCastling()
self.movePiece(source, target)
self.position.movePiece(source, target)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(source)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(target)
@ -421,9 +421,9 @@ proc doMove*(self: var Chessboard, move: Move) =
if move.isCapture():
# Get rid of captured pieces
let captured = self.getPiece(move.targetSquare)
let captured = self.position.getPiece(move.targetSquare)
self.position.zobristKey = self.position.zobristKey xor captured.getKey(move.targetSquare)
self.removePiece(move.targetSquare)
self.position.removePiece(move.targetSquare)
# If a rook has been captured, castling on that side is prohibited
if captured.kind == Rook:
if move.targetSquare == captured.color.kingSideRook():
@ -432,13 +432,13 @@ proc doMove*(self: var Chessboard, move: Move) =
self.position.castlingAvailability[captured.color].queen = false
# Move the piece to its target square
self.movePiece(move)
self.position.movePiece(move)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.startSquare)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare)
if move.isPromotion():
# Move is a pawn promotion: get rid of the pawn
# and spawn a new piece
self.removePiece(move.targetSquare)
self.position.removePiece(move.targetSquare)
self.position.zobristKey = self.position.zobristKey xor piece.getKey(move.targetSquare)
var spawnedPiece: Piece
case move.getPromotionType():
@ -454,9 +454,9 @@ proc doMove*(self: var Chessboard, move: Move) =
# Unreachable
discard
self.position.zobristKey = self.position.zobristKey xor spawnedPiece.getKey(move.targetSquare)
self.spawnPiece(move.targetSquare, spawnedPiece)
self.position.spawnPiece(move.targetSquare, spawnedPiece)
# Updates checks and pins for the (new) side to move
self.updateChecksAndPins()
self.position.updateChecksAndPins()
# Last updates to zobrist key
if self.position.castlingAvailability[piece.color].king:
self.position.zobristKey = self.position.zobristKey xor getKingSideCastlingKey(piece.color)
@ -496,7 +496,7 @@ proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
proc testPieceCount(board: Chessboard, kind: PieceKind, color: PieceColor, count: int) =
let pieces = board.countPieces(kind, color)
let pieces = board.position.countPieces(kind, color)
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
@ -553,45 +553,45 @@ proc basicTests* =
# Pawns
for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]:
testPiece(board.getPiece(loc), Pawn, White)
testPiece(board.position.getPiece(loc), Pawn, White)
for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]:
testPiece(board.getPiece(loc), Pawn, Black)
testPiece(board.position.getPiece(loc), Pawn, Black)
# Rooks
testPiece(board.getPiece("a1"), Rook, White)
testPiece(board.getPiece("h1"), Rook, White)
testPiece(board.getPiece("a8"), Rook, Black)
testPiece(board.getPiece("h8"), Rook, Black)
testPiece(board.position.getPiece("a1"), Rook, White)
testPiece(board.position.getPiece("h1"), Rook, White)
testPiece(board.position.getPiece("a8"), Rook, Black)
testPiece(board.position.getPiece("h8"), Rook, Black)
# Knights
testPiece(board.getPiece("b1"), Knight, White)
testPiece(board.getPiece("g1"), Knight, White)
testPiece(board.getPiece("b8"), Knight, Black)
testPiece(board.getPiece("g8"), Knight, Black)
testPiece(board.position.getPiece("b1"), Knight, White)
testPiece(board.position.getPiece("g1"), Knight, White)
testPiece(board.position.getPiece("b8"), Knight, Black)
testPiece(board.position.getPiece("g8"), Knight, Black)
# Bishops
testPiece(board.getPiece("c1"), Bishop, White)
testPiece(board.getPiece("f1"), Bishop, White)
testPiece(board.getPiece("c8"), Bishop, Black)
testPiece(board.getPiece("f8"), Bishop, Black)
testPiece(board.position.getPiece("c1"), Bishop, White)
testPiece(board.position.getPiece("f1"), Bishop, White)
testPiece(board.position.getPiece("c8"), Bishop, Black)
testPiece(board.position.getPiece("f8"), Bishop, Black)
# Kings
testPiece(board.getPiece("e1"), King, White)
testPiece(board.getPiece("e8"), King, Black)
testPiece(board.position.getPiece("e1"), King, White)
testPiece(board.position.getPiece("e8"), King, Black)
# Queens
testPiece(board.getPiece("d1"), Queen, White)
testPiece(board.getPiece("d8"), Queen, Black)
testPiece(board.position.getPiece("d1"), Queen, White)
testPiece(board.position.getPiece("d8"), Queen, Black)
# Ensure our bitboards match with the board
let
whitePawns = board.getBitboard(Pawn, White)
whiteKnights = board.getBitboard(Knight, White)
whiteBishops = board.getBitboard(Bishop, White)
whiteRooks = board.getBitboard(Rook, White)
whiteQueens = board.getBitboard(Queen, White)
whiteKing = board.getBitboard(King, White)
blackPawns = board.getBitboard(Pawn, Black)
blackKnights = board.getBitboard(Knight, Black)
blackBishops = board.getBitboard(Bishop, Black)
blackRooks = board.getBitboard(Rook, Black)
blackQueens = board.getBitboard(Queen, Black)
blackKing = board.getBitboard(King, Black)
whitePawns = board.position.getBitboard(Pawn, White)
whiteKnights = board.position.getBitboard(Knight, White)
whiteBishops = board.position.getBitboard(Bishop, White)
whiteRooks = board.position.getBitboard(Rook, White)
whiteQueens = board.position.getBitboard(Queen, White)
whiteKing = board.position.getBitboard(King, White)
blackPawns = board.position.getBitboard(Pawn, Black)
blackKnights = board.position.getBitboard(Knight, Black)
blackBishops = board.position.getBitboard(Bishop, Black)
blackRooks = board.position.getBitboard(Rook, Black)
blackQueens = board.position.getBitboard(Queen, Black)
blackKing = board.position.getBitboard(King, Black)
whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)]
whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)]
whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)]

View File

@ -183,7 +183,6 @@ func `$`*(self: Move): string =
result &= ")"
func toAlgebraic*(self: Move): string =
if self == nullMove():
return "null"

View File

@ -51,6 +51,7 @@ func isValid*(a: Square): bool {.inline.} = a.int8 in 0..63
func isLightSquare*(a: Square): bool {.inline.} = (a.int8 and 2) == 0
# Overridden operators for our distinct type
func `xor`*(a: Square, b: SomeInteger): Square {.inline.} = Square(a.int8 xor b)
func `==`*(a, b: Square): bool {.inline.} = a.int8 == b.int8
func `!=`*(a, b: Square): bool {.inline.} = a.int8 != b.int8
func `<`*(a: Square, b: SomeInteger): bool {.inline.} = a.int8 < b.int8

View File

@ -11,10 +11,18 @@
# 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/strformat
import std/strutils
import bitboards
import magics
import pieces
import zobrist
import moves
import rays
export bitboards, magics, pieces, zobrist, moves, rays
type
@ -57,6 +65,11 @@ type
mailbox*: array[Square(0)..Square(63), Piece]
func inCheck*(self: Position): bool {.inline.} =
## Returns if the current side to move is in check
return self.checkers != 0
func getKingStartingSquare*(color: PieceColor): Square {.inline.} =
## Retrieves the starting square of the king
## for the given color
@ -90,3 +103,484 @@ func getOccupancy*(self: Position): Bitboard {.inline.} =
## Get the occupancy bitboard for every piece on
## the chessboard
result = self.getOccupancyFor(Black) or self.getOccupancyFor(White)
func getPawnAttacks*(self: Position, square: Square, attacker: PieceColor): Bitboard {.inline.} =
## Returns the locations of the pawns attacking the given square
return self.getBitboard(Pawn, attacker) and getPawnAttacks(attacker, square)
func getKingAttacks*(self: Position, 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)
squareBB = square.toBitboard()
if (getKingAttacks(square) and squareBB) != 0:
result = result or king
func getKnightAttacks*(self: Position, square: Square, attacker: PieceColor): Bitboard =
## Returns the locations of the knights attacking the given square
let
knights = self.getBitboard(Knight, attacker)
squareBB = square.toBitboard()
result = Bitboard(0)
for knight in knights:
if (getKnightAttacks(knight) and squareBB) != 0:
result = result or knight.toBitboard()
proc getSlidingAttacks*(self: Position, 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: Position, square: Square, attacker: PieceColor): Bitboard =
## Computes the attack bitboard for the given square from
## the given side
result = Bitboard(0) 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: Position, 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, for example, 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
nonSideToMove = self.sideToMove.opposite()
knights = self.getBitboard(Knight, nonSideToMove)
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
if self.getPawnAttacks(square, nonSideToMove) != 0:
return true
proc canCastle*(self: Position): tuple[queen, king: bool] =
## Returns if the current side to move can castle
if self.inCheck():
return (false, false)
let
sideToMove = self.sideToMove
occupancy = self.getOccupancy()
result = self.castlingAvailability[sideToMove]
if result.king:
result.king = (kingSideCastleRay(sideToMove) and occupancy) == 0
if result.queen:
result.queen = (queenSideCastleRay(sideToMove) and occupancy) == 0
if result.king:
# There are no pieces in between our friendly king and
# rook: check for attacks
let
king = self.getBitboard(King, sideToMove).toSquare()
for square in getRayBetween(king, sideToMove.kingSideRook()):
if self.isOccupancyAttacked(square, occupancy):
result.king = false
break
if result.queen:
let
king: Square = self.getBitboard(King, sideToMove).toSquare()
# The king always moves two squares, but the queen side rook moves
# 3 squares. We only need to check for attacks on the squares where
# the king moves to and not any further. We subtract 3 instead of 2
# because getRayBetween ignores the start and target squares in the
# ray it returns so we have to extend it by one
destination = makeSquare(rankFromSquare(king), fileFromSquare(king) - 3)
for square in getRayBetween(king, destination):
if self.isOccupancyAttacked(square, occupancy):
result.queen = false
break
func countPieces*(self: Position, kind: PieceKind, color: PieceColor): int {.inline.} =
## Returns the number of pieces with
## the given color and type in the
## position
return self.pieces[color][kind].countSquares()
func getPiece*(self: Position, square: Square): Piece {.inline.} =
## Gets the piece at the given square in
## the position
return self.mailbox[square]
func getPiece*(self: Position, square: string): Piece {.inline.} =
## Gets the piece on the given square
## in algebraic notation
return self.getPiece(square.toSquare())
proc removePieceFromBitboard*(self: var Position, square: Square) =
## Removes a piece at the given square from
## its respective bitboard
let piece = self.getPiece(square)
self.pieces[piece.color][piece.kind].clearBit(square)
proc addPieceToBitboard*(self: var Position, square: Square, piece: Piece) =
## Adds the given piece at the given square to
## its respective bitboard
self.pieces[piece.color][piece.kind].setBit(square)
proc spawnPiece*(self: var Position, square: Square, piece: Piece) =
## Spawns a new piece at the given square
when not defined(danger):
doAssert self.getPiece(square).kind == Empty
self.addPieceToBitboard(square, piece)
self.mailbox[square] = piece
proc removePiece*(self: var Position, square: Square) =
## Removes a piece from the board, updating necessary
## metadata
when not defined(danger):
let piece = self.getPiece(square)
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
self.removePieceFromBitboard(square)
self.mailbox[square] = nullPiece()
proc movePiece*(self: var Position, move: Move) =
## Internal helper to move a piece from
## its current square to a target square
let piece = self.getPiece(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 movePiece*(self: var Position, startSquare, targetSquare: Square) =
## Moves a piece from the given start square to the given
## target square
self.movePiece(createMove(startSquare, targetSquare))
func countPieces*(self: Position, piece: Piece): int {.inline.} =
## Returns the number of pieces in the position that
## are of the same type and color as the given piece
return self.countPieces(piece.kind, piece.color)
proc updateChecksAndPins*(self: var Position) =
## 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.sideToMove
nonSideToMove = sideToMove.opposite()
friendlyKing = self.getBitboard(King, sideToMove).toSquare()
friendlyPieces = self.getOccupancyFor(sideToMove)
enemyPieces = self.getOccupancyFor(nonSideToMove)
# Update checks
self.checkers = self.getAttacksTo(friendlyKing, nonSideToMove)
# Update pins
self.diagonalPins = Bitboard(0)
self.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() == 1:
self.diagonalPins = self.diagonalPins or pinningRay
for piece in canPinOrthogonally:
let pinningRay = getRayBetween(friendlyKing, piece) or piece.toBitboard()
if (pinningRay and friendlyPieces).countSquares() == 1:
self.orthogonalPins = self.orthogonalPins or pinningRay
proc hash*(self: var Position) =
## Computes the zobrist hash of the position
## This only needs to be called when a position
## is loaded the first time, as all subsequent
## hashes are updated incrementally at every
## call to doMove()
self.zobristKey = ZobristKey(0)
if self.sideToMove == Black:
self.zobristKey = self.zobristKey xor getBlackToMoveKey()
for sq in self.getOccupancy():
self.zobristKey = self.zobristKey xor self.getPiece(sq).getKey(sq)
if self.castlingAvailability[White].king:
self.zobristKey = self.zobristKey xor getKingSideCastlingKey(White)
if self.castlingAvailability[White].queen:
self.zobristKey = self.zobristKey xor getQueenSideCastlingKey(White)
if self.castlingAvailability[Black].king:
self.zobristKey = self.zobristKey xor getKingSideCastlingKey(Black)
if self.castlingAvailability[Black].queen:
self.zobristKey = self.zobristKey xor getQueenSideCastlingKey(Black)
if self.enPassantSquare != nullSquare():
self.zobristKey = self.zobristKey xor getEnPassantKey(fileFromSquare(self.enPassantSquare))
proc loadFEN*(fen: string): Position =
## Initializes a position from the given
## FEN string
result = Position(enPassantSquare: nullSquare())
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
# Make sure the mailbox is actually empty
for sq in Square(0)..Square(63):
result.mailbox[sq] = nullPiece()
# 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.pieces[piece.color][piece.kind].setBit(square)
result.mailbox[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.sideToMove = White
of 'b':
result.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.castlingAvailability[White].king = true
of 'Q':
result.castlingAvailability[White].queen = true
of 'k':
result.castlingAvailability[Black].king = true
of 'q':
result.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.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.halfMoveClock = parseInt(s).uint16
of 5:
# Fullmove number
var s = ""
while index <= fen.high():
s.add(fen[index])
inc(index)
result.fullMoveCount = parseInt(s).uint16
else:
raise newException(ValueError, "invalid FEN: too many fields in FEN string")
inc(index)
result.updateChecksAndPins()
result.hash()
proc `$`*(self: Position): string =
result &= "- - - - - - - -"
var file = 8
for i in 0..7:
result &= "\n"
for j in 0..7:
let piece = self.mailbox[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 toFEN*(self: Position): string =
## Returns a FEN string of the
## position
var skip: int
# Piece placement data
for i in 0..7:
skip = 0
for j in 0..7:
let piece = self.mailbox[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.sideToMove == White: "w" else: "b")
result &= " "
# Castling availability
let castleWhite = self.castlingAvailability[White]
let castleBlack = self.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.enPassantSquare == nullSquare():
result &= "-"
else:
result &= self.enPassantSquare.toAlgebraic()
result &= " "
# Halfmove clock
result &= $self.halfMoveClock
result &= " "
# Fullmove number
result &= $self.fullMoveCount
proc pretty*(self: Position): string =
## Returns a colored version of the
## position 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.mailbox[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"

View File

@ -45,9 +45,10 @@ type
currentExtensionCount: uint8
proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager =
proc newSearchManager*(position: Position, transpositions: TTable): SearchManager =
new(result)
result.board = board
result.board = newChessboard()
result.board.position = position
result.bestMoveRoot = nullMove()
result.transpositionTable = transpositions
@ -79,7 +80,7 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
# second goal we want to use our least valuable pieces to do so (this
# is why we multiply the score of the captured piece by 10, to give
# it priority)
result += 10 * self.board.getPieceScore(move.targetSquare) - self.board.getPieceScore(move.startSquare)
result += 10 * self.board.position.getPieceScore(move.targetSquare) - self.board.position.getPieceScore(move.startSquare)
if move.isPromotion():
# Promotions are a good idea to search first
var piece: Piece
@ -94,12 +95,12 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
piece = Piece(kind: Queen, color: sideToMove)
else:
discard # Unreachable
result += self.board.getPieceScore(piece, move.targetSquare)
if self.board.getPawnAttacks(move.targetSquare, nonSideToMove) != 0:
result += self.board.position.getPieceScore(piece, move.targetSquare)
if self.board.position.getPawnAttacks(move.targetSquare, nonSideToMove) != 0:
# Moving on a square attacked by an enemy pawn is _usually_ a very bad
# idea. Assume the piece is lost and give a malus based on the fact that
# losing a piece this way is dumb
result -= self.board.getPieceScore(move.startSquare) * 2
result -= self.board.position.getPieceScore(move.startSquare) * 2
proc reorderMoves(self: SearchManager, moves: var MoveList) =
@ -152,7 +153,7 @@ proc getSearchExtension(self: SearchManager, move: Move): int {.used.} =
# if we can do other interesting things!
inc(self.currentExtensionCount)
return 1
let piece = self.board.getPiece(move.targetSquare)
let piece = self.board.position.getPiece(move.targetSquare)
# If a pawn has just moved to its second-last rank, extend to
# see if a promotion would yield some good position
if piece.kind == Pawn:

View File

@ -56,7 +56,7 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
echo &"Ply (from root): {board.position.plyFromRoot}"
echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}"
echo &"Turn: {board.position.sideToMove}"
echo &"Piece: {board.getPiece(move.startSquare).kind}"
echo &"Piece: {board.position.getPiece(move.startSquare).kind}"
echo &"Flags: {move.getFlags()}"
echo &"In check: {(if board.inCheck(): \"yes\" else: \"no\")}"
echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}"
@ -137,10 +137,10 @@ proc handleMoveCommand(board: var Chessboard, command: seq[string]): Move {.disc
# we have to figure out all the flags by ourselves (whether it's a double
# push, a capture, a promotion, etc.)
if board.getPiece(targetSquare).kind != Empty:
if board.position.getPiece(targetSquare).kind != Empty:
flags.add(Capture)
if board.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
if board.position.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
flags.add(DoublePush)
if len(moveString) == 5:
@ -160,12 +160,13 @@ proc handleMoveCommand(board: var Chessboard, command: seq[string]): Move {.disc
var move = createMove(startSquare, targetSquare, flags)
let piece = board.getPiece(move.startSquare)
let piece = board.position.getPiece(move.startSquare)
if piece.kind == King and move.startSquare == board.position.sideToMove.getKingStartingSquare():
if move.targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]:
move.flags = move.flags or Castle.uint16
elif move.targetSquare == board.position.enPassantSquare:
move.flags = move.flags or EnPassant.uint16
elif piece.kind == Pawn and targetSquare == board.position.enPassantSquare:
# I hate en passant I hate en passant I hate en passant I hate en passant I hate en passant I hate en passant
flags.add(EnPassant)
result = board.makeMove(move)
if result == nullMove():
echo &"Error: move: {moveString} is illegal"
@ -343,7 +344,8 @@ const HELP_TEXT = """Nimfish help menu:
- fen: Shorthand for "position fen"
- pos <args>: Shorthand for "position <args>"
- get <square>: Get the piece on the given square
- atk <square>: Print which pieces are currently attacking the given square
- atk <square>: Print which opponent pieces are attacking the given square
- def <square>: Print which friendly pieces are attacking the given square
- pins: Print the current pin masks, if any
- checks: Print the current check mask, if in check
- skip: Make a null move (i.e. pass your turn). Useful for debugging. Very much illegal
@ -381,7 +383,7 @@ proc commandLoop*: int =
echo HELP_TEXT
of "skip":
board.position.sideToMove = board.position.sideToMove.opposite()
board.updateChecksAndPins()
board.position.updateChecksAndPins()
of "go":
handleGoCommand(board, cmd)
of "position", "pos":
@ -402,10 +404,19 @@ proc commandLoop*: int =
echo "error: atk: invalid number of arguments"
continue
try:
echo board.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite())
echo board.position.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove.opposite())
except ValueError:
echo "error: atk: invalid square"
continue
of "def":
if len(cmd) != 2:
echo "error: def: invalid number of arguments"
continue
try:
echo board.position.getAttacksTo(cmd[1].toSquare(), board.position.sideToMove)
except ValueError:
echo "error: def: invalid square"
continue
of "ep":
let target = board.position.enPassantSquare
if target != nullSquare():
@ -417,7 +428,7 @@ proc commandLoop*: int =
echo "error: get: invalid number of arguments"
continue
try:
echo board.getPiece(cmd[1])
echo board.position.getPiece(cmd[1])
except ValueError:
echo "error: get: invalid square"
continue

View File

@ -89,10 +89,10 @@ proc parseUCIMove(session: UCISession, move: string): tuple[move: Move, command:
# Since the client tells us just the source and target square of the move,
# we have to figure out all the flags by ourselves (whether it's a double
# push, a capture, a promotion, etc.)
if session.board.getPiece(targetSquare).kind != Empty:
if session.board.position.getPiece(targetSquare).kind != Empty:
flags.add(Capture)
if session.board.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
if session.board.position.getPiece(startSquare).kind == Pawn and abs(rankFromSquare(startSquare) - rankFromSquare(targetSquare)) == 2:
flags.add(DoublePush)
if len(move) == 5:
@ -108,7 +108,7 @@ proc parseUCIMove(session: UCISession, move: string): tuple[move: Move, command:
flags.add(PromoteToRook)
else:
return
let piece = session.board.getPiece(startSquare)
let piece = session.board.position.getPiece(startSquare)
if piece.kind == King and startSquare == session.board.position.sideToMove.getKingStartingSquare():
if targetSquare in [piece.kingSideCastling(), piece.queenSideCastling()]:
flags.add(Castle)
@ -303,7 +303,7 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.}
echo &"info string created {session.hashTableSize} MiB TT"
session.transpositionTable = newTranspositionTable(session.hashTableSize * 1024 * 1024)
var command = args.command
var searcher = newSearchManager(session.board, session.transpositionTable)
var searcher = newSearchManager(session.board.position, session.transpositionTable)
session.currentSearch.store(searcher)
var
timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime)