ExpectimaxSearch

dicechess.engine.search.ExpectimaxSearch
See theExpectimaxSearch companion object
final class ExpectimaxSearch(evalBatch: (Array[GameState], Color) => Array[Int], config: ExpectimaxConfig = ..., rootRescore: Option[RootRescore] = ..., preRank: (Array[GameState], Color) => Array[Int] = ..., statsSink: RootSearchStats => Unit = ..., tt: Option[TranspositionTable] = ..., chanceCollapse: Option[ChanceCollapse] = ...) extends TimeBudgetedSearch

Configurable two- or three-ply expectimax search for Dice Chess.

Unlike a one-ply evaluator (GreedySearch, OnnxEvalSearch), this looks one full turn ahead and so sees tactical punishments — a capture that hangs a bigger piece to the recapture — that a static evaluation cannot. The layer between the two plies is a chance node: the opponent's roll is unknown when we move, so the value of our turn is the expectation over all DiceRolls outcomes of the opponent's best (for them) reply.

The evaluation function is injected as a batch (evalBatch(states, color) scores every state from color's perspective) so the same search works with any leaf evaluator — the engine's material score, or an externally trained model — and so the many leaves under one chance node can be scored in a single call. Non-terminal leaf scores must not exceed ExpectimaxSearch.UpperScoreBound because Star pruning uses that ceiling; values below zero are supported down to the terminal-loss sentinel.

Two terminal cases sit outside the evaluator, because a king capture ends the game and material scores never see the king:

  • if one of our own turns captures the opponent's king, we play it immediately (an outright win);
  • a leaf where the opponent captures our king is worth ExpectimaxSearch.LossValue — below any real score on any scale — so the opponent always takes it and we always rank that line last.

At depth 3, every opponent reply is followed by a chance node over our next roll and a MAX node over our legal replies before leaf evaluation. As a TimeBudgetedSearch the implementation honours a wall-clock deadline at every chance-node roll boundary, expanding pre-ranked root candidates until time runs out.

Value parameters

chanceCollapse

when present, replaces every root candidate's exact chance-node expansion with one batched model call — see ChanceCollapse for what that keeps and what it gives up. Absent by default, and absent means the exact search runs unchanged: the collapsed path is a separate branch that the default configuration never enters.

preRank

batched evaluator used to rank the mover's own legal turns before the (expensive) chance-node expansion — only the top ExpectimaxConfig.candidateLimit are explored. Defaults to material (ExpectimaxSearch.materialBatch — the historical, hardcoded behaviour). Widening candidateLimit compensates for a crude pre-ranker at linear search-cost growth; a sharper pre-ranker (e.g. the same value model already driving the chance node) attacks the actual bottleneck instead — candidateLimit=16 vs material pre-ranking measured +4.8pp purely from surfacing turns the material proxy had buried outside the top 8.

statsSink

receives one RootSearchStats per findBestMove call. Defaults to a no-op so the hot path pays nothing beyond a single call; a real sink lets a host (arena runner, production bot) observe how many candidates the deadline actually allowed — see RootSearchStats for why that matters.

Attributes

Companion
object
Source
ExpectimaxSearch.scala
Graph
Supertypes
class Object
trait Matchable
class Any

Members list

Value members

Concrete methods

override def findBestMove(state: GameState): Option[ScoredSequence]

Finds and returns the best full-turn path according to this strategy.

Finds and returns the best full-turn path according to this strategy.

Value parameters

state

the current dicechess.engine.domain.GameState; state.activeColor identifies the side to move

Attributes

Returns

Some([[ScoredSequence]]) when at least one legal path exists; None when the player must pass

Definition Classes
Source
ExpectimaxSearch.scala
override def findBestMove(state: GameState, rand: Random): Option[ScoredSequence]

Finds the best turn with an explicit Random, running to completion over every candidate.

Finds the best turn with an explicit Random, running to completion over every candidate.

Attributes

Definition Classes
Source
ExpectimaxSearch.scala
override def findBestMove(state: GameState, deadlineNanos: Long, random: Random): Option[ScoredSequence]

Finds the best turn under a wall-clock deadline.

Finds the best turn under a wall-clock deadline.

Candidates are expanded in material-ranked order, and the deadline is honoured between dice rolls inside a candidate's chance node — not merely between candidates. Even the top candidate can therefore be abandoned before it yields a comparable value, which is the point: one candidate routinely costs more than the whole per-turn budget, so a coarser check made the deadline advisory rather than binding (#496).

The result is the best completed candidate, or — when the deadline left no candidate time to finish — the pre-ranker's own top pick, so the anytime contract still delivers a legal turn. A partially expanded candidate is never ranked: its expectation covers only the rolls the clock allowed, so comparing it against complete ones would let the instant the deadline landed choose the move. RootSearchStats reports both cases.

All of this matters because a single roll can generate thousands of opponent replies (the Dice Chess branching tail).

Attributes

Definition Classes
Source
ExpectimaxSearch.scala

Inherited methods

protected def estimateWinProbability(state: GameState): Double

Estimates the winning probability in [0.0, 1.0] for the side to move. Convenience over winProbability for offer decisions, where the deciding side is the active colour; do not override it, override winProbability.

Estimates the winning probability in [0.0, 1.0] for the side to move. Convenience over winProbability for offer decisions, where the deciding side is the active colour; do not override it, override winProbability.

Attributes

Inherited from:
SearchAlgorithm
Source
SearchAlgorithm.scala
def shouldAcceptDouble(state: GameState, currentStake: Int): Boolean

Determines whether the bot should accept (Take) or decline (Drop) a double from the opponent, taking the side to move as the responder. Kept for source and binary compatibility; it delegates to the three-argument overload.

Determines whether the bot should accept (Take) or decline (Drop) a double from the opponent, taking the side to move as the responder. Kept for source and binary compatibility; it delegates to the three-argument overload.

Value parameters

currentStake

the proposed double stake (e.g. 2, 4...)

state

current game state (dice pool is empty)

Attributes

Returns

true to accept the double (Take), false to resign the current stake (Drop)

Note

Perspective hazard: this overload evaluates the win probability of state.activeColor. In a double decision state, state.activeColor is the player who offered the double, NOT the responder, so a caller that feeds the delivered position here asks whether the offerer should take. Callers must prefer shouldAcceptDouble(state, currentStake, responder); strategies must override that overload, not this one.

Inherited from:
SearchAlgorithm
Source
SearchAlgorithm.scala
def shouldAcceptDouble(state: GameState, currentStake: Int, responder: Color): Boolean

Determines whether the bot should accept (Take) or decline (Drop) a double from the opponent, evaluated from the perspective of responder.

Determines whether the bot should accept (Take) or decline (Drop) a double from the opponent, evaluated from the perspective of responder.

This is the single policy point for the take/drop decision: strategies that want their own threshold or estimator override this overload. The two-argument form below only delegates here, so a policy written against it is bypassed whenever a caller names the responder explicitly (which every caller holding a doubleDecision state must, see the hazard note there).

Value parameters

currentStake

the proposed double stake (e.g. 2, 4...)

responder

the colour of the player responding to the double offer

state

current game state (dice pool is empty); its active colour is the side to move, which in a double decision is the offerer, not the responder

Attributes

Returns

true to accept the double (Take), false to resign the current stake (Drop)

Inherited from:
SearchAlgorithm
Source
SearchAlgorithm.scala
def shouldAcceptDraw(state: GameState): Boolean

Determines whether the bot should accept a draw offered by the opponent.

Determines whether the bot should accept a draw offered by the opponent.

Value parameters

state

current game state

Attributes

Returns

true to accept the draw

Inherited from:
SearchAlgorithm
Source
SearchAlgorithm.scala
def shouldOfferDouble(state: GameState, currentStake: Int): Boolean

Determines whether the bot should offer a double before its dice roll.

Determines whether the bot should offer a double before its dice roll.

Value parameters

currentStake

the current stake of the game

state

current game state (dice pool is empty)

Attributes

Returns

true to offer a double, false otherwise

Inherited from:
SearchAlgorithm
Source
SearchAlgorithm.scala
def shouldOfferDraw(state: GameState): Boolean

Determines whether the bot should offer a draw in the current position.

Determines whether the bot should offer a draw in the current position.

Value parameters

state

current game state

Attributes

Returns

true to offer a draw

Inherited from:
SearchAlgorithm
Source
SearchAlgorithm.scala
protected def winProbability(state: GameState, color: Color): Double

Estimates the winning probability in [0.0, 1.0] of color, whichever side is to move.

Estimates the winning probability in [0.0, 1.0] of color, whichever side is to move.

This is the estimator hook: a strategy with its own equity model (for example a Monte-Carlo rollout) overrides this method, so that both the offer decision (active colour) and the take/drop decision (responder colour) see the same estimate. The default maps the centipawn evaluation to a probability with a logistic sigmoid.

Attributes

Inherited from:
SearchAlgorithm
Source
SearchAlgorithm.scala