More cleanup and refactoring, fix minor issue in Position.hash(), unify TUI and UCI (bench 5583594)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -183,4 +183,5 @@ pretty.json
|
||||
fcq
|
||||
*.bullet
|
||||
nim.cfg
|
||||
data/
|
||||
data/
|
||||
Dockerfile.ppc64le
|
||||
@@ -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:
|
||||
|
||||
@@ -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.} =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
94
src/heimdall/util/help.nim
Normal file
94
src/heimdall/util/help.nim
Normal 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
|
||||
"""
|
||||
@@ -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
110
src/heimdall/util/perft.nim
Normal 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
|
||||
@@ -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)
|
||||
@@ -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]]]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user