Merge tunable parameters and use SPSA tuned values (21.94 +- 9.68) (bench 2582385)

This commit is contained in:
Mattia Giambirtone 2024-06-29 15:48:46 +02:00
commit 5c6b28dee1
Signed by: nocturn9x
GPG Key ID: B6025DD9B4458B69
5 changed files with 387 additions and 107 deletions

View File

@ -24,6 +24,8 @@ import heimdallpkg/board
import heimdallpkg/transpositions
import heimdallpkg/search
import heimdallpkg/eval
import heimdallpkg/tunables
import std/os
@ -51,13 +53,14 @@ proc runBench =
historyTable = create(HistoryTable)
killerMoves = create(KillersTable)
counterMoves = create(CountersTable)
parameters = getDefaultParameters()
transpositionTable[] = newTranspositionTable(64 * 1024 * 1024)
echo "Benchmark started"
var nodes = 0'u64
let startTime = cpuTime()
for i, fen in benchFens:
echo &"Position {i + 1}/{len(benchFens)}: {fen}\n"
var mgr = newSearchManager(@[loadFEN(fen)], transpositionTable, historyTable, killerMoves, counterMoves)
var mgr = newSearchManager(@[loadFEN(fen)], transpositionTable, historyTable, killerMoves, counterMoves, parameters)
let line = mgr.search(0, 0, 10, 0, @[], false, true, false, 1)
if line.len() == 1:
echo &"bestmove {line[0].toAlgebraic()}"
@ -87,9 +90,15 @@ when isMainModule:
for kind, key, value in parser.getopt():
case kind:
of cmdArgument:
if key == "bench":
runBench()
quit(0)
case key:
of "bench":
runBench()
quit(0)
of "spsa":
echo getSPSAInput(getDefaultParameters())
quit(0)
else:
discard
of cmdLongOption:
discard
of cmdShortOption:

View File

