More cleanup and refactoring, fix minor issue in Position.hash(), unify TUI and UCI (bench 5583594)

This commit is contained in:
2025-10-20 23:18:19 +02:00
parent a6989fd32c
commit a03ca34961
17 changed files with 795 additions and 948 deletions

3
.gitignore vendored
View File

@@ -183,4 +183,5 @@ pretty.json
fcq
*.bullet
nim.cfg
data/
data/
Dockerfile.ppc64le

View File

@@ -13,7 +13,7 @@
# limitations under the License.
import std/[os, math, times, atomics, parseopt, strutils, strformat, options, random]
import heimdall/[uci, tui, moves, board, search, movegen, position, transpositions, eval]
import heimdall/[uci, moves, board, search, movegen, position, transpositions, eval]
import heimdall/util/[magics, limits, tunables, book_augment]
@@ -89,7 +89,7 @@ when isMainModule:
skip = 0
rounds = 1
const subcommands = ["magics", "testonly", "bench", "spsa", "tui", "chonk"]
const subcommands = ["magics", "testonly", "bench", "spsa", "chonk"]
for kind, key, value in parser.getopt():
case kind:
of cmdArgument:
@@ -127,9 +127,6 @@ when isMainModule:
of "spsa":
runUCI = false
getParams = true
of "tui":
runUCI = false
runTUI = true
of "chonk":
# Hehe me make chonky book
augment = true
@@ -188,8 +185,6 @@ when isMainModule:
of cmdEnd:
break
if not magicGen and not augment:
if runTUI:
quit(commandLoop())
if runUCI:
startUCISession()
if bench:

View File

@@ -34,7 +34,6 @@ type
BackwardLeft,
BackwardRight
# Overloaded operators and functions for our bitboard type
func `shl`*(a: Bitboard, x: Natural): Bitboard {.borrow, inline.}
func `shr`*(a: Bitboard, x: Natural): Bitboard {.borrow, inline.}
func `and`*(a, b: Bitboard): Bitboard {.borrow, inline.}
@@ -52,50 +51,34 @@ func `div`*(a: Bitboard, b: SomeUnsignedInt): Bitboard {.borrow, inline.}
func `*`*(a: Bitboard, b: SomeUnsignedInt): Bitboard {.borrow, inline.}
func `*`*(a: SomeUnsignedInt, b: Bitboard): Bitboard {.borrow, inline.}
func `==`*(a, b: Bitboard): bool {.inline, borrow.}
func `==`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 == b.uint64
func `!=`*(a, b: Bitboard): bool {.inline.} = a.uint64 != b.uint64
func `!=`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 != b.uint64
func countSetBits*(a: Bitboard): int {.borrow.}
func countLeadingZeroBits*(a: Bitboard): int {.borrow, inline.}
func countTrailingZeroBits*(a: Bitboard): int {.borrow, inline.}
func countLeadingZeroBits(a: Bitboard): int {.borrow, inline.}
func countTrailingZeroBits(a: Bitboard): int {.borrow, inline.}
func clearBit*(a: var Bitboard, bit: SomeInteger) {.borrow, inline.}
func setBit*(a: var Bitboard, bit: SomeInteger) {.borrow, inline.}
func clearBit*(a: var Bitboard, bit: Square) {.inline.} = a.clearBit(bit.uint8)
func setBit*(a: var Bitboard, bit: Square) {.inline.} = a.setBit(bit.uint8)
# func clearBit*(a: var Bitboard, bit: Square) {.borrow, inline.}
# func setBit*(a: var Bitboard, bit: Square) {.borrow, inline.}
func removed*(a, b: Bitboard): Bitboard {.inline.} = a and not b
func isEmpty*(self: Bitboard): bool {.inline.} = self == Bitboard(0)
func count*(self: Bitboard): int {.inline.} =
## Returns the number of active squares
## in the bitboard
result = self.countSetBits()
func lowestSquare*(self: Bitboard): Square {.inline.} =
## Returns the index of the lowest set bit
## in the given bitboard as a square
result = Square(self.countTrailingZeroBits().uint8)
func highestSquare*(self: Bitboard): Square {.inline.} =
## Returns the index of the highest set bit
## in the given bitboard as a square
result = Square(self.countLeadingZeroBits().uint8 xor 0x3f)
func `==`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 == b.uint64
func `!=`*(a, b: Bitboard): bool {.inline.} = a.uint64 != b.uint64
func `!=`*(a: Bitboard, b: SomeInteger): bool {.inline.} = a.uint64 != b.uint64
func clearBit*(a: var Bitboard, bit: Square) {.inline.} = a.clearBit(bit.uint8)
func setBit*(a: var Bitboard, bit: Square) {.inline.} = a.setBit(bit.uint8)
func removed*(a, b: Bitboard): Bitboard {.inline.} = a and not b
func isEmpty*(self: Bitboard): bool {.inline.} = self == Bitboard(0)
func count*(self: Bitboard): int {.inline.} = self.countSetBits()
func lowestSquare*(self: Bitboard): Square {.inline.} = Square(self.countTrailingZeroBits().uint8)
func highestSquare*(self: Bitboard): Square {.inline.} = Square(self.countLeadingZeroBits().uint8 xor 0x3f)
func fileMask*(file: pieces.File): Bitboard {.inline.} = Bitboard(0x101010101010101'u64) shl file.uint8
func rankMask*(rank: Rank): Bitboard {.inline.} = Bitboard(0xff) shl uint64(8 * rank.uint8)
func toBitboard*(square: SomeInteger): Bitboard {.inline.} = Bitboard(1'u64) shl square
func toBitboard*(square: Square): Bitboard {.inline.} = square.int8.toBitboard()
func toSquare*(b: Bitboard): Square {.inline.} = Square(b.countTrailingZeroBits())
func lowestBit*(self: Bitboard): Bitboard {.inline.} =
{.push overflowChecks:off.}
## Returns the least significant bit of the bitboard
result = self and Bitboard(-cast[int64](self))
{.pop.}
func fileMask*(file: pieces.File): Bitboard {.inline.} = Bitboard(0x101010101010101'u64) shl file.uint8
func rankMask*(rank: Rank): Bitboard {.inline.} = Bitboard(0xff) shl uint64(8 * rank.uint8)
func toBitboard*(square: SomeInteger): Bitboard {.inline.} = Bitboard(1'u64) shl square
func toBitboard*(square: Square): Bitboard {.inline.} = square.int8.toBitboard()
func toSquare*(b: Bitboard): Square {.inline.} = Square(b.countTrailingZeroBits())
func createMove*(startSquare: Bitboard, targetSquare: Square, flag: MoveFlag = Normal): Move {.inline, noinit.} =
result = createMove(startSquare.toSquare(), targetSquare, flag)
@@ -171,29 +154,29 @@ func pretty*(self: Bitboard): string =
func `$`*(self: Bitboard): string {.inline.} = self.pretty()
func generateShifters: array[White..Black, array[Direction, (Bitboard {.noSideEffect.} -> Bitboard)]] {.compileTime.} =
result[White][Forward] = (x: Bitboard) => x shr 8
result[White][Backward] = (x: Bitboard) => x shl 8
result[White][Left] = (x: Bitboard) => x shr 1
result[White][Right] = (x: Bitboard) => x shl 1
result[White][ForwardRight] = (x: Bitboard) => x shr 7
result[White][ForwardLeft] = (x: Bitboard) => x shr 9
result[White][Forward] = (x: Bitboard) => x shr 8
result[White][Backward] = (x: Bitboard) => x shl 8
result[White][Left] = (x: Bitboard) => x shr 1
result[White][Right] = (x: Bitboard) => x shl 1
result[White][ForwardRight] = (x: Bitboard) => x shr 7
result[White][ForwardLeft] = (x: Bitboard) => x shr 9
result[White][BackwardRight] = (x: Bitboard) => x shl 9
result[White][BackwardLeft] = (x: Bitboard) => x shl 7
result[White][BackwardLeft] = (x: Bitboard) => x shl 7
result[Black][Backward] = (x: Bitboard) => x shr 8
result[Black][Forward] = (x: Bitboard) => x shl 8
result[Black][Right] = (x: Bitboard) => x shr 1
result[Black][Left] = (x: Bitboard) => x shl 1
result[Black][BackwardLeft] = (x: Bitboard) => x shr 7
result[Black][Backward] = (x: Bitboard) => x shr 8
result[Black][Forward] = (x: Bitboard) => x shl 8
result[Black][Right] = (x: Bitboard) => x shr 1
result[Black][Left] = (x: Bitboard) => x shl 1
result[Black][BackwardLeft] = (x: Bitboard) => x shr 7
result[Black][BackwardRight] = (x: Bitboard) => x shr 9
result[Black][ForwardLeft] = (x: Bitboard) => x shl 9
result[Black][ForwardRight] = (x: Bitboard) => x shl 7
result[Black][ForwardLeft] = (x: Bitboard) => x shl 9
result[Black][ForwardRight] = (x: Bitboard) => x shl 7
const shifters: array[White..Black, array[Direction, (Bitboard) {.noSideEffect.} -> Bitboard]] = generateShifters()
func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Direction): Bitboard {.inline.} =
func directionMask*(bitboard: Bitboard, color: PieceColor, direction: Direction): Bitboard {.inline.} =
## Get a bitmask relative to the given bitboard
## for the given direction for a piece of the
## given color
@@ -201,69 +184,69 @@ func getDirectionMask*(bitboard: Bitboard, color: PieceColor, direction: Directi
const relativeRanks: array[White..Black, array[Rank, Rank]] = [[Rank(7), Rank(6), Rank(5), Rank(4), Rank(3), Rank(2), Rank(1), Rank(0)], [Rank(0), Rank(1), Rank(2), Rank(3), Rank(4), Rank(5), Rank(6), Rank(7)]]
func getRelativeRank*(color: PieceColor, rank: Rank): Rank {.inline.} = relativeRanks[color][rank]
func relativeRank*(color: PieceColor, rank: Rank): Rank {.inline.} = relativeRanks[color][rank]
const
eighthRanks: array[White..Black, Bitboard] = [rankMask(getRelativeRank(White, Rank(7))), rankMask(getRelativeRank(Black, Rank(7)))]
firstRanks: array[White..Black, Bitboard] = [rankMask(getRelativeRank(White, Rank(0))), rankMask(getRelativeRank(Black, Rank(0)))]
secondRanks: array[White..Black, Bitboard] = [rankMask(getRelativeRank(White, Rank(1))), rankMask(getRelativeRank(Black, Rank(1)))]
seventhRanks: array[White..Black, Bitboard] = [rankMask(getRelativeRank(White, Rank(6))), rankMask(getRelativeRank(Black, Rank(6)))]
eighthRanks: array[White..Black, Bitboard] = [rankMask(relativeRank(White, Rank(7))), rankMask(relativeRank(Black, Rank(7)))]
firstRanks: array[White..Black, Bitboard] = [rankMask(relativeRank(White, Rank(0))), rankMask(relativeRank(Black, Rank(0)))]
secondRanks: array[White..Black, Bitboard] = [rankMask(relativeRank(White, Rank(1))), rankMask(relativeRank(Black, Rank(1)))]
seventhRanks: array[White..Black, Bitboard] = [rankMask(relativeRank(White, Rank(6))), rankMask(relativeRank(Black, Rank(6)))]
leftmostFiles: array[White..Black, Bitboard] = [fileMask(pieces.File(0)), fileMask(pieces.File(7))]
rightmostFiles: array[White..Black, Bitboard] = [fileMask(pieces.File(7)), fileMask(pieces.File(0))]
func getEighthRank*(color: PieceColor): Bitboard {.inline.} = eighthRanks[color]
func getFirstRank*(color: PieceColor): Bitboard {.inline.} = firstRanks[color]
func getSeventhRank*(color: PieceColor): Bitboard {.inline.} = seventhRanks[color]
func getSecondRank*(color: PieceColor): Bitboard {.inline.} = secondRanks[color]
func getLeftmostFile*(color: PieceColor): Bitboard {.inline.}= leftmostFiles[color]
func getRightmostFile*(color: PieceColor): Bitboard {.inline.} = rightmostFiles[color]
func eighthRank*(color: PieceColor): Bitboard {.inline.} = eighthRanks[color]
func firstRank*(color: PieceColor): Bitboard {.inline.} = firstRanks[color]
func seventhRank*(color: PieceColor): Bitboard {.inline.} = seventhRanks[color]
func secondRank*(color: PieceColor): Bitboard {.inline.} = secondRanks[color]
func leftmostFile*(color: PieceColor): Bitboard {.inline.}= leftmostFiles[color]
func rightmostFile*(color: PieceColor): Bitboard {.inline.} = rightmostFiles[color]
func getDirectionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard {.inline.} =
func directionMask*(square: Square, color: PieceColor, direction: Direction): Bitboard {.inline.} =
## Get a bitmask for the given direction for a piece
## of the given color located at the given square
result = getDirectionMask(square.toBitboard(), color, direction)
result = directionMask(square.toBitboard(), color, direction)
func forward*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Forward)
func forward*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = directionMask(self, side, Forward)
func doubleForward*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forward(side).forward(side)
func backward*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Backward)
func backward*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = directionMask(self, side, Backward)
func doubleBackward*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backward(side).backward(side)
func leftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Left) and not getRightmostFile(side)
func rightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = getDirectionMask(self, side, Right) and not getLeftmostFile(side)
func left*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = directionMask(self, side, Left) and not rightmostFile(side)
func right*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = directionMask(self, side, Right) and not leftmostFile(side)
# We mask off the opposite files to make sure there are
# no weird wraparounds when moving at the edges
func forwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, ForwardRight) and not getLeftmostFile(side)
directionMask(self, side, ForwardRight) and not leftmostFile(side)
func forwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, ForwardLeft) and not getRightmostFile(side)
directionMask(self, side, ForwardLeft) and not rightmostFile(side)
func backwardRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, BackwardRight) and not getLeftmostFile(side)
directionMask(self, side, BackwardRight) and not leftmostFile(side)
func backwardLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} =
getDirectionMask(self, side, BackwardLeft) and not getRightmostFile(side)
directionMask(self, side, BackwardLeft) and not rightmostFile(side)
func longKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleForward(side).leftRelativeTo(side)
func longKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleForward(side).rightRelativeTo(side)
func longKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleBackward(side).leftRelativeTo(side)
func longKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleBackward(side).rightRelativeTo(side)
func longKnightUpLeft*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleForward(side).left(side)
func longKnightUpRight*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleForward(side).right(side)
func longKnightDownLeft*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleBackward(side).left(side)
func longKnightDownRight*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.doubleBackward(side).right(side)
func shortKnightUpLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forward(side).leftRelativeTo(side).leftRelativeTo(side)
func shortKnightUpRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forward(side).rightRelativeTo(side).rightRelativeTo(side)
func shortKnightDownLeftRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backward(side).leftRelativeTo(side).leftRelativeTo(side)
func shortKnightDownRightRelativeTo*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backward(side).rightRelativeTo(side).rightRelativeTo(side)
func shortKnightUpLeft*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forward(side).left(side).left(side)
func shortKnightUpRight*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.forward(side).right(side).right(side)
func shortKnightDownLeft*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backward(side).left(side).left(side)
func shortKnightDownRight*(self: Bitboard, side: PieceColor): Bitboard {.inline.} = self.backward(side).right(side).right(side)
# We precompute as much stuff as possible: lookup tables are fast!
@@ -275,13 +258,13 @@ func computeKingBitboards: array[Square.smallest()..Square.biggest(), Bitboard]
# It doesn't really matter which side we generate
# the move for, they're identical for both
var movements = king.forward(White)
movements = movements or king.forwardLeftRelativeTo(White)
movements = movements or king.leftRelativeTo(White)
movements = movements or king.rightRelativeTo(White)
movements = movements or king.backward(White)
movements = movements or king.forwardRightRelativeTo(White)
movements = movements or king.backwardRightRelativeTo(White)
movements = movements or king.backwardLeftRelativeTo(White)
movements = movements or king.forwardLeftRelativeTo(White)
movements = movements or king.left(White)
movements = movements or king.right(White)
movements = movements or king.backward(White)
movements = movements or king.forwardRightRelativeTo(White)
movements = movements or king.backwardRightRelativeTo(White)
movements = movements or king.backwardLeftRelativeTo(White)
# We don't *need* to mask the king off: the engine already masks off
# the board's occupancy when generating moves, but it may be useful for
# other parts of the movegen for this stuff not to say "the king can just
@@ -296,16 +279,16 @@ func computeKnightBitboards: array[Square.smallest()..Square.biggest(), Bitboard
let knight = i.toBitboard()
# It doesn't really matter which side we generate
# the move for, they're identical for both
var movements = knight.longKnightDownLeftRelativeTo(White)
movements = movements or knight.longKnightDownRightRelativeTo(White)
movements = movements or knight.longKnightUpLeftRelativeTo(White)
movements = movements or knight.longKnightUpRightRelativeTo(White)
movements = movements or knight.shortKnightDownLeftRelativeTo(White)
movements = movements or knight.shortKnightDownRightRelativeTo(White)
movements = movements or knight.shortKnightUpLeftRelativeTo(White)
movements = movements or knight.shortKnightUpRightRelativeTo(White)
movements = movements and not knight
result[i] = movements
var movements = knight.longKnightDownLeft(White)
movements = movements or knight.longKnightDownRight(White)
movements = movements or knight.longKnightUpLeft(White)
movements = movements or knight.longKnightUpRight(White)
movements = movements or knight.shortKnightDownLeft(White)
movements = movements or knight.shortKnightDownRight(White)
movements = movements or knight.shortKnightUpLeft(White)
movements = movements or knight.shortKnightUpRight(White)
movements = movements and not knight
result[i] = movements
func computePawnAttackers(color: PieceColor): array[Square.smallest()..Square.biggest(), Bitboard] {.compileTime.} =

View File

@@ -46,9 +46,8 @@ proc newChessboard*(positions: seq[Position]): Chessboard =
func position*(self: Chessboard): lent Position {.inline.} =
## Returns the current position in
## the chessboard *without* copying
## it
## Returns a read-only view into the
## current position on the chessboard
return self.positions[^1]
@@ -113,12 +112,7 @@ proc canCastle*(self: Chessboard): tuple[queen, king: Square] {.inline.} =
proc isInsufficientMaterial*(self: Chessboard): bool {.inline.} =
## Returns whether the current position is drawn
## due to insufficient mating material. Note that
## this is not a strict implementation of the FIDE
## rule about material draws due to the fact that
## it would be basically impossible to implement
## those efficiently
# Note: we only implement a subset of the rules (the cheap ones)
# Break out early if there's more than 4 pieces on the
# board

View File

@@ -13,8 +13,7 @@
# limitations under the License.
## Position evaluation utilities
import heimdall/[board, moves, pieces, position]
import heimdall/nnue/model
import heimdall/[board, moves, pieces, position, nnue]
when defined(simd):
import heimdall/util/simd

View File

@@ -34,12 +34,10 @@ proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask:
epTarget = self.enPassantSquare
diagonalPins = self.diagonalPins
orthogonalPins = self.orthogonalPins
promotionRank = sideToMove.getEighthRank()
startingRank = sideToMove.getSecondRank()
promotionRank = sideToMove.eighthRank()
startingRank = sideToMove.secondRank()
friendlyKing = self.pieces(King, sideToMove).toSquare()
# Single and double pushes
# If a pawn is pinned diagonally, it cannot push forward
let
# If a pawn is pinned horizontally, it cannot move either. It can move vertically
@@ -49,8 +47,8 @@ proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask:
pushablePawns = pawns and not diagonalPins and not horizontalPins
singlePushes = (pushablePawns.forward(sideToMove) and not occupancy) and destinationMask
# We do this weird dance instead of using doubleForward() because that doesn't have any
# way to check if there's pieces on the two squares ahead of the pawn and will just happily les
# us phase through a piece
# way to check if there's pieces on the two squares ahead of the pawn and will just happily
# let us phase through a piece
var canDoublePush = pushablePawns and startingRank
canDoublePush = canDoublePush.forward(sideToMove) and not occupancy
canDoublePush = canDoublePush.forward(sideToMove) and not occupancy and destinationMask
@@ -106,7 +104,6 @@ proc generatePawnMoves(self: var Position, moves: var MoveList, destinationMask:
for promotion in [CapturePromotionBishop, CapturePromotionKnight, CapturePromotionRook, CapturePromotionQueen]:
moves.add(createMove(pawn.toBitboard().backwardLeftRelativeTo(sideToMove), pawn, promotion))
# En passant captures
let epLegality = self.isEPLegal(friendlyKing, epTarget, occupancy, pawns, sideToMove)
if epLegality.left != nullSquare():
moves.add(createMove(epLegality.left, epTarget, EnPassant))
@@ -221,8 +218,8 @@ proc generateMoves*(self: var Position, moves: var MoveList, capturesOnly: bool
nonSideToMove = sideToMove.opposite()
self.generateKingMoves(moves, capturesOnly)
if self.checkers.count() > 1:
# King is in double check: no need to generate any more
# moves
# King is in double check: can only be resolved
# by a king move
return
self.generateCastling(moves)
@@ -259,7 +256,6 @@ proc generateMoves*(self: var Position, moves: var MoveList, capturesOnly: bool
proc generateMoves*(self: Chessboard, moves: var MoveList, capturesOnly=false) {.inline.} =
## The same as Position.generateMoves()
self.positions[^1].generateMoves(moves, capturesOnly)
@@ -282,12 +278,9 @@ proc doMove*(self: Chessboard, move: Move) {.gcsafe.} =
kingSq = self.pieces(King, sideToMove).toSquare()
king = self.on(kingSq)
# Copy previous position
self.positions.add(self.positions[^1].clone())
self.positions.add(self.position.clone())
# 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
self.positions[^1].halfMoveClock = 0
else:
inc(self.positions[^1].halfMoveClock)
@@ -308,8 +301,6 @@ proc doMove*(self: Chessboard, move: Move) {.gcsafe.} =
if previousEPTarget != nullSquare():
self.positions[^1].zobristKey = self.position.zobristKey xor enPassantKey(file(previousEPTarget))
# Update position metadata
if move.isEnPassant():
let epPawnSquare = move.targetSquare.toBitboard().backward(sideToMove).toSquare()
self.positions[^1].remove(epPawnSquare)
@@ -374,7 +365,7 @@ proc doMove*(self: Chessboard, move: Move) {.gcsafe.} =
self.positions[^1].enPassantSquare = nullSquare()
else:
# EP is legal, update zobrist hash
self.positions[^1].zobristKey = self.positions[^1].zobristKey xor enPassantKey(file(self.positions[^1].enPassantSquare))
self.positions[^1].zobristKey = self.position.zobristKey xor enPassantKey(file(self.position.enPassantSquare))
self.positions[^1].updateChecksAndPins()
# Swap the side to move
@@ -413,7 +404,7 @@ proc makeNullMove*(self: Chessboard) {.inline.} =
self.positions[^1].enPassantSquare = nullSquare()
self.positions[^1].fromNull = true
self.positions[^1].updateChecksAndPins()
self.positions[^1].zobristKey = self.positions[^1].zobristKey xor blackToMoveKey()
self.positions[^1].zobristKey = self.position.zobristKey xor blackToMoveKey()
self.positions[^1].halfMoveClock = 0
@@ -447,10 +438,9 @@ proc isStalemate*(self: Chessboard): bool {.inline.} =
proc isDrawn*(self: Chessboard, ply: int): bool {.inline.} =
if self.position.halfMoveClock >= 100:
# Draw by 50 move rule. Note
# that mate always takes priority over
# the 50-move draw, so we need to account
# for that
# Draw by 50 move rule. Note that mate
# always takes priority over the 50-move
# draw, so we need to account for that
return not self.isCheckmate()
if self.isInsufficientMaterial():
@@ -469,20 +459,18 @@ proc isGameOver*(self: Chessboard): bool {.inline.} =
proc unmakeMove*(self: Chessboard) {.inline.} =
if self.positions.len() == 0:
if self.positions.len() == 1:
return
discard self.positions.pop()
## Testing stuff
proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) =
doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead"
proc testPieceCount(board: Chessboard, kind: PieceKind, color: PieceColor, count: int) =
let pieces = board.positions[^1].pieces(kind, color).count()
let pieces = board.pieces(kind, color).count()
doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead"
@@ -510,12 +498,10 @@ const drawnFens = [("4k3/2b5/8/8/8/5B2/8/4K3 w - - 0 1", false), # KBvKB (curr
proc basicTests* =
# Test the FEN parser
for fen in testFens:
let f = fromFEN(fen).toFEN()
doAssert fen == f, &"{fen} != {f}"
# Test zobrist hashing
for fen in testFens:
var
board = newChessboardFromFEN(fen)
@@ -524,17 +510,15 @@ proc basicTests* =
board.generateMoves(moves)
for move in moves:
board.makeMove(move)
let key = board.positions[^1].zobristKey
let key = board.position.zobristKey
board.unmakeMove()
doAssert not hashes.contains(key), &"{fen} has zobrist collisions {move} -> {hashes[key]} (key is {key.uint64})"
hashes[key] = move
# Test detection of (some) draws by insufficient material
for (fen, isDrawn) in drawnFens:
doAssert newChessboardFromFEN(fen).isInsufficientMaterial() == isDrawn, &"draw check failed for {fen} (expected {isDrawn})"
var board = newDefaultChessboard()
# Ensure correct number of pieces
testPieceCount(board, Pawn, White, 8)
testPieceCount(board, Pawn, Black, 8)
testPieceCount(board, Knight, White, 2)
@@ -552,45 +536,44 @@ proc basicTests* =
# Pawns
for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]:
testPiece(board.positions[^1].on(loc), Pawn, White)
testPiece(board.on(loc), Pawn, White)
for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]:
testPiece(board.positions[^1].on(loc), Pawn, Black)
testPiece(board.on(loc), Pawn, Black)
# Rooks
testPiece(board.positions[^1].on("a1"), Rook, White)
testPiece(board.positions[^1].on("h1"), Rook, White)
testPiece(board.positions[^1].on("a8"), Rook, Black)
testPiece(board.positions[^1].on("h8"), Rook, Black)
testPiece(board.on("a1"), Rook, White)
testPiece(board.on("h1"), Rook, White)
testPiece(board.on("a8"), Rook, Black)
testPiece(board.on("h8"), Rook, Black)
# Knights
testPiece(board.positions[^1].on("b1"), Knight, White)
testPiece(board.positions[^1].on("g1"), Knight, White)
testPiece(board.positions[^1].on("b8"), Knight, Black)
testPiece(board.positions[^1].on("g8"), Knight, Black)
testPiece(board.on("b1"), Knight, White)
testPiece(board.on("g1"), Knight, White)
testPiece(board.on("b8"), Knight, Black)
testPiece(board.on("g8"), Knight, Black)
# Bishops
testPiece(board.positions[^1].on("c1"), Bishop, White)
testPiece(board.positions[^1].on("f1"), Bishop, White)
testPiece(board.positions[^1].on("c8"), Bishop, Black)
testPiece(board.positions[^1].on("f8"), Bishop, Black)
testPiece(board.on("c1"), Bishop, White)
testPiece(board.on("f1"), Bishop, White)
testPiece(board.on("c8"), Bishop, Black)
testPiece(board.on("f8"), Bishop, Black)
# Kings
testPiece(board.positions[^1].on("e1"), King, White)
testPiece(board.positions[^1].on("e8"), King, Black)
testPiece(board.on("e1"), King, White)
testPiece(board.on("e8"), King, Black)
# Queens
testPiece(board.positions[^1].on("d1"), Queen, White)
testPiece(board.positions[^1].on("d8"), Queen, Black)
testPiece(board.on("d1"), Queen, White)
testPiece(board.on("d8"), Queen, Black)
# Ensure our bitboards match with the board
let
whitePawns = board.positions[^1].pieces(Pawn, White)
whiteKnights = board.positions[^1].pieces(Knight, White)
whiteBishops = board.positions[^1].pieces(Bishop, White)
whiteRooks = board.positions[^1].pieces(Rook, White)
whiteQueens = board.positions[^1].pieces(Queen, White)
whiteKing = board.positions[^1].pieces(King, White)
blackPawns = board.positions[^1].pieces(Pawn, Black)
blackKnights = board.positions[^1].pieces(Knight, Black)
blackBishops = board.positions[^1].pieces(Bishop, Black)
blackRooks = board.positions[^1].pieces(Rook, Black)
blackQueens = board.positions[^1].pieces(Queen, Black)
blackKing = board.positions[^1].pieces(King, Black)
whitePawns = board.pieces(Pawn, White)
whiteKnights = board.pieces(Knight, White)
whiteBishops = board.pieces(Bishop, White)
whiteRooks = board.pieces(Rook, White)
whiteQueens = board.pieces(Queen, White)
whiteKing = board.pieces(King, White)
blackPawns = board.pieces(Pawn, Black)
blackKnights = board.pieces(Knight, Black)
blackBishops = board.pieces(Bishop, Black)
blackRooks = board.pieces(Rook, Black)
blackQueens = board.pieces(Queen, Black)
blackKing = board.pieces(King, Black)
whitePawnSquares = @[makeSquare(6, 0), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)]
whiteKnightSquares = @[makeSquare(7, 1), makeSquare(7, 6)]
whiteBishopSquares = @[makeSquare(7, 2), makeSquare(7, 5)]
@@ -633,7 +616,7 @@ proc basicTests* =
eval = 100
else:
eval = -100
let game = createMarlinFormatRecord(board.positions[^1], board.sideToMove, eval)
let game = createMarlinFormatRecord(board.position, board.sideToMove, eval)
let rebuilt = game.toMarlinformat().fromMarlinformat()
let newPos = rebuilt.position
# We could just check that game == rebuilt, but this allows a more granular error message

View File

@@ -422,10 +422,11 @@ proc hash*(self: var Position) =
## a position is loaded the first time, as all
## subsequent hashes are updated incrementally
## at every call to doMove()
self.zobristKey = ZobristKey(0)
self.pawnKey = ZobristKey(0)
self.zobristKey = ZobristKey(0)
self.pawnKey = ZobristKey(0)
self.nonpawnKeys = [ZobristKey(0), ZobristKey(0)]
self.majorKey = ZobristKey(0)
self.majorKey = ZobristKey(0)
self.minorKey = ZobristKey(0)
if self.sideToMove == Black:
self.zobristKey = self.zobristKey xor blackToMoveKey()
@@ -710,7 +711,7 @@ proc toFEN*(self: Position): string =
for rank in Rank.all():
skip = 0
for file in File.all():
let piece = self.mailbox[makeSquare(rank, file)]
let piece = self.on(makeSquare(rank, file))
if piece.kind == Empty:
inc(skip)
elif skip > 0:
@@ -780,7 +781,7 @@ proc pretty*(self: Position): string =
result &= "\x1b[39;44;1m"
else:
result &= "\x1b[39;40;1m"
let piece = self.mailbox[makeSquare(rank, file)]
let piece = self.on(makeSquare(rank, file))
if piece.kind == Empty:
result &= " \x1b[0m"
else:

View File

@@ -1193,8 +1193,6 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
self.statistics.nodeCount.atomicInc()
self.board.makeNullMove()
# We perform a shallower search because otherwise there would be no point in
# doing NMP at all!
const
NMP_BASE_REDUCTION = 4
NMP_DEPTH_REDUCTION = 3
@@ -1203,15 +1201,14 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
reduction += min((staticEval - beta) div self.parameters.nmpEvalDivisor, NMP_EVAL_DEPTH_MAX_REDUCTION)
let score = -self.search(depth - reduction, ply + 1, -beta - 1, -beta, isPV=false, root=false, cutNode=not cutNode)
self.board.unmakeMove()
# Note to future self: having shouldStop() checks sprinkled throughout the
# search function makes Heimdall respect the node limit exactly. Do not change
# this
# Note to future self: having shouldStop() at every recursive search call
# makes Heimdall respect the node limit exactly. Do not change this
if self.shouldStop():
return Score(0)
if score >= beta:
const NMP_VERIFICATION_THRESHOLD = 14
# Note: verification search yoinked from Stormphrax
# Note: yoinked from Stormphrax
if depth <= NMP_VERIFICATION_THRESHOLD or self.minNmpPly > 0:
return (if not score.isMateScore(): score else: beta)
@@ -1236,13 +1233,12 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
# seenMoves counts how many moves were yielded by the move picker
playedMoves = 0
seenMoves = 0
# Quiets that failed low
failedQuiets {.noinit.} = newMoveList()
# Quiet moves that failed low
failedQuiets = newMoveList()
# The pieces that moved for each failed
# quiet move in the above list
failedQuietPieces {.noinit.}: array[MAX_MOVES, Piece]
# Captures that failed low
failedCaptures {.noinit.} = newMoveList()
failedCaptures = newMoveList()
for (move, _) in self.pickMoves(hashMove, ply):
when root:
if self.searchMoves.len() > 0 and move notin self.searchMoves:
@@ -1255,7 +1251,7 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
nodesBefore {.used.} = self.statistics.nodeCount.load()
# Ensures we don't prune moves that stave off checkmate
isNotMated {.used.} = not bestScore.isLossScore()
# We make move loop pruning decisions based on the depth that is
# We make move loop pruning decisions based on a depth that is
# closer to the one the move is likely to actually be searched at
lmrDepth {.used.} = depth - LMR_TABLE[depth][seenMoves]
when not isPV:
@@ -1296,9 +1292,8 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
if not isSingularSearch and depth > SE_MIN_DEPTH and expectFailHigh and move == hashMove and ttDepth + SE_DEPTH_OFFSET >= depth:
# Singular extensions. If there is a TT move and we expect the node to fail high, we do a null
# window shallower search (using a new beta derived from the TT score) that excludes the TT move
# to verify whether it is the only good move: if the search fails low, then said move is "singular"
# and it is searched with an increased depth. Singular extensions are disabled when we are already
# in a singular search
# to verify whether it is the only good move: if the search fails low, then said move is "singular",
# and it is searched with an increased depth
const
SE_DEPTH_MULTIPLIER = 1
@@ -1344,8 +1339,6 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
self.stack[ply].reduction = reduction
self.board.doMove(move)
self.statistics.nodeCount.atomicInc()
# Find the best move for us (worst move
# for our opponent, hence the negative sign)
var score: Score
# Prefetch next TT entry: 0 means read, 3 means the value has high temporal locality
# and should be kept in all possible cache levels if possible
@@ -1376,10 +1369,10 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
score = -self.search(depth - 1, ply + 1, -alpha - 1, -alpha, isPV=false, root=false, cutNode=not cutNode)
if seenMoves > 0 and score > alpha and score < beta:
# The position beat alpha (and not beta, which would mean it was too good for us and
# our opponent wouldn't let us play it) in the null window search, search it
# again with the full depth and full window. Note to future self: alpha and beta
# are integers, so in a non-pv node it's never possible that this condition is triggered
# since there's no value between alpha and beta (which is alpha + 1)
# our opponent wouldn't let us play it) in the null window search, search it again
# with the full depth and full window. Note to future self: alpha and beta are integers,
# so in a non-pv node it's never possible that this condition is triggered since there's
# no value between alpha and beta (which is alpha + 1)
score = -self.search(depth - 1, ply + 1, -beta, -alpha, isPV, root=false, cutNode=false)
if self.shouldStop():
self.evalState.undo()
@@ -1400,6 +1393,7 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
elif move.isCapture():
failedCaptures.add(move)
if score > alpha:
# We found a new best move
alpha = score
bestMove = move
when root:
@@ -1426,7 +1420,7 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
self.histories.counterMoves[prevMove.startSquare][prevMove.targetSquare] = move
let histDepth = depth + (bestScore - beta > self.parameters.historyDepthEvalThreshold).int
# If the best move we found is a tactical move, we don't want to punish quiets
# If the best move we found is a tactical move, we don't want to punish quiets,
# because they still might be good (just not as good wrt the best move).
# Very important to note that move == bestMove here!
if move.isQuiet():
@@ -1472,14 +1466,13 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
self.updateCorrectionHistories(sideToMove, depth, bestScore, rawEval, staticEval, beta)
# If the node failed low, we preserve the previous hash move
# If the whole node failed low, we preserve the previous hash move
if bestMove == nullMove():
bestMove = hashMove
# Don't store in the TT during a singular search. We also don't overwrite
# the entry in the TT for the root node to avoid poisoning the original
# score
if not isSingularSearch and (not root or self.statistics.currentVariation.load() == 1) and not self.expired and not self.cancelled():
# Store the best move in the transposition table so we can find it later
let nodeType = if bestScore >= beta: LowerBound elif bestScore <= originalAlpha: UpperBound else: Exact
self.transpositionTable.store(depth.uint8, bestScore.compressScore(ply), self.board.zobristKey, bestMove, nodeType, staticEval.int16, wasPV)
@@ -1487,9 +1480,10 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV,
proc startClock*(self: var SearchManager) =
## Starts the manager's internal clock if
## it wasn't already started. If we're not
## the main thread, this is a no-op
## Starts the manager's internal clock.
## If we're not the main thread, or the
## clock was already started, this is a
## no-op
if not self.state.isMainThread.load() or self.clockStarted:
return
self.state.searchStart.store(getMonoTime())

View File

@@ -1,646 +0,0 @@
# Copyright 2025 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/math
import std/times
import std/strformat
import std/strutils
import heimdall/nnue/model
import heimdall/[eval, uci, movegen]
import heimdall/util/[wdl, scharnagl]
from std/lenientops import `/`
type CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
proc perft*(board: Chessboard, ply: int, verbose = false, divide = false, bulk = false, capturesOnly = false): CountData =
## Counts (and debugs) the number of legal positions reached after
## the given number of ply
if ply == 0:
result.nodes = 1
return
var moves = newMoveList()
board.generateMoves(moves, capturesOnly=capturesOnly)
if not bulk:
if len(moves) == 0 and board.inCheck():
result.checkmates = 1
# TODO: Should we count stalemates/draws?
if ply == 0:
result.nodes = 1
return
elif ply == 1 and bulk:
if divide:
for move in moves:
echo &"{move.toUCI()}: 1"
if verbose:
echo ""
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
for move in moves:
if verbose:
let canCastle = board.canCastle()
echo &"Move: {move.startSquare.toUCI()}{move.targetSquare.toUCI()}"
echo &"Turn: {board.sideToMove}"
echo &"Piece: {board.position.on(move.startSquare).kind}"
echo &"Flag: {move.flag()}"
echo &"In check: {(if board.inCheck(): \"yes\" else: \"no\")}"
echo &"Castling targets:\n - King side: {(if canCastle.king != nullSquare(): canCastle.king.toUCI() else: \"None\")}\n - Queen side: {(if canCastle.queen != nullSquare(): canCastle.queen.toUCI() else: \"None\")}"
echo &"Position before move: {board.toFEN()}"
echo &"Hash: {board.zobristKey}"
stdout.write("En Passant target: ")
if board.position.enPassantSquare != nullSquare():
echo board.position.enPassantSquare.toUCI()
else:
echo "None"
echo "\n", board.pretty()
board.doMove(move)
when not defined(danger):
let incHash = board.zobristKey
let pawnKey = board.pawnKey
board.positions[^1].hash()
doAssert board.zobristKey == incHash, &"{board.zobristKey} != {incHash} at {move} ({board.positions[^2].toFEN()})"
doAssert board.pawnKey == pawnKey, &"{board.pawnKey} != {pawnKey} at {move} ({board.positions[^2].toFEN()})"
if ply == 1:
if move.isCapture():
inc(result.captures)
if move.isCastling():
inc(result.castles)
if move.isPromotion():
inc(result.promotions)
if move.isEnPassant():
inc(result.enPassant)
if board.inCheck():
# Opponent king is in check
inc(result.checks)
if verbose:
let canCastle = board.canCastle()
echo "\n"
echo &"Opponent in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
echo &"Opponent castling targets:\n - King side: {(if canCastle.king != nullSquare(): canCastle.king.toUCI() else: \"None\")}\n - Queen side: {(if canCastle.queen != nullSquare(): canCastle.queen.toUCI() else: \"None\")}"
echo &"Position after move: {board.toFEN()}"
echo "\n", board.pretty()
stdout.write("nextpos>> ")
try:
discard readLine(stdin)
except IOError:
discard
except EOFError:
discard
let next = board.perft(ply - 1, verbose, bulk=bulk)
board.unmakeMove()
if divide and (not bulk or ply > 1):
echo &"{move.toUCI()}: {next.nodes}"
if verbose:
echo ""
result.nodes += next.nodes
result.captures += next.captures
result.checks += next.checks
result.promotions += next.promotions
result.castles += next.castles
result.enPassant += next.enPassant
result.checkmates += next.checkmates
proc handleMoveCommand(board: Chessboard, state: EvalState, command: seq[string]): Move {.discardable.} =
if len(command) != 2:
echo &"Error: move: invalid number of arguments"
return
let moveString = command[1]
if len(moveString) notin 4..5:
echo &"Error: move: invalid move syntax"
return
var
startSquare: Square
targetSquare: Square
flag = Normal
try:
startSquare = moveString[0..1].toSquare()
except ValueError:
echo &"Error: move: invalid start square ({moveString[0..1]})"
return
try:
targetSquare = moveString[2..3].toSquare()
except ValueError:
echo &"Error: move: invalid target square ({moveString[2..3]})"
return
# Since the user 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 board.on(startSquare).kind == Pawn and absDistance(rank(startSquare), rank(targetSquare)) == 2:
flag = DoublePush
if len(moveString) == 5:
# Promotion
case moveString[4]:
of 'b':
flag = PromotionBishop
of 'n':
flag = PromotionKnight
of 'q':
flag = PromotionQueen
of 'r':
flag = PromotionRook
else:
echo &"Error: move: invalid promotion piece '{moveString[4]}'"
return
let piece = board.on(startSquare)
if board.on(targetSquare).color == piece.color.opposite():
case flag:
of PromotionBishop:
flag = CapturePromotionBishop
of PromotionKnight:
flag = CapturePromotionKnight
of PromotionRook:
flag = CapturePromotionRook
of PromotionQueen:
flag = CapturePromotionQueen
else:
flag = Capture
let canCastle = board.canCastle()
if piece.kind == King:
if startSquare in ["e1".toSquare(), "e8".toSquare()]:
# Support for standard castling notation
case targetSquare:
of "c1".toSquare(), "c8".toSquare():
flag = LongCastling
targetSquare = canCastle.queen
of "g1".toSquare(), "g8".toSquare():
flag = ShortCastling
targetSquare = canCastle.king
else:
if targetSquare == canCastle.king:
flag = ShortCastling
elif targetSquare == canCastle.queen:
flag = LongCastling
elif targetSquare == canCastle.king:
flag = ShortCastling
elif targetSquare == canCastle.queen:
flag = LongCastling
let move = createMove(startSquare, targetSquare, flag)
if board.isLegal(move):
let kingSq = board.pieces(King, board.sideToMove).toSquare()
state.update(move, board.sideToMove, board.on(move.startSquare).kind, board.on(move.targetSquare).kind, kingSq)
board.doMove(move)
return move
else:
echo &"Error: move: {moveString} is illegal"
proc handleGoCommand(board: Chessboard, command: seq[string]) =
if len(command) < 2:
echo &"Error: go: invalid number of arguments"
return
case command[1]:
of "perft":
if len(command) == 2:
echo &"Error: go: perft: invalid number of arguments"
return
var
args = command[2].splitWhitespace()
bulk = false
verbose = false
captures = false
divide = true
if args.len() > 1:
var ok = true
for arg in args[1..^1]:
case arg:
of "bulk":
bulk = true
of "verbose":
verbose = true
of "captures":
captures = true
of "nosplit":
divide = false
else:
echo &"Error: go: {command[1]}: invalid argument '{args[1]}'"
ok = false
break
if not ok:
return
try:
let ply = parseInt(args[0])
if bulk:
let t = cpuTime()
let nodes = board.perft(ply, divide=divide, bulk=true, verbose=verbose, capturesOnly=captures).nodes
let tot = cpuTime() - t
if divide:
echo ""
echo &"Nodes searched (bulk-counting: on): {nodes}"
echo &"Time taken: {tot:.3f} seconds\nNodes per second: {round(nodes / tot).uint64}"
else:
let t = cpuTime()
let data = board.perft(ply, divide=divide, verbose=verbose, capturesOnly=captures)
let tot = cpuTime() - t
if divide:
echo ""
echo &"Nodes searched (bulk-counting: off): {data.nodes}"
echo &" - Captures: {data.captures}"
echo &" - Checks: {data.checks}"
echo &" - E.P: {data.enPassant}"
echo &" - Checkmates: {data.checkmates}"
echo &" - Castles: {data.castles}"
echo &" - Promotions: {data.promotions}"
echo ""
echo &"Time taken: {tot:.3f} seconds\nNodes per second: {round(data.nodes / tot).uint64}"
except ValueError:
echo &"error: go: {command[1]}: invalid depth"
else:
echo &"error: go: unknown subcommand '{command[1]}'"
proc handlePositionCommand(board: var Chessboard, state: EvalState, command: seq[string]) =
if len(command) < 2:
echo "Error: position: invalid number of arguments"
return
# Makes sure we don't leave the board in an invalid state if
# some error occurs
var tempBoard: Chessboard
case command[1]:
of "startpos", "kiwipete":
if command[1] == "kiwipete":
tempBoard = newChessboardFromFen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -")
else:
tempBoard = newDefaultChessboard()
if command.len() > 2:
let args = command[2].splitWhitespace()
if args.len() > 0:
var i = 0
while i < args.len():
case args[i]:
of "moves":
var j = i + 1
while j < args.len():
if handleMoveCommand(tempBoard, state, @["move", args[j]]) == nullMove():
return
inc(j)
inc(i)
board = tempBoard
state.init(board)
of "frc":
let args = command[2].splitWhitespace()
if len(args) != 1:
echo &"error: position: frc: invalid number of arguments"
return
try:
let scharnaglNumber = args[0].parseInt()
if scharnaglNumber notin 0..959:
echo &"error: position: frc: scharnagl number must be 0 <= 0 < 960"
return
handlePositionCommand(board, state, @["position", "fen", scharnaglNumber.scharnaglToFEN()])
except ValueError:
echo &"error: position: frc: invalid scharnagl number"
return
of "dfrc":
let args = command[2].splitWhitespace()
if len(args) != 2:
echo &"error: position: dfrc: invalid number of arguments"
return
try:
let whiteScharnaglNumber = args[0].parseInt()
let blackScharnaglNumber = args[1].parseInt()
if whiteScharnaglNumber notin 0..959 or blackScharnaglNumber notin 0..959:
echo &"error: position: dfrc: scharnagl number must be 0 <= n < 960"
return
handlePositionCommand(board, state, @["position", "fen", scharnaglToFEN(whiteScharnaglNumber, blackScharnaglNumber)])
except ValueError:
echo &"error: position: dfrc: invalid scharnagl number"
return
of "fen":
if len(command) == 2:
echo &"Current position: {board.toFEN()}"
return
var
args = command[2].splitWhitespace()
fenString = ""
stop = 0
for i, arg in args:
if arg in ["moves", ]:
break
if i > 0:
fenString &= " "
fenString &= arg
inc(stop)
args = args[stop..^1]
try:
tempBoard = newChessboardFromFEN(fenString)
except ValueError:
echo &"error: position: {getCurrentExceptionMsg()}"
return
if args.len() > 0:
var i = 0
while i < args.len():
case args[i]:
of "moves":
var j = i + 1
while j < args.len():
if handleMoveCommand(tempBoard, state, @["move", args[j]]) == nullMove():
return
inc(j)
inc(i)
board = tempBoard
state.init(board)
of "print":
echo board
of "pretty":
echo board.pretty()
else:
echo &"error: position: unknown subcommand '{command[1]}'"
return
const HELP_TEXT = """heimdall help menu:
- go: Begin a search. Currently does not implement UCI search features (simply
switch to UCI mode for that)
Subcommands:
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
print the results
Options:
- bulk: Enable bulk-counting (significantly faster, gives less statistics)
- verbose: Enable move debugging (for each and every move, not recommended on large searches)
- captures: Only generate capture moves
- nosplit: Do not print the number of legal moves after each root move
Example: go perft 5 bulk
- position: Get/set board position
Subcommands:
- fen [string]: Set the board to the given fen string if one is provided, or print
the current position as a FEN string if no arguments are given
- startpos: Set the board to the starting position
- frc <number>: Set the board to the given Chess960 (aka Fischer Random Chess) position
- dfrc <whiteNum> <blackNum>: Set a double fischer random chess position with the given white and black
Chess960 positions
- kiwipete: Set the board to the famous kiwipete position
- pretty: Pretty-print the current position
- print: Print the current position using ASCII characters only
Options:
- moves {moveList}: Perform the given moves in UCI notation
after the position is loaded. This option only applies to the
subcommands that set a position, it is ignored otherwise
Examples:
- position startpos
- position fen ... moves a2a3 a7a6
- clear: Clear the screen
- move <move>: Perform the given move in UCI notation
- castle: Print castling rights for the side to move
- check: Print if the current side to move is in check
- unmove, u: Unmakes the last move. Can be used in succession
- stm: Print which side is to move
- ep: Print the current en passant target
- pretty: Shorthand for "position pretty"
- print: Shorthand for "position print"
- fen: Shorthand for "position fen"
- pos <args>: Shorthand for "position <args>"
- get <square>: Get the piece on 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). If ran in succession, undoes the null move.
- uci: enter UCI mode
- quit: exit
- zobrist: Print the zobrist hash for the current position
- pkey: Print the pawn-only zobrist hash for the current position
- eval: Evaluate the current position
- rep: Show whether this position is a draw by repetition
- status: Print the status of the game
- threats: Print the current threats by the opponent, if there are any
- ibucket: Print the current king input bucket
- obucket: Print the current output bucket
- mat: Print the sum of material currently on the board
- verbatim <path>: Dumps the built-in network to the specified path, straight from the binary
- network: Prints the name of the network embedded into the engine
"""
proc commandLoop*: int =
## Heimdall's terminal user interface
echo "Heimdall TUI by nocturn9x (see LICENSE)"
var
board = newDefaultChessboard()
state = newEvalState()
startUCI = false
state.init(board)
while true:
var
cmd: seq[string]
cmdStr: string
try:
stdout.write(">>> ")
stdout.flushFile()
cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '})
if cmdStr.len() == 0:
continue
cmd = cmdStr.splitWhitespace(maxsplit=2)
case cmd[0]:
of "uci":
if len(cmd) != 1:
echo "error: uci: invalid number of arguments"
continue
startUCI = true
break
of "clear":
if len(cmd) != 1:
echo "error: clear: invalid number of arguments"
continue
echo "\x1Bc"
of "help":
if len(cmd) != 1:
echo "error: help: invalid number of arguments"
continue
echo HELP_TEXT
of "skip":
if len(cmd) != 1:
echo "error: uci: invalid number of arguments"
continue
if board.position.fromNull:
board.unmakeMove()
else:
board.makeNullMove()
of "verbatim":
if len(cmd) != 2:
echo "error: verbatim: invalid number of arguments"
dumpVerbatimNet(cmd[1], network)
of "go":
handleGoCommand(board, cmd)
of "position", "pos":
handlePositionCommand(board, state, cmd)
of "move":
handleMoveCommand(board, state, cmd)
of "pretty", "print", "fen":
handlePositionCommand(board, state, @["position", cmd[0]])
of "unmove", "u":
if len(cmd) != 1:
echo &"error: {cmd[0]}: invalid number of arguments"
continue
if board.positions.len() == 1:
echo "No previous move to undo"
else:
state.undo()
board.unmakeMove()
of "stm":
if len(cmd) != 1:
echo "error: stm: invalid number of arguments"
continue
echo &"Side to move: {board.sideToMove}"
of "atk":
if len(cmd) != 2:
echo "error: atk: invalid number of arguments"
continue
try:
echo board.position.attackers(cmd[1].toSquare(), board.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.attackers(cmd[1].toSquare(), board.sideToMove)
except ValueError:
echo "error: def: invalid square"
continue
of "ep":
if len(cmd) != 1:
echo "error: ep: invalid number of arguments"
continue
let target = board.position.enPassantSquare
if target != nullSquare():
echo &"En passant target: {target.toUCI()}"
else:
echo "En passant target: None"
of "get":
if len(cmd) != 2:
echo "error: get: invalid number of arguments"
continue
try:
echo board.position.on(cmd[1])
except ValueError:
echo "error: get: invalid square"
continue
of "castle":
let castleRights = board.position.castlingAvailability[board.sideToMove]
let canCastle = board.canCastle()
echo &"Castling targets for {($board.sideToMove).toLowerAscii()}:\n - King side: {(if castleRights.king != nullSquare(): castleRights.king.toUCI() else: \"None\")}\n - Queen side: {(if castleRights.queen != nullSquare(): castleRights.queen.toUCI() else: \"None\")}"
echo &"{($board.sideToMove)} can currently castle:\n - King side: {(if canCastle.king != nullSquare(): \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen != nullSquare(): \"yes\" else: \"no\")}"
of "check":
echo &"{board.sideToMove} king in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
of "pins":
if len(cmd) != 1:
echo "error: pins: invalid number of arguments"
continue
if not board.position.orthogonalPins.isEmpty():
echo &"Orthogonal pins:\n{board.position.orthogonalPins}"
if not board.position.diagonalPins.isEmpty():
echo &"Diagonal pins:\n{board.position.diagonalPins}"
of "checks":
if len(cmd) != 1:
echo "error: checks: invalid number of arguments"
continue
if not board.position.checkers.isEmpty():
echo board.position.checkers
of "quit":
if len(cmd) != 1:
echo "error: quit: invalid number of arguments"
continue
return 0
of "zobrist":
if len(cmd) != 1:
echo "error: zobrist: invalid number of arguments"
continue
echo &"Current Zobrist key: 0x{board.zobristKey.uint64.toHex().toLowerAscii()} ({board.zobristKey})"
of "pkey":
if len(cmd) != 1:
echo "error: pkey: invalid number of arguments"
continue
echo &"Current Pawn key: 0x{board.pawnKey.uint64.toHex().toLowerAscii()} ({board.pawnKey})"
of "rep":
if len(cmd) != 1:
echo "error: rep: invalid number of arguments"
continue
echo "Position is drawn by repetition: ", if board.drawnByRepetition(0): "yes" else: "no"
of "eval":
if len(cmd) != 1:
echo "error: eval: invalid number of arguments"
continue
let rawEval = board.evaluate(state)
echo &"Raw eval: {rawEval} engine units"
echo &"Normalized eval: {rawEval.normalizeScore(board.material())} cp"
of "status":
if len(cmd) != 1:
echo "error: status: invalid number of arguments"
continue
if board.isStalemate():
echo "Draw by stalemate"
elif board.drawnByRepetition(0):
echo "Draw by repetition"
elif board.isDrawn(0):
echo "Draw"
elif board.isCheckmate():
echo &"{board.sideToMove.opposite()} wins by checkmate"
else:
echo "Game is not over"
of "threats":
if len(cmd) != 1:
echo "error: threats: invalid number of arguments"
continue
if not board.position.threats.isEmpty():
echo board.position.threats
of "ibucket":
if len(cmd) != 1:
echo "error: ibucket: invalid number of arguments"
continue
let kingSq = board.pieces(King, board.sideToMove).toSquare()
echo &"Current king input bucket for {board.sideToMove}: {kingBucket(board.sideToMove, kingSq)}"
of "obucket":
if len(cmd) != 1:
echo "error: obucket: invalid number of arguments"
continue
const divisor = 32 div NUM_OUTPUT_BUCKETS
let outputBucket = (board.pieces().count() - 2) div divisor
echo &"Current output bucket: {outputBucket}"
of "mat":
if len(cmd) != 1:
echo "error: mat: invalid number of arguments"
continue
echo &"Material: {board.material()}"
of "network":
if len(cmd) != 1:
echo "error: network: invalid number of arguments"
continue
echo &"ID of the built-in network: {NET_ID}"
else:
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
except IOError:
echo ""
return 0
except EOFError:
echo ""
return 0
if startUCI:
startUCISession()

View File

@@ -13,23 +13,23 @@
# limitations under the License.
## Implementation of a UCI compatible server
import std/[os, random, atomics, options, terminal, strutils, strformat]
import heimdall/[board, search, movegen, transpositions, pieces as pcs]
import heimdall/util/[limits, tunables, aligned]
import heimdall/[board, search, movegen, transpositions, pieces as pcs, eval, nnue]
import heimdall/util/[perft, limits, tunables, aligned, scharnagl, help, wdl]
import std/[os, math, times, random, atomics, options, terminal, strutils, strformat, sequtils]
from std/lenientops import `/`
randomize()
type
UCISession = ref object
## A UCI session
# Print verbose logs for every action
debug: bool
board: Chessboard
# Information about the current search
searcher: SearchManager
# Size of the transposition table (in megabytes, and not the retarded kind!)
# Size of the transposition table (in mebibytes, aka the only sensible unit.)
hashTableSize: uint64
# Number of (extra) workers to use during search alongside
# the main search thread. This is always Threads - 1
@@ -41,11 +41,10 @@ type
variations: int
# The move overhead
overhead: int
# Can we ponder?
# Are we alloved to ponder when go ponder is sent?
canPonder: bool
# Do we print minimal logs? (only final depth)
minimal: bool
# Are we in datagen mode?
datagenMode: bool
# Should we interpret the nodes from go nodes
# as a soft bound instead of a hard bound? Only
@@ -62,10 +61,51 @@ type
# The upper bound for the soft node limit when using soft node
# limit randomization (defaults to the lower bound if not set)
softNodeRandomLimit: int
# Used to avoid blocking forever when sending wait after a
# go infinite command
isInfiniteSearch: bool
# Are we in mixed mode?
isMixedMode: bool
BareUCICommand = enum
Icu = "icu"
Wait = "wait"
Barbecue = "Dont"
Clear = "clear"
NullMove = "nullMove"
SideToMove = "stm"
EnPassant = "epTarget"
Repeated = "repeated"
PrintFEN = "fen"
PrintASCII = "print"
PrettyPrint = "pretty"
CastlingRights = "castle"
ZobristKey = "zobrist"
PawnKey = "pkey"
MinorKey = "minKey"
MajorKey = "majKey"
NonpawnKeys = "npKeys"
GameStatus = "status"
Threats = "threats"
InCheck = "inCheck"
Checkers = "checkers"
UnmakeMove = "unmove"
StaticEval = "eval"
Material = "material"
InputBucket = "ibucket"
OutputBucket = "obucket"
PrintNetName = "network"
PinnedPieces = "pins"
SimpleUCICommand = enum
Help = "help"
Attackers = "atk"
Defenders = "def"
GetPiece = "on"
MakeMove = "move"
DumpNet = "verbatim"
UCICommandType = enum
## A UCI command type enumeration
Unknown,
IsReady,
NewGame,
@@ -77,13 +117,12 @@ type
Stop,
PonderHit,
Uci,
# Revert to pretty-print
Icu,
Wait,
Barbecue
## Custom commands after here
Bare, # Bare commands take no arguments
Simple, # Simple commands take only one argument
UCICommand = object
## A UCI command
case kind: UCICommandType
of Debug:
on: bool
@@ -108,17 +147,26 @@ type
searchmoves: seq[Move]
ponder: bool
mate: Option[int]
# Custom bits
perft: Option[tuple[depth: int, verbose, capturesOnly, divide, bulk: bool]]
of Simple:
simpleCmd: SimpleUCICommand
arg: string
of Bare:
bareCmd: BareUCICommand
else:
discard
WorkerAction = enum
Search, Exit
WorkerCommand = object
case kind: WorkerAction
of Search:
command: UCICommand
else:
discard
WorkerResponse = enum
Exiting, SearchComplete
@@ -128,9 +176,6 @@ type
proc parseUCIMove(session: UCISession, position: Position, move: string): tuple[move: Move, command: UCICommand] =
## Parses a UCI move string into a move
## object, ensuring it is legal for the
## current position
var
startSquare: Square
targetSquare: Square
@@ -210,52 +255,84 @@ proc parseUCIMove(session: UCISession, position: Position, move: string): tuple[
proc handleUCIMove(session: UCISession, board: Chessboard, moveStr: string): tuple[move: Move, cmd: UCICommand] {.discardable.} =
## Attempts to parse a move and performs it on the
## chessboard if it is legal
if session.debug:
echo &"info string making move {moveStr}"
let
r = session.parseUCIMove(board.position, moveStr)
move = r.move
command = r.command
if move == nullMove():
return (move, command)
let r = session.parseUCIMove(board.position, moveStr)
result.move = r.move
result.cmd = r.command
if result.move == nullMove():
return
else:
if session.debug:
echo &"info string {moveStr} parses to {move}"
result.move = board.makeMove(move)
echo &"info string {moveStr} parses to {r.move}"
board.doMove(r.move)
proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand =
## Handles the "go" UCI command
result = UCICommand(kind: Go)
var current = 1 # Skip the "go"
while current < command.len():
let flag = command[current]
let subcommand = command[current]
inc(current)
case flag:
case subcommand:
of "infinite":
result.infinite = true
of "ponder":
result.ponder = true
of "wtime":
result.wtime = some(command[current].parseInt())
try:
result.wtime = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "btime":
result.btime = some(command[current].parseInt())
try:
result.btime = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "winc":
result.winc = some(command[current].parseInt())
try:
result.winc = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "binc":
result.binc = some(command[current].parseInt())
try:
result.binc = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "movestogo":
result.movesToGo = some(command[current].parseInt())
try:
result.movesToGo = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "depth":
result.depth = some(command[current].parseInt())
try:
result.depth = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "movetime":
result.moveTime = some(command[current].parseInt())
try:
result.moveTime = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "nodes":
result.nodes = some(command[current].parseBiggestUInt().uint64)
try:
result.nodes = some(command[current].parseBiggestUInt().uint64)
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "mate":
result.mate = some(command[current].parseInt())
try:
result.mate = some(command[current].parseInt())
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand}' subcommand")
of "searchmoves":
while current < command.len():
if command[current] == "":
@@ -265,22 +342,69 @@ proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand =
return UCICommand(kind: Unknown, reason: &"invalid move '{command[current]}' for searchmoves")
result.searchmoves.add(move)
inc(current)
of "perft":
if current >= command.len():
return UCICommand(kind: Unknown, reason: "missing depth argument for '{subcommand}'")
var depth: int
try:
depth = command[current].parseInt()
inc(current)
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer '{command[current]}' for '{subcommand} depth'")
var tup = (depth: depth, verbose: false, capturesOnly: false, divide: true, bulk: false)
while current < command.len():
if command[current] == "":
break
case command[current]:
of "bulk":
tup.bulk = true
of "verbose":
tup.verbose = true
of "captures":
tup.capturesOnly = true
of "nosplit":
tup.divide = false
else:
return UCICommand(kind: Unknown, reason: &"unknown option '{command[current]}' for '{subcommand}' subcommand")
inc(current)
result.perft = some(tup)
else:
discard
return UCICommand(kind: Unknown, reason: &"unknown subcommand '{command[current - 1]}' for 'go'")
let
isLimitedSearch = anyIt([result.wtime, result.btime, result.winc, result.binc, result.movesToGo, result.depth, result.moveTime, result.mate], it.isSome()) or result.nodes.isSome()
isPerftSearch = result.perft.isSome()
if result.infinite:
if result.ponder:
return UCICommand(kind: Unknown, reason: "'go infinite' does not make sense with the 'ponder' option")
if isLimitedSearch:
return UCICommand(kind: Unknown, reason: "'go infinite' does not make sense with other search limits")
if isPerftSearch:
# Note: go perft <stuff> and go <limits> are already mutually exclusive because one
# will be parsed as a subcommand of the other and will cause a parse error
return UCICommand(kind: Unknown, reason: "'go infinite' and 'go perft' are mutually exclusive")
if not isLimitedSearch and not isPerftSearch:
# A bare 'go' is interpreted as 'go infinite'
result.infinite = true
proc handleUCIPositionCommand(session: var UCISession, command: seq[string]): UCICommand =
## Handles the "position" UCI command
result = UCICommand(kind: Position)
# Makes sure we don't leave the board in an invalid state if
# some error occurs
var chessboard: Chessboard
if command[1] notin ["startpos", "kiwipete"] and len(command) < 3:
return UCICommand(kind: Unknown, reason: &"missing FEN/scharnagl number for 'position {command[1]}' command")
var args = command[2..^1]
case command[1]:
of "startpos", "fen":
var args = command[2..^1]
of "startpos", "fen", "kiwipete":
if command[1] == "startpos":
result.fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
elif command[1] == "kiwipete":
result.fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -"
else:
var fenString = ""
var stop = 0
@@ -309,35 +433,100 @@ proc handleUCIPositionCommand(session: var UCISession, command: seq[string]): UC
result.moves.add(args[j])
inc(j)
inc(i)
of "frc":
if len(args) < 1:
return UCICommand(kind: Unknown, reason: "missing scharnagl number for 'position frc' command")
if len(args) > 1:
return UCICommand(kind: Unknown, reason: "too many arguments for 'position frc' command")
try:
let scharnaglNumber = args[0].parseInt()
if scharnaglNumber notin 0..959:
return UCICommand(kind: Unknown, reason: &"scharnagl number must be 0 <= n < 960")
result = session.handleUCIPositionCommand(@["position", "fen", scharnaglNumber.scharnaglToFEN()])
if not session.searcher.state.chess960.load():
if session.debug:
echo "info automatically enabling Chess960 support"
session.searcher.state.chess960.store(true)
return
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer for 'position frc' command")
of "dfrc":
if len(args) < 1:
return UCICommand(kind: Unknown, reason: "missing white scharnagl number for 'position dfrc' command")
if len(args) > 2:
return UCICommand(kind: Unknown, reason: "too many arguments for 'position dfrc' command")
try:
var whiteScharnaglNumber: int
var blackScharnaglNumber: int
if len(args) == 2:
whiteScharnaglNumber = args[0].parseInt()
blackScharnaglNumber = args[1].parseInt()
if whiteScharnaglNumber notin 0..959 or blackScharnaglNumber notin 0..959:
return UCICommand(kind: Unknown, reason: &"scharnagl numbers must be 0 <= n < 960")
else:
let n = args[0].parseInt()
if n >= 960 * 960:
return UCICommand(kind: Unknown, reason: &"scharnagl index must be 0 <= n < 921600")
whiteScharnaglNumber = n mod 960
blackScharnaglNumber = n div 960
result = session.handleUCIPositionCommand(@["position", "fen", scharnaglToFEN(whiteScharnaglNumber, blackScharnaglNumber)])
if not session.searcher.state.chess960.load():
if session.debug:
echo "info automatically enabling Chess960 support"
session.searcher.state.chess960.store(true)
return
except ValueError:
return UCICommand(kind: Unknown, reason: &"invalid integer for 'position dfrc' command")
else:
return UCICommand(kind: Unknown, reason: &"unknown subcomponent '{command[1]}'")
return UCICommand(kind: Unknown, reason: &"unknown subcomponent '{command[1]}' for 'position' command")
let
sideToMove = chessboard.sideToMove
attackers = chessboard.position.attackers(chessboard.position.pieces(King, sideToMove.opposite()).toSquare(), sideToMove)
if not attackers.isEmpty():
return UCICommand(kind: Unknown, reason: "opponent must not be in check")
return UCICommand(kind: Unknown, reason: "position is illegal: opponent must not be in check")
session.board.positions.setLen(0)
for position in chessboard.positions:
session.board.positions.add(position.clone())
proc parseUCICommand(session: var UCISession, command: string): UCICommand =
## Attempts to parse the given UCI command
var cmd = command.replace("\t", "").splitWhitespace()
result = UCICommand(kind: Unknown)
var current = 0
while current < cmd.len():
# Try bare commands first, then simple commands, then standard UCI commands.
# We call toLowerAscii because parseEnum does style-insensitive comparisons
# and they bother me greatly
try:
let bareCmd = parseEnum[BareUCICommand](cmd[current].toLowerAscii())
inc(current)
if current != cmd.len() and bareCmd != Barbecue:
return UCICommand(kind: Unknown, reason: &"too many arguments for '{cmd[current - 1]}' command")
if bareCmd != Barbecue:
# The easter egg is another special case which requires
# more validation
return UCICommand(kind: Bare, bareCmd: bareCmd)
except ValueError:
try:
let simpleCmd = parseEnum[SimpleUCICommand](cmd[current].toLowerAscii())
inc(current)
# Help is the only special command taking in an optional argument
if current >= cmd.len() and simpleCmd != Help:
return UCICommand(kind: Unknown, reason: &"insufficient arguments for '{cmd[current - 1]}' command")
return UCICommand(kind: Simple, simpleCmd: simpleCmd, arg: "")
except ValueError:
discard
case cmd[current]:
of "isready":
return UCICommand(kind: IsReady)
of "uci":
return UCICommand(kind: Uci)
of "icu":
return UCICommand(kind: Icu)
of "wait":
return UCICommand(kind: Wait)
of "stop":
return UCICommand(kind: Stop)
of "help":
# TODO: Help with submenus
return UCICommand(kind: Simple, arg: "")
of "ucinewgame":
return UCICommand(kind: NewGame)
of "quit":
@@ -383,7 +572,7 @@ proc parseUCICommand(session: var UCISession, command: string): UCICommand =
inc(i)
inc(current)
if i == words.len():
return UCICommand(kind: Barbecue)
return UCICommand(kind: Bare, bareCmd: Barbecue)
else:
# Unknown UCI commands should be ignored. Attempt
# to make sense of the input regardless
@@ -595,6 +784,7 @@ proc searchWorkerLoop(self: UCISearchWorker) {.thread.} =
echo &"bestmove {line[0].toUCI()}"
if self.session.debug:
echo "info string worker has finished searching"
self.session.isInfiniteSearch = false
self.sendResponse(SearchComplete)
@@ -606,17 +796,18 @@ proc startUCISession* =
var
cmd: UCICommand
cmdStr: string
session = UCISession(hashTableSize: 64, board: newDefaultChessboard(), variations: 1, overhead: 250)
session = UCISession(hashTableSize: 64, board: newDefaultChessboard(), variations: 1, overhead: 250, isMixedMode: true)
transpositionTable = allocHeapAligned(TTable, 64)
parameters = getDefaultParameters()
searchWorker = session.createSearchWorker()
searchWorkerThread: Thread[UCISearchWorker]
evalState = newEvalState()
# Start search worker
createThread(searchWorkerThread, searchWorkerLoop, searchWorker)
transpositionTable[] = newTranspositionTable(session.hashTableSize * 1024 * 1024)
transpositionTable.init(1)
session.searcher = newSearchManager(session.board.positions, transpositionTable, parameters)
session.searcher = newSearchManager(session.board.positions, transpositionTable, parameters, evalState=evalState)
if not isatty(stdout) or getEnv("NO_COLOR").len() != 0:
session.searcher.setUCIMode(true)
@@ -634,9 +825,9 @@ proc startUCISession* =
echo &"info string received command '{cmdStr}' -> {cmd}"
if cmd.kind == Unknown:
if cmd.reason.len() > 0:
stderr.writeLine(&"info string received unknown or invalid command '{cmdStr}' -> {cmd.reason}")
stderr.writeLine(&"info string error: received unknown or invalid command '{cmdStr}' -> {cmd.reason}")
else:
stderr.writeLine(&"info string received unknown or invalid command '{cmdStr}'")
stderr.writeLine(&"info string error: received unknown or invalid command '{cmdStr}'")
continue
case cmd.kind:
of Uci:
@@ -666,9 +857,132 @@ proc startUCISession* =
echo &"option name {param.name} type spin default {param.default} min {param.min} max {param.max}"
echo "uciok"
session.searcher.setUCIMode(true)
of Icu:
echo "koicu"
session.searcher.setUCIMode(false)
session.isMixedMode = false
of Simple:
if not session.isMixedMode:
echo "info string this command is disabled while in UCI mode, send icu to revert to mixed mode"
continue
case cmd.simpleCmd:
of Help:
# TODO: Handle submenus, colored output, etc.
echo HELP_TEXT
of Attackers:
try:
echo &"Enemy pieces attacking the given square:\n{session.board.position.attackers(cmd.arg.toSquare(), session.board.sideToMove.opposite())}"
except ValueError:
stderr.writeLine("error: invalid square")
continue
of Defenders:
try:
echo &"Friendly pieces defending the given square:\n{session.board.position.attackers(cmd.arg.toSquare(), session.board.sideToMove)}"
except ValueError:
stderr.writeLine("error: invalid square")
continue
of GetPiece:
try:
echo session.board.position.on(cmd.arg)
except ValueError:
stderr.writeLine("error: invalid square")
continue
of MakeMove:
discard
of DumpNet:
echo &"Dumping built-in network {NET_ID} to '{cmd.arg}'"
dumpVerbatimNet(cmd.arg, network)
of Bare:
if not session.isMixedMode and cmd.bareCmd notin [Wait, Icu, Barbecue]:
echo "info string this command is disabled while in UCI mode, send icu to revert to mixed mode"
continue
case cmd.bareCmd:
of Icu:
echo "koicu"
session.isMixedMode = true
session.searcher.setUCIMode(false)
of Wait:
if session.isInfiniteSearch:
stderr.writeLine("info string error: cannot wait for infinite search")
continue
if session.searcher.isSearching():
searchWorker.waitFor(SearchComplete)
of Barbecue:
echo "info string just tell me the date and time..."
of Clear:
echo "\x1Bc"
of NullMove:
if session.board.position.fromNull:
session.board.unmakeMove()
else:
session.board.makeNullMove()
of SideToMove:
echo &"Side to move: {session.board.sideToMove}"
of EnPassant:
let target = session.board.position.enPassantSquare
if target != nullSquare():
echo &"En passant target: {target.toUCI()}"
else:
echo "En passant target: None"
of CastlingRights:
let castleRights = session.board.position.castlingAvailability[session.board.sideToMove]
let canCastle = session.board.canCastle()
echo &"Castling targets for {($session.board.sideToMove).toLowerAscii()}:\n - King side: {(if castleRights.king != nullSquare(): castleRights.king.toUCI() else: \"None\")}\n - Queen side: {(if castleRights.queen != nullSquare(): castleRights.queen.toUCI() else: \"None\")}"
echo &"{($session.board.sideToMove)} can currently castle:\n - King side: {(if canCastle.king != nullSquare(): \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen != nullSquare(): \"yes\" else: \"no\")}"
of InCheck:
echo &"{session.board.sideToMove} king in check: {(if session.board.inCheck(): \"yes\" else: \"no\")}"
of Checkers:
echo &"Pieces checking the {($session.board.sideToMove).toLowerAscii()} king:\n{session.board.position.checkers}"
of UnmakeMove:
session.board.unmakeMove()
of Repeated:
echo "Position is drawn by repetition: ", if session.board.drawnByRepetition(0): "yes" else: "no"
of StaticEval:
let rawEval = session.board.evaluate(evalState)
echo &"Raw eval: {rawEval} engine units"
echo &"Normalized eval: {rawEval.normalizeScore(session.board.material())} cp"
of PrintFEN:
echo &"FEN of the current position: {session.board.position.toFEN()}"
of PrintASCII:
echo $session.board
of PrettyPrint:
echo session.board.pretty()
of ZobristKey:
echo &"Current Zobrist key: 0x{session.board.zobristKey.uint64.toHex().toLowerAscii()} ({session.board.zobristKey})"
of PawnKey:
echo &"Current pawn Zobrist key: 0x{session.board.pawnKey.uint64.toHex().toLowerAscii()} ({session.board.pawnKey})"
of MajorKey:
echo &"Current major piece Zobrist key: 0x{session.board.majorKey.uint64.toHex().toLowerAscii()} ({session.board.majorKey})"
of MinorKey:
echo &"Current minor piece Zobrist key: 0x{session.board.minorKey.uint64.toHex().toLowerAscii()} ({session.board.minorKey})"
of NonpawnKeys:
echo &"Current nonpawn piece Zobrist key for white: 0x{session.board.nonpawnKey(White).uint64.toHex().toLowerAscii()} ({session.board.nonpawnKey(White)})"
echo &"Current nonpawn piece Zobrist key for black: 0x{session.board.nonpawnKey(Black).uint64.toHex().toLowerAscii()} ({session.board.nonpawnKey(Black)})"
of GameStatus:
stdout.write("Current game status: ")
if session.board.isStalemate():
echo "drawn by stalemate"
elif session.board.drawnByRepetition(0):
echo "drawn by repetition"
elif session.board.isDrawn(0):
echo "drawn"
elif session.board.isCheckmate():
echo &"{session.board.sideToMove.opposite()} wins by checkmate"
else:
echo "in progress"
of Threats:
echo &"Squares threathened by the opponent in the current position:\n{session.board.position.threats}"
of Material:
echo &"Material currently on the board: {session.board.material()} points"
of InputBucket:
let kingSq = session.board.pieces(King, session.board.sideToMove).toSquare()
echo &"Current king input bucket for {session.board.sideToMove}: {kingBucket(session.board.sideToMove, kingSq)}"
of OutputBucket:
const divisor = 32 div NUM_OUTPUT_BUCKETS
let outputBucket = (session.board.pieces().count() - 2) div divisor
echo &"Current output bucket: {outputBucket}"
of PrintNetName:
echo &"ID of the built-in network: {NET_ID}"
of PinnedPieces:
echo &"Orthogonal pins:\n{session.board.position.orthogonalPins}"
echo &"Diagonal pins:\n{session.board.position.diagonalPins}"
of Quit:
if session.searcher.isSearching():
session.searcher.stop()
@@ -692,7 +1006,7 @@ proc startUCISession* =
session.debug = cmd.on
of NewGame:
if session.searcher.isSearching():
stderr.writeLine("info string cannot start a new game while searching")
stderr.writeLine("info string error: cannot start a new game while searching")
continue
if session.debug:
echo &"info string clearing out TT of size {session.hashTableSize} MiB"
@@ -710,25 +1024,49 @@ proc startUCISession* =
if session.debug:
echo "info string switched to normal search"
of Go:
if session.searcher.isSearching():
# Search already running. Let's teach the user a lesson
session.searcher.stop()
searchWorker.waitFor(SearchComplete)
echo "info string premium membership is required to send go during search. Please check out https://n9x.co/heimdall-premium for details"
continue
if session.board.isGameOver():
stderr.writeLine("info string position is in terminal state (checkmate or draw)")
echo "bestmove 0000"
continue
# Start the clock as soon as possible to account
# for startup delays in our time management
session.searcher.startClock()
searchWorker.channels.receive.send(WorkerCommand(kind: Search, command: cmd))
if session.debug:
echo "info string search started"
of Wait:
if session.searcher.isSearching():
searchWorker.waitFor(SearchComplete)
if cmd.perft.isSome():
let perftInfo = cmd.perft.get()
if perftInfo.bulk:
let t = cpuTime()
let nodes = session.board.perft(perftInfo.depth, divide=perftInfo.divide, bulk=true, verbose=perftInfo.verbose, capturesOnly=perftInfo.capturesOnly).nodes
let tot = cpuTime() - t
if perftInfo.divide:
echo ""
echo &"Nodes searched (bulk-counting: on): {nodes}"
echo &"Time taken: {tot:.3f} seconds\nNodes per second: {round(nodes / tot).uint64}"
else:
let t = cpuTime()
let data = session.board.perft(perftInfo.depth, divide=perftInfo.divide, bulk=false, verbose=perftInfo.verbose, capturesOnly=perftInfo.capturesOnly)
let tot = cpuTime() - t
if perftInfo.divide:
echo ""
echo &"Nodes searched (bulk-counting: off): {data.nodes}"
echo &" - Captures: {data.captures}"
echo &" - Checks: {data.checks}"
echo &" - E.P: {data.enPassant}"
echo &" - Checkmates: {data.checkmates}"
echo &" - Castles: {data.castles}"
echo &" - Promotions: {data.promotions}"
echo ""
echo &"Time taken: {tot:.3f} seconds\nNodes per second: {round(data.nodes / tot).uint64}"
else:
session.isInfiniteSearch = cmd.infinite
if session.searcher.isSearching():
# Search already running. Let's teach the user a lesson
session.searcher.stop()
searchWorker.waitFor(SearchComplete)
echo "info string premium membership is required to send go during search. Please check out https://n9x.co/heimdall-premium for details"
continue
if session.board.isGameOver():
stderr.writeLine("info string position is in terminal state (checkmate or draw)")
echo "bestmove 0000"
continue
# Start the clock as soon as possible to account
# for startup delays in our time management
session.searcher.startClock()
searchWorker.channels.receive.send(WorkerCommand(kind: Search, command: cmd))
if session.debug:
echo "info string search started"
of Stop:
if session.searcher.isSearching():
session.searcher.stop()
@@ -872,8 +1210,6 @@ proc startUCISession* =
# session.history and they will be set as the searcher's
# board state once search starts
discard
of Barbecue:
echo "info string just tell me the date and time..."
else:
discard
except IOError:

View File

@@ -0,0 +1,94 @@
# Copyright 2025 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.
# TODO: Better help menu (colored) and with help <cmd> functionality.
# Right now we just use the old help menu from the TUI
const HELP_TEXT* = """heimdall help menu:
Note: currently this only lists additional commands that
are not part of the UCI specification or UCI commands that
have additional meaning on top of their standard behavior
Heimdall is a UCI engine, but by default it starts in "mixed" mode,
meaning it will accept both standard UCI commands and a set of custom
extensions listed below. When switching to UCI mode, all custom extensions
are disabled (unless explicitly stated otherwise)
- go : Begin a search.
Subcommands:
- perft <depth> [options]: Run the performance test at the given depth (in ply) and
print the results
Options:
- bulk : Enable bulk-counting (significantly faster, gives less statistics)
- verbose : Enable move debugging (for each and every move, not recommended on large searches)
- captures: Only generate capture moves
- nosplit : Do not print the number of legal moves after each root move
Example: go perft 5 bulk -> Run the performance test at depth 5 in bulk-counting mode
- position : Get/set board position
Subcommands:
- fen [string]: Set the board to the given fen string
- startpos: Set the board to the starting position
- frc <number>: Set the board to the given Chess960 (aka Fischer Random Chess) position
- dfrc <whiteNum> <blackNum>: Set a double fischer random chess position with the given
white and black Chess960 positions
- dfrc <number>: Set a double fischer random chess position using a single index in the
format blackNum*960+whiteNum. For example 'position dfrc 308283' is equivalent to
'position dfrc 123 321'. Mostly meant for automation purposes
- kiwipete: Set the board to the famous kiwipete position
Options:
- moves {moveList}: Perform the given moves in UCI notation
after the position is loaded. This option only applies to the
subcommands that set a position, it is ignored otherwise
Examples:
- position startpos
- position fen <fen> moves a2a3 a7a6
Note: the frc and dfrc subcommands automatically set UCI_Chess960 to true
- clear : Clear the screen
- move <move> : Make the given move on the board (expects UCI notation, e.g. e2e4)
- castle : Print castling rights for the side to move
- inCheck : Print if the current side to move is in check
- unmove : Unmakes the last move, if there is one. Can be used in succession
- stm : Print which side is to move
- epTarget : Print the current en passant target
- pretty : Print a colored, Unicode chessboard representing the current position
- print : Like pretty, but uses ASCII only and no colors
- fen : Print the FEN of the current position
- pos <args> : Shorthand for "position <args>"
- on <square> : Get the piece on the given square
- atk <square> : Print which opponent pieces are attacking the given square
- def <square> : Print which friendly pieces are defending the given square
- pins : Print the current pin masks
- checkers : Print the current check mask
- nullMove : Make a "null move" (i.e. pass your turn). If ran after a null move was made, it is reverted
- zobrist : Print the zobrist key for the current position
- pkey : Print the pawn zobrist key for the current position
- minkey : Print the minor piece zobrist key for the current position
- majKey : Print the major piece zobrist key for the current position
- npKeys : Print the nonpawn zobrist keys for the current position
- eval : Print the static evaluation of the current position
- repeated : Print whether this position is drawn by repetition
- status : Print the status of the game
- threats : Print the current threats by the opponent
- ibucket : Print the current king input bucket
- obucket : Print the current output bucket
- material : Print the sum of material (using 1, 3, 3, 5, 9 as values) currently on the board
- verbatim <path>: Dumps the built-in network to the specified path, straight from the binary
- network : Prints the name of the network embedded into the engine
- uci : Switches from mixed mode to UCI mode
- icu : The opposite of the uci command, reverts back to mixed mode.
This nonstandard command is (obviously) available even in UCI mode.
- wait : Stop processing input until the current search completes.
This nonstandard command is available even in UCI mode.
- quit : exit the program
"""

View File

@@ -47,14 +47,14 @@ proc generateRookBlockers: array[Square.smallest()..Square.biggest(), Bitboard]
current = bitboard
last = makeSquare(rank, pieces.File(7)).toBitboard()
while true:
current = current.rightRelativeTo(White)
current = current.right(White)
if current == last or current.isEmpty():
break
result[square] = result[square] or current
current = bitboard
last = makeSquare(rank, pieces.File(0)).toBitboard()
while true:
current = current.leftRelativeTo(White)
current = current.left(White)
if current == last or current.isEmpty():
break
result[square] = result[square] or current

110
src/heimdall/util/perft.nim Normal file
View File

@@ -0,0 +1,110 @@
# Copyright 2025 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 heimdall/[moves, movegen, pieces]
import std/strformat
type PerftData* = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64]
proc perft*(board: Chessboard, ply: int, verbose = false, divide = false, bulk = false, capturesOnly = false): PerftData =
## Counts (and debugs) the number of legal positions reached after
## the given number of ply
if ply == 0:
result.nodes = 1
return
var moves = newMoveList()
board.generateMoves(moves, capturesOnly=capturesOnly)
if not bulk:
if len(moves) == 0 and board.inCheck():
result.checkmates = 1
# TODO: Should we count stalemates/draws?
if ply == 0:
result.nodes = 1
return
elif ply == 1 and bulk:
if divide:
for move in moves:
echo &"{move.toUCI()}: 1"
if verbose:
echo ""
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
for move in moves:
if verbose:
let canCastle = board.canCastle()
echo &"Move: {move.startSquare.toUCI()}{move.targetSquare.toUCI()}"
echo &"Turn: {board.sideToMove}"
echo &"Piece: {board.position.on(move.startSquare).kind}"
echo &"Flag: {move.flag()}"
echo &"In check: {(if board.inCheck(): \"yes\" else: \"no\")}"
echo &"Castling targets:\n - King side: {(if canCastle.king != nullSquare(): canCastle.king.toUCI() else: \"None\")}\n - Queen side: {(if canCastle.queen != nullSquare(): canCastle.queen.toUCI() else: \"None\")}"
echo &"Position before move: {board.toFEN()}"
echo &"Hash: {board.zobristKey}"
stdout.write("En Passant target: ")
if board.position.enPassantSquare != nullSquare():
echo board.position.enPassantSquare.toUCI()
else:
echo "None"
echo "\n", board.pretty()
board.doMove(move)
when not defined(danger):
let incHash = board.zobristKey
let pawnKey = board.pawnKey
board.positions[^1].hash()
doAssert board.zobristKey == incHash, &"{board.zobristKey} != {incHash} at {move} ({board.positions[^2].toFEN()})"
doAssert board.pawnKey == pawnKey, &"{board.pawnKey} != {pawnKey} at {move} ({board.positions[^2].toFEN()})"
if ply == 1:
if move.isCapture():
inc(result.captures)
if move.isCastling():
inc(result.castles)
if move.isPromotion():
inc(result.promotions)
if move.isEnPassant():
inc(result.enPassant)
if board.inCheck():
# Opponent king is in check
inc(result.checks)
if verbose:
let canCastle = board.canCastle()
echo "\n"
echo &"Opponent in check: {(if board.inCheck(): \"yes\" else: \"no\")}"
echo &"Opponent castling targets:\n - King side: {(if canCastle.king != nullSquare(): canCastle.king.toUCI() else: \"None\")}\n - Queen side: {(if canCastle.queen != nullSquare(): canCastle.queen.toUCI() else: \"None\")}"
echo &"Position after move: {board.toFEN()}"
echo "\n", board.pretty()
stdout.write("nextpos>> ")
try:
discard readLine(stdin)
except IOError:
discard
except EOFError:
discard
let next = board.perft(ply - 1, verbose, bulk=bulk)
board.unmakeMove()
if divide and (not bulk or ply > 1):
echo &"{move.toUCI()}: {next.nodes}"
if verbose:
echo ""
result.nodes += next.nodes
result.captures += next.captures
result.checks += next.checks
result.promotions += next.promotions
result.castles += next.castles
result.enPassant += next.enPassant
result.checkmates += next.checkmates

View File

@@ -16,7 +16,7 @@
import heimdall/pieces
import std/[enumerate, strformat]
import std/[enumerate, strformat, algorithm]
func scharnaglConfig(scharnagl_number: int): array[8, PieceKind] =
@@ -74,21 +74,29 @@ func scharnaglConfig(scharnagl_number: int): array[8, PieceKind] =
func scharnaglToFEN*(whiteScharnaglNumber: int, blackScharnaglNumber: int): string =
var castleRights: string
var whiteCastleRights: string
var whiteConfig: string
for file, pieceKind in enumerate(scharnaglConfig(whiteScharnaglNumber)):
whiteConfig &= Piece(color: White, kind: pieceKind).toChar()
if pieceKind == Rook:
castleRights &= char('A'.uint8 + file.uint8)
whiteCastleRights &= char('A'.uint8 + file.uint8)
# Swap them: they should be in KQkq order, but due to how we generate
# them they are swapped
reverse(whiteCastleRights)
var blackCastleRights: string
var blackConfig: string
for file, pieceKind in enumerate(scharnaglConfig(blackScharnaglNumber)):
blackConfig &= Piece(color: Black, kind: pieceKind).toChar()
if pieceKind == Rook:
castleRights &= char('a'.uint8 + file.uint8)
blackcastleRights &= char('a'.uint8 + file.uint8)
return fmt"{blackConfig}/pppppppp/8/8/8/8/PPPPPPPP/{whiteConfig} w {castleRights} - 0 1"
reverse(blackCastleRights)
return &"{blackConfig}/pppppppp/8/8/8/8/PPPPPPPP/{whiteConfig} w {whiteCastleRights & blackCastleRights} - 0 1"
func scharnaglToFEN*(scharnaglNumber: int): string = scharnaglToFEN(scharnaglNumber, scharnaglNumber)

View File

@@ -39,7 +39,7 @@ type
# The current best move
bestMove*: Atomic[Move]
# How many nodes were spent on each
# move, indexed by from/to square,
# root move, indexed by from/to square,
# across the entire search
spentNodes*: array[Square.smallest()..Square.biggest(), array[Square.smallest()..Square.biggest(), Atomic[uint64]]]

View File

@@ -21,11 +21,7 @@ import heimdall/pieces
type
ZobristKey* = distinct uint64
## A zobrist key
TruncatedZobristKey* = distinct uint16
## A 16-bit truncated version
## of a full zobrist key
func `xor`*(a, b: ZobristKey): ZobristKey {.borrow.}
@@ -38,7 +34,6 @@ func `$`*(a: TruncatedZobristKey): string {.borrow.}
func computeZobristKeys: array[781, ZobristKey] {.compileTime.} =
## Precomputes our zobrist keys
var prng = initRand(69420) # Nice.
# One for each piece on each square