@ -17,6 +17,7 @@ import board
import movegen
import eval
import see
import tunables
import transpositions
@ -28,44 +29,10 @@ import std/monotimes
import std/strformat
# Lots of knobs and dials to configure the search
# Miscellaneous parameters that are not meant to be tuned
const
# Constants to configure how aggressively
# NMP reduces the search depth
# Start pruning moves after this depth has
# been cleared
NMP_DEPTH_THRESHOLD = 2
# Reduce search depth by at least this value
NMP_BASE_REDUCTION = 3
# Reduce search depth proportionally to the
# current depth divided by this value, plus
# the base reduction
NMP_DEPTH_REDUCTION = 3
# Constants to configure RFP
# (Reverse Futility Pruning)
# Advantage threshold
RFP_EVAL_THRESHOLD = 100
# Depth after which RFP is disabled
RFP_DEPTH_LIMIT = 7
# Constants to configure FP
# (Futility pruning)
# Limit after which FP is disabled
FP_DEPTH_LIMIT = 2
# Advantage threshold
FP_EVAL_MARGIN = 250
# Constants to configure LMP (Late
# Move Pruning)
# Start pruning after at least LMP_DEPTH_OFFSET + (LMP_DEPTH_MULTIPLIER * depth ^ 2)
# moves have been played
LMP_DEPTH_OFFSET = 6
LMP_DEPTH_MULTIPLIER = 2
# TODO
# Constants to configure razoring
# Only prune when depth <= this value
@ -76,35 +43,6 @@ const
# is <= alpha
RAZORING_EVAL_THRESHOLD {.used.} = 400
# Constants to configure LMR (Late Move
# Reductions)
LMR_MIN_DEPTH = 3
LMR_MOVENUMBER = (pv: 5, nonpv: 2)
# Constants to configure IIR (Internal
# iterative reductions)
# Only reduce when depth >= this value
IIR_MIN_DEPTH {.used.} = 4
# Constants to configure aspiration
# windows
# Only use aspiration windows when search
# depth is >= this value
ASPIRATION_WINDOW_DEPTH_THRESHOLD = 5
ASPIRATION_WINDOW_INITIAL_DELTA = 30
ASPIRATION_WINDOW_MAX_DELTA = 1000
# Constants to configure SEE pruning
# Only SEE prune when depth <= this value
SEE_PRUNING_MAX_DEPTH = 5
# Prune quiets whose SEE score is < depth * this value
SEE_PRUNING_QUIET_MARGIN = 80
# Miscellaneaus configuration
NUM_KILLERS* = 2
@ -129,12 +67,6 @@ const
# Max value for scores in our quiet
# history
HISTORY_SCORE_CAP = 16384
# Good quiets get a bonus of GOOD_QUIET_HISTORY_BONUS * depth
# in the quiet history table, bad quiets get a malus of BAD_QUIET_HISTORY_MALUS *
# depth instead
GOOD_QUIET_HISTORY_BONUS = 170
BAD_QUIET_HISTORY_MALUS = -450
func computeLMRTable: array[MAX_DEPTH, array[MAX_MOVES, int]] {.compileTime.} =
## Precomputes the table containing reduction offsets at compile
@ -193,14 +125,19 @@ type
# The piece that moved in the previous
# move
previousPiece: Piece
# The set of parameters used by search
parameters: SearchParameters
proc newSearchManager*(positions: seq[Position], transpositions: ptr TTable,
history: ptr HistoryTable, killers: ptr KillersTable, counters: ptr CountersTable, mainWorker=true): SearchManager =
history: ptr HistoryTable, killers: ptr KillersTable,
counters: ptr CountersTable, parameters: SearchParameters,
mainWorker=true): SearchManager =
## Initializes a new search manager
new(result)
result = SearchManager(transpositionTable: transpositions, history: history,
killers: killers, counters: counters, isMainWorker: mainWorker)
killers: killers, counters: counters, isMainWorker: mainWorker,
parameters: parameters)
new(result.board)
result.board.positions = positions
for i in 0..MAX_DEPTH:
@ -255,7 +192,7 @@ func storeHistoryScore(self: SearchManager, sideToMove: PieceColor, move: Move,
## Stores a move for the given side in our quiet history table,
## tweaking the score appropriately if it failed high or low
let bonus = if good: GOOD_QUIET_HISTORY_BONUS * depth else: BAD_QUIET_HISTORY_MALUS * depth
let bonus = if good: self.parameters.goodQuietBonus * depth else: -self.parameters.badQuietMalus * depth
# We use this formula to evenly spread the improvement the more we increase it (or decrease it)
# while keeping it constrained to a maximum (or minimum) value so it doesn't (over|under)flow.
self.history[sideToMove][move.startSquare][move.targetSquare] += Score(bonus) - abs(bonus.int32) * self.getHistoryScore(sideToMove, move) div HISTORY_SCORE_CAP
@ -444,8 +381,8 @@ proc shouldStop(self: SearchManager): bool =
proc getReduction(self: SearchManager, move: Move, depth, ply, moveNumber: int, isPV, improving: bool): int =
## Returns the amount a search depth should be reduced to
let moveCount = if isPV: LMR_MOVENUMBER.pv else: LMR_MOVENUMBER.nonpv
if moveNumber > moveCount and depth >= LMR_MIN_DEPTH:
let moveCount = if isPV: self.parameters.lmrMoveNumber.pv else: self.parameters.lmrMoveNumber.nonpv
if moveNumber > moveCount and depth >= self.parameters.lmrMinDepth:
result = LMR_TABLE[depth][moveNumber]
if isPV:
# Reduce PV nodes less
@ -608,13 +545,13 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score, isPV: bool
of UpperBound:
if score <= alpha:
return score
if ply > 0 and depth >= IIR_MIN_DEPTH and query.isNone():
if ply > 0 and depth >= self.parameters.iirMinDepth and query.isNone():
# Internal iterative reductions: if there is no best move in the TT
# for this node, it's not worth it to search it at full depth, so we
# reduce it and hope that the next search iteration yields better
# results
depth -= 1
if not isPV and not self.board.inCheck() and depth <= RFP_DEPTH_LIMIT and staticEval - RFP_EVAL_THRESHOLD * depth >= beta:
if not isPV and not self.board.inCheck() and depth <= self.parameters.rfpDepthLimit and staticEval - self.parameters.rfpEvalThreshold * depth >= beta:
# Reverse futility pruning: if the side to move has a significant advantage
# in the current position and is not in check, return the position's static
# evaluation to encourage the engine to deal with any potential threats from
@ -624,7 +561,7 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score, isPV: bool
# careful we want to be with our estimate for how much of an advantage we may
# or may not have)
return staticEval
if not isPV and depth > NMP_DEPTH_THRESHOLD and self.board.canNullMove() and staticEval >= beta:
if not isPV and depth > self.parameters.nmpDepthThreshold and self.board.canNullMove() and staticEval >= beta:
# Null move pruning: it is reasonable to assume that
# it is always better to make a move than not to do
# so (with some exceptions noted below). To take advantage
@ -655,7 +592,7 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score, isPV: bool
self.board.makeNullMove()
# We perform a shallower search because otherwise there would be no point in
# doing NMP at all!
let reduction = NMP_BASE_REDUCTION + depth div NMP_DEPTH_REDUCTION
let reduction = self.parameters.nmpBaseReduction + depth div self.parameters.nmpDepthReduction
let score = -self.search(depth - reduction, ply + 1, -beta - 1, -beta, isPV=false)
self.board.unmakeMove()
if score >= beta:
@ -693,7 +630,7 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score, isPV: bool
continue
# Ensures we don't prune moves that stave off checkmate
let isNotMated = bestScore > -mateScore() + MAX_DEPTH
if not isPV and move.isQuiet() and depth <= FP_DEPTH_LIMIT and staticEval + FP_EVAL_MARGIN * (depth + improving.int) < alpha and isNotMated:
if not isPV and move.isQuiet() and depth <= self.parameters.fpDepthLimit and staticEval + self.parameters.fpEvalMargin * (depth + improving.int) < alpha and isNotMated:
# Futility pruning: If a (quiet) move cannot meaningfully improve alpha, prune it from the
# tree. Much like RFP, this is an unsound optimization (and a riskier one at that,
# apparently), so our depth limit and evaluation margins are very conservative
@ -701,16 +638,16 @@ proc search(self: SearchManager, depth, ply: int, alpha, beta: Score, isPV: bool
# we'd risk pruning moves that evade checkmate
inc(i)
continue
if ply > 0 and move.isQuiet() and isNotMated and playedMoves >= (LMP_DEPTH_OFFSET + LMP_DEPTH_MULTIPLIER * depth * depth) #[div (2 - improving.int)]#:
if ply > 0 and move.isQuiet() and isNotMated and playedMoves >= (self.parameters.lmpDepthOffset + self.parameters.lmpDepthMultiplier * depth * depth) #[div (2 - improving.int)]#:
# Late move pruning: prune moves when we've played enough of them. Since the optimization
# is unsound, we want to make sure we don't accidentally miss a move that staves off
# checkmate
inc(i)
continue
if ply > 0 and isNotMated and depth <= SEE_PRUNING_MAX_DEPTH and move.isQuiet():
if ply > 0 and isNotMated and depth <= self.parameters.seePruningMaxDepth and move.isQuiet():
# SEE pruning: prune moves with a bad SEE score
let seeScore = self.board.positions[^1].see(move)
let margin = -depth * SEE_PRUNING_QUIET_MARGIN
let margin = -depth * self.parameters.seePruningQuietMargin
if seeScore < margin:
inc(i)
continue
@ -852,13 +789,13 @@ proc findBestLine(self: SearchManager, timeRemaining, increment: int64, maxDepth
self.searching.store(true)
var score = Score(0)
for depth in 1..min(MAX_DEPTH, maxDepth):
if depth < ASPIRATION_WINDOW_DEPTH_THRESHOLD:
if depth < self.parameters.aspWindowDepthThreshold:
score = self.search(depth, 0, lowestEval(), highestEval(), true)
else:
# Aspiration windows: start subsequent searches with tighter
# alpha-beta bounds and widen them as needed (i.e. when the score
# goes beyond the window) to increase the number of cutoffs
var delta = Score(ASPIRATION_WINDOW_INITIAL_DELTA)
var delta = Score(self.parameters.aspWindowInitialSize)
var alpha = max(lowestEval(), score - delta)
var beta = min(highestEval(), score + delta)
var searchDepth {.used.} = depth
@ -875,7 +812,7 @@ proc findBestLine(self: SearchManager, timeRemaining, increment: int64, maxDepth
break
# Try again with larger window
delta += delta
if delta >= Score(ASPIRATION_WINDOW_MAX_DELTA):
if delta >= Score(self.parameters.aspWindowMaxSize):
# Window got too wide, give up and search with the full range
# of alpha-beta values
delta = highestEval()
@ -960,7 +897,7 @@ proc search*(self: SearchManager, timeRemaining, increment: int64, maxDepth: int
for toSq in Square(0)..Square(63):
counters[fromSq][toSq] = self.counters[fromSq][toSq]
# Create a new search manager to send off to a worker thread
self.children.add(newSearchManager(self.board.positions, self.transpositionTable, history, killers, counters, false))
self.children.add(newSearchManager(self.board.positions, self.transpositionTable, history, killers, counters, self.parameters, false))
# Off you go, you little search minion!
createThread(workers[i][], workerFunc, (self.children[i], timeRemaining, increment, maxDepth, maxNodes div numWorkers.uint64, searchMoves, timePerMove, ponder, silent))
# Pin thread to one CPU core to remove task switching overheads

View File

@ -0,0 +1,316 @@
# Copyright 2024 Mattia Giambirtone & All Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import std/tables
import std/strformat
import std/strutils
const isTuningEnabled* {.booldefine:"enableTuning".} = false
type
TunableParameter* = ref object
## An SPSA-tunable parameter
name*: string
min*: int
max*: int
default*: int
SearchParameters* = ref object
## A set of search parameters
# Null move pruning
# Start pruning moves when depth > this
# value
nmpDepthThreshold*: int
# Reduce search depth by at least this value
nmpBaseReduction*: int
# Reduce search depth proportionally to the
# current depth divided by this value, plus
# the base reduction
nmpDepthReduction*: int
# Reverse futility pruning
# Prune only when we're at least this
# many engine units ahead in the static
# evaluation (multiplied by depth)
rfpEvalThreshold*: int
# Prune only when depth <= this value
rfpDepthLimit*: int
# Futility pruning
# Prune only when depth <= this value
fpDepthLimit*: int
# Prune only when (staticEval + margin) * (depth - improving)
# is less than alpha
fpEvalMargin*: int
# Late move pruning
# Start pruning after at least lmpDepthOffset + (lmpDepthMultiplier * depth ^ 2)
# moves have been played
lmpDepthOffset*: int
lmpDepthMultiplier*: int
# Late move reductions
# Reduce when depth is >= this value
lmrMinDepth*: int
# Reduce when the number of moves yielded by the move
# picker reaches this value in either a pv or non-pv
# node
lmrMoveNumber*: tuple[pv, nonpv: int]
# Internal Iterative reductions
# Reduce only when depth >= this value
iirMinDepth*: int
# Aspiration windows
# Use aspiration windows when depth >
# this value
aspWindowDepthThreshold*: int
# Use this value as the initial
# aspiration window size
aspWindowInitialSize*: int
# Give up and search the full range
# of alpha beta values once the window
# size gets to this value
aspWindowMaxSize*: int
# SEE pruning
# Only prune when depth <= this value
seePruningMaxDepth*: int
# Only prune quiet moves whose SEE score
# is < this value
seePruningQuietMargin*: int
# Quiet history bonuses
# Good quiets get a bonus of goodQuietBonus * depth in the
# quiet history table, bad quiets get a malus of badQuietMalus *
# depth instead
goodQuietBonus*: int
badQuietMalus*: int
var params = newTable[string, TunableParameter]()
proc newTunableParameter*(name: string, min, max, default: int): TunableParameter =
## Initializes a new tunable parameter
new(result)
result.name = name
result.min = min
result.max = max
result.default = default
# Paste here the SPSA output from openbench and the values
# will be loaded automatically into the default field of each
# parameter
const SPSA_OUTPUT = """
IIRMinDepth, 4
FPDepthLimit, 5
LMPDepthMultiplier, 1
NMPDepthThreshold, 1
AspWindowInitialSize, 32
LMRPvMovenumber, 5
NMPDepthReduction, 3
RFPEvalThreshold, 119
GoodQuietBonus, 182
SEEPruningQuietMargin, 81
LMRNonPvMovenumber, 2
AspWindowMaxSize, 929
LMPDepthOffset, 5
NMPBaseReduction, 3
LMRMinDepth, 3
SEEPruningMaxDepth, 5
FPEvalMargin, 249
RFPDepthLimit, 7
AspWindowDepthThreshold, 5
BadQuietMalus, 418
""".replace(" ", "")
proc addTunableParameters =
## Adds all our tunable parameters to the global
## parameter list
params["NMPDepthThreshold"] = newTunableParameter("NMPDepthThreshold", 1, 4, 2)
params["NMPBaseReduction"] = newTunableParameter("NMPBaseReduction", 1, 6, 3)
params["NMPDepthReduction"] = newTunableParameter("NMPDepthReduction", 1, 6, 3)
params["RFPEvalThreshold"] = newTunableParameter("RFPEvalThreshold", 1, 200, 100)
params["RFPDepthLimit"] = newTunableParameter("RFPDepthLimit", 1, 14, 7)
params["FPDepthLimit"] = newTunableParameter("FPDepthLimit", 1, 5, 2)
params["FPEvalMargin"] = newTunableParameter("FPEvalMargin", 1, 500, 250)
params["LMPDepthOffset"] = newTunableParameter("LMPDepthOffset", 1, 12, 6)
params["LMPDepthMultiplier"] = newTunableParameter("LMPDepthMultiplier", 1, 4, 2)
params["LMRMinDepth"] = newTunableParameter("LMRMinDepth", 1, 6, 3)
params["LMRPvMovenumber"] = newTunableParameter("LMRPvMovenumber", 1, 10, 5)
params["LMRNonPvMovenumber"] = newTunableParameter("LMRNonPvMovenumber", 1, 4, 2)
params["IIRMinDepth"] = newTunableParameter("IIRMinDepth", 1, 8, 4)
params["AspWindowDepthThreshold"] = newTunableParameter("AspWindowDepthThreshold", 1, 10, 5)
params["AspWindowInitialSize"] = newTunableParameter("AspWindowInitialSize", 1, 60, 30)
params["AspWindowMaxSize"] = newTunableParameter("AspWindowMaxSize", 1, 2000, 1000)
params["SEEPruningMaxDepth"] = newTunableParameter("SEEPruningMaxDepth", 1, 10, 5)
params["SEEPruningQuietMargin"] = newTunableParameter("SEEPruningQuietMargin", 1, 160, 80)
params["GoodQuietBonus"] = newTunableParameter("GoodQuietBonus", 1, 340, 170)
params["BadQuietMalus"] = newTunableParameter("BadQuietMalus", 1, 900, 450)
for line in SPSA_OUTPUT.splitLines(keepEol=false):
if line.len() == 0:
continue
let splosh = line.split(",", maxsplit=2)
params[splosh[0]].default = splosh[1].parseInt()
proc isParamName*(name: string): bool =
## Returns whether the given string
## represents a tunable parameter name
return name in params
proc setParameter*(self: SearchParameters, name: string, value: int) =
## Sets the tunable parameter with the given name
## to the given integer value
# This is ugly, but short of macro shenanigans it's
# the best we can do
case name:
of "NMPDepthThreshold":
self.nmpDepthThreshold = value
of "NMPBaseReduction":
self.nmpDepthReduction = value
of "NMPDepthReduction":
self.nmpBaseReduction = value
of "RFPEvalThreshold":
self.rfpEvalThreshold = value
of "RFPDepthLimit":
self.rfpDepthLimit = value
of "FPDepthLimit":
self.fpDepthLimit = value
of "FPEvalMargin":
self.fpEvalMargin = value
of "LMPDepthOffset":
self.lmpDepthOffset = value
of "LMPDepthMultiplier":
self.lmpDepthMultiplier = value
of "LMRMinDepth":
self.lmrMinDepth = value
of "LMRPvMovenumber":
self.lmrMoveNumber.pv = value
of "LMRNonPvMovenumber":
self.lmrMoveNumber.nonpv = value
of "IIRMinDepth":
self.iirMinDepth = value
of "AspWindowDepthThreshold":
self.aspWindowDepthThreshold = value
of "AspWindowInitialSize":
self.aspWindowInitialSize = value
of "AspWindowMaxSize":
self.aspWindowMaxSize = value
of "SEEPruningMaxDepth":
self.seePruningMaxDepth = value
of "SEEPruningQuietMargin":
self.seePruningQuietMargin = value
of "GoodQuietBonus":
self.goodQuietBonus = value
of "BadQuietMalus":
self.badQuietMalus = value
else:
raise newException(ValueError, &"invalid tunable parameter '{name}'")
proc getParameter*(self: SearchParameters, name: string): int =
# Retrieves the value of the given search parameter.
# This is not meant to be used during search (it's
# not the fastest thing ever), but rather for SPSA
# tuning!
# This is ugly, but short of macro shenanigans it's
# the best we can do
case name:
of "NMPDepthThreshold":
return self.nmpDepthThreshold
of "NMPBaseReduction":
return self.nmpBaseReduction
of "NMPDepthReduction":
return self.nmpDepthReduction
of "RFPEvalThreshold":
return self.rfpEvalThreshold
of "RFPDepthLimit":
return self.rfpDepthLimit
of "FPDepthLimit":
return self.fpDepthLimit
of "FPEvalMargin":
return self.fpEvalMargin
of "LMPDepthOffset":
return self.lmpDepthOffset
of "LMPDepthMultiplier":
return self.lmpDepthMultiplier
of "LMRMinDepth":
return self.lmrMinDepth
of "LMRPvMovenumber":
return self.lmrMoveNumber.pv
of "LMRNonPvMovenumber":
return self.lmrMoveNumber.nonpv
of "IIRMinDepth":
return self.iirMinDepth
of "AspWindowDepthThreshold":
return self.aspWindowDepthThreshold
of "AspWindowInitialSize":
return self.aspWindowInitialSize
of "AspWindowMaxSize":
return self.aspWindowMaxSize
of "SEEPruningMaxDepth":
return self.seePruningMaxDepth
of "SEEPruningQuietMargin":
return self.seePruningQuietMargin
of "GoodQuietBonus":
return self.goodQuietBonus
of "BadQuietMalus":
return self.badQuietMalus
else:
raise newException(ValueError, &"invalid tunable parameter '{name}'")
iterator getParameters*: TunableParameter =
## Yields all parameters that can be
## tuned
for key in params.keys():
yield params[key]
proc getDefaultParameters*: SearchParameters =
## Returns the set of parameters to be
## used during search
new(result)
for key in params.keys():
result.setParameter(key, params[key].default)
proc getSPSAInput*(parameters: SearchParameters): string =
## Returns the SPSA input to be passed to
## OpenBench for tuning
for param in getParameters():
let current = parameters.getParameter(param.name)
result &= &"{param.name}, int, {current}, {param.min}, {param.max}, {max(0.5, (param.max - param.min) / 20)}, 0.002\n"
addTunableParameters()

View File

@ -22,6 +22,7 @@ import board
import movegen
import search
import eval
import tunables
import transpositions
@ -31,13 +32,10 @@ type
debug: bool
# All reached positions
history: seq[Position]
## Information about the current search. We use a
## raw pointer because Nim's memory management strategy
## doesn't like sharing references across thread (despite
## the fact that it should be safe to do so)
## Information about the current search
searchState: SearchManager
printMove: ptr Atomic[bool]
# Size of the transposition table (in megabytes)
# Size of the transposition table (in megabytes, and not the retarded kind!)
hashTableSize: uint64
# Number of workers to use during search
workers: int
@ -87,6 +85,9 @@ type
proc parseUCIMove(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
@ -134,7 +135,9 @@ proc parseUCIMove(position: Position, move: string): tuple[move: Move, command:
result.move = createMove(startSquare, targetSquare, flags)
proc handleUCIMove(session: var UCISession, board: var Chessboard, move: string): tuple[move: Move, cmd: UCICommand] {.discardable.} =
proc handleUCIMove(session: UCISession, board: Chessboard, move: 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 {move}"
let
@ -148,6 +151,7 @@ proc handleUCIMove(session: var UCISession, board: var Chessboard, move: string)
proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand =
## Handles the "go" UCI command
result = UCICommand(kind: Go)
result.wtime = 0
result.btime = 0
@ -157,8 +161,7 @@ proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand =
result.depth = -1
result.moveTime = -1
result.nodes = 0
var
current = 1 # Skip the "go"
var current = 1 # Skip the "go"
while current < command.len():
let flag = command[current]
inc(current)
@ -198,6 +201,8 @@ proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand =
proc handleUCIPositionCommand(session: var UCISession, command: seq[string]): UCICommand =
## Handles the "position" UCI command
# Makes sure we don't leave the board in an invalid state if
# some error occurs
result = UCICommand(kind: Position)
@ -348,11 +353,18 @@ proc startUCISession* =
echo "id author Nocturn9x & Contributors (see LICENSE)"
echo "option name Hash type spin default 64 min 1 max 33554432"
echo "option name Threads type spin default 1 min 1 max 1024"
# Clears the TT
echo "option name TTClear type button"
# Clears the quiet history
echo "option name HClear type button"
# Clears the killers table
echo "option name KClear type button"
# Clears the counter moves table
echo "option name CClear type button"
echo "option name EnableWeirdTCs type check default false"
when isTuningEnabled:
for param in getParameters():
echo &"option name {param.name} type spin default {param.default} min {param.min} max {param.max}"
echo "uciok"
var
cmd: UCICommand
@ -365,11 +377,9 @@ proc startUCISession* =
historyTable = create(HistoryTable)
killerMoves = create(KillersTable)
counterMoves = create(CountersTable)
parameters = getDefaultParameters()
transpositionTable[] = newTranspositionTable(session.hashTableSize * 1024 * 1024)
session.searchState = newSearchManager(session.history, transpositionTable, historyTable, killerMoves, counterMoves)
# This is only ever written to from the main thread and read from
# the worker starting the search, so it doesn't need to be wrapped
# in an atomic
session.searchState = newSearchManager(session.history, transpositionTable, historyTable, killerMoves, counterMoves, parameters)
session.printMove = create(Atomic[bool])
# Initialize history table
for color in PieceColor.White..PieceColor.Black:
@ -380,6 +390,7 @@ proc startUCISession* =
for i in 0..<MAX_DEPTH:
for j in 0..<NUM_KILLERS:
killerMoves[i][j] = nullMove()
# Initialize counter moves table
for fromSq in Square(0)..Square(63):
for toSq in Square(0)..Square(63):
counterMoves[fromSq][toSq] = nullMove()
@ -424,10 +435,14 @@ proc startUCISession* =
for i in Square(0)..Square(63):
for j in Square(0)..Square(63):
historyTable[color][i][j] = Score(0)
# Re-nitialize killer move table
# Re-initialize killer move table
for i in 0..<MAX_DEPTH:
for j in 0..<NUM_KILLERS:
killerMoves[i][j] = nullMove()
# Re-initialize counter moves table
for fromSq in Square(0)..Square(63):
for toSq in Square(0)..Square(63):
counterMoves[fromSq][toSq] = nullMove()
of PonderHit:
if session.debug:
echo "info string ponder move has ben hit"
@ -505,7 +520,9 @@ proc startUCISession* =
echo &"info string set thread count to {numWorkers}"
session.workers = numWorkers
else:
discard
when isTuningEnabled:
if cmd.name.isParamName():
parameters.setParameter(cmd.name, cmd.value.parseInt())
of Position:
if session.searchState.isPondering():
# The ponder move was not played. Stop

View File

@ -7,3 +7,4 @@
--mm:arc
--panics:on
#-d:mimalloc
#-d:enableTuning