dicechess.engine.search

Search and evaluation layer for the Dice Chess Engine.

This package implements the bot strategies, turn-path enumeration, and static evaluation function that power the engine's decision-making. It sits above the move-generation layer and operates on complete dicechess.engine.domain.GameStates.

Architecture

Component Responsibility
TurnGenerator Enumerates all legal full-turn paths (1–3 micro-moves) via DFS
dicechess.engine.movegen.LegalMovesFilter Filters first moves under the Maximum Micro-moves Rule (in movegen)
SearchAlgorithm Trait contract for bot strategies; returns a ScoredSequence
GreedySearch Picks the turn path with the highest material score; prefers short wins
RandomSearch Selects a uniformly random legal path; useful as a baseline / test bot
Evaluator Pure material-balance evaluator used by all current bot strategies
SearchScoring Shared scoring helper: scores a path and handles king-capture termination

Turn Model

A turn in Dice Chess consists of:

  1. A dice roll that yields 1–3 die values.
  2. A sequence of 1–3 micro-moves — each micro-move must be made by the piece type matching the consumed die.
  3. The active color is not toggled between micro-moves; only after the full turn completes.

The Maximum Micro-moves Rule requires a player to choose an opening move that is part of the longest achievable micro-move sequence. Any sequence that ends with a King capture is always legal regardless of length.

Scoring

SearchScoring.scorePath assigns SearchScoring.TerminalWinScore (Int.MaxValue) to any path that captures the opponent's King, and falls back to Evaluator.evaluateMaterial for non-terminal paths.

Attributes

Members list

Type members

Classlikes

Aggressive search algorithm for Dice Chess.

Aggressive search algorithm for Dice Chess.

This bot represents a Difficulty 5 intellect ("Aggressive Bot" or "King Attack"). It aims to actively hunt the opponent's King, push pawns forward aggressively (pawn storms), and cluster its attacking forces (Knights, Bishops, Rooks, Queens) around the enemy King's ring, while maintaining its own King safety.

Attributes

Source
AggressiveSearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type
case class BotInfo(id: String, name: String, description: String, difficulty: Int, isExperimental: Boolean)

Metadata representing a Search Algorithm (Bot) in the engine.

Metadata representing a Search Algorithm (Bot) in the engine.

Value parameters

description

Brief description of the bot's behavior

difficulty

Difficulty level from 1 to 10

id

Unique identifier (e.g., "random", "greedy")

isExperimental

True if the bot is in beta or experimental phase

name

Human-readable name

Attributes

Source
BotRegistry.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object BotRegistry

Central registry for all available search algorithms in the engine.

Central registry for all available search algorithms in the engine.

Attributes

Source
BotRegistry.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object BudgetedRules

Opt-in bounded traversal; the existing complete rules APIs are unchanged.

Opt-in bounded traversal; the existing complete rules APIs are unchanged.

One work unit admits a move-generator invocation, an examined micro-move, or a capture-roll iteration. A single move-generator invocation is not preempted internally. These counters bound traversal and retained path entries, not elapsed time, instructions or heap bytes; callers still need external wall/memory guards.

Attributes

Source
BudgetedRules.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class ChanceCollapse(evalBatch: (Array[GameState], Color) => Array[Int], lossGuard: Boolean = ...)

Learned replacement for a root candidate's chance node: instead of expanding the opponent's 56 weighted dice outcomes and their replies, one batched call scores the position each candidate reaches and returns that expectation directly.

Learned replacement for a root candidate's chance node: instead of expanding the opponent's 56 weighted dice outcomes and their replies, one batched call scores the position each candidate reaches and returns that expectation directly.

This is the search's most expensive layer by orders of magnitude — every candidate pays hundreds to thousands of leaf evaluations for one number — so a model that predicts that number turns a chance node into a single row of a batch. It is off by default and has to be enabled explicitly, because it trades an exact expectation for an estimate: what comes back is what the model thinks the expansion would have produced.

What it keeps: the immediate-king-capture shortcut and the forced pass above it, pre-ranking and its deterministic order, the random tie-break, the deadline's meaning (the batch is indivisible and is not started once the deadline has passed, so the anytime fallback to the pre-ranker's pick is unchanged), and root rescoring.

What it gives up: Star1/Star2 pruning and the transposition table have nothing to prune or store — a collapsed value is a different quantity from the exact expectation the table holds at that depth, so the table is left untouched rather than mixed — and ExpectimaxConfig.searchDepth has no effect, since no tree is built below the root.

It also gives up per-roll knowledge, and with it the one rule RootRescore treats as inviolable: a candidate that loses our king on some roll must never be rescued by a rescorer. lossGuard restores exactly that rule without expanding anything — KingCaptureProbability answers "can the opponent capture our king on their next roll" over the same 56 multisets, exactly (king-capture paths ignore the maximum micro-moves rule, so its depth-first search is not an estimate), at one 216-outcome search per root candidate. Leave it off for a pure collapse; turn it on when a rescorer is configured at a positive weight, or the rescorer can outvote a lost line.

Value parameters

evalBatch

scores the positions the candidates reach, from the root mover's perspective, on the same Int axis as the search's own evalBatch — the value means "the expectation of this chance node", not "the value of this leaf"

lossGuard

whether to re-establish the loss-taint rule described above. It only has an effect while a RootRescore is active at a positive weight — that rule is about what a rescorer may not rescue, and with no rescorer the collapsed value is returned unchanged — so the search skips the work entirely in that case rather than paying a 216-outcome search per candidate for a value nothing reads.

Attributes

Source
ExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Checkmate-Aware search algorithm for Dice Chess.

Checkmate-Aware search algorithm for Dice Chess.

This bot represents a Difficulty 2 intellect. It understands the primary objective of the game:

  • Capturing the opponent's King (instant win).
  • Protecting its own King (avoiding immediate capture).

It is completely blind to material balance or other positional values, making it play randomly when there are no immediate checkmates to deliver or checks to escape.

Attributes

Source
CheckmateAwareSearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class ClockState(remainingMs: Long, incrementMs: Long, moveNumber: Int, movesToGo: Option[Int] = ...)

A snapshot of the moving side's game clock, in milliseconds.

A snapshot of the moving side's game clock, in milliseconds.

This is the input to time management — everything TimeManager needs to turn a game clock into a per-turn thinking budget. The unit of time is a full Dice Chess turn (1–3 micro-moves searched together), and the Fischer increment is credited per turn.

Value parameters

incrementMs

per-turn Fischer increment in milliseconds (0 for sudden death)

moveNumber

the full-move number (the DFEN 6th field); used by the selected moves-to-go policy

movesToGo

explicit moves-to-go for a tournament-style control, when known; otherwise TimeManager estimates it

remainingMs

time left on the moving side's clock before this turn, in milliseconds

Attributes

Source
TimeManager.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object CollapseModel

Attributes

Companion
class
Source
OnnxExpectimaxSearch.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class CollapseModel(modelPath: String, extractFeatures: (GameState, Color) => Array[Float], lossGuard: Boolean = ..., contract: Option[ModelManifest] = ...)

A model that answers a root candidate's whole chance node in one row instead of an expansion of the opponent's 56 weighted dice outcomes — wired as ChanceCollapse, whose Scaladoc states what the hook keeps and what it gives up.

A model that answers a root candidate's whole chance node in one row instead of an expansion of the opponent's 56 weighted dice outcomes — wired as ChanceCollapse, whose Scaladoc states what the hook keeps and what it gives up.

It gets its own ONNX session, never the leaf model's: the two answer different questions (an expectation over the opponent's roll versus the value of one position), so a model in this slot is a different artifact with a different training target, and CollapseModel.fromPackage refuses one whose manifest says otherwise.

Value parameters

extractFeatures

this model's own feature extractor

lossGuard

whether to re-establish the exact search's loss-taint rule with one KingCaptureProbability search per root candidate — see ChanceCollapse.lossGuard

modelPath

path to the chance-collapse model, independent of the main model

Attributes

Companion
object
Source
OnnxExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object DiceRolls

The probability distribution of a single Dice Chess roll (3d6), as the chance-node primitive shared by every algorithm that has to integrate over an unknown roll.

The probability distribution of a single Dice Chess roll (3d6), as the chance-node primitive shared by every algorithm that has to integrate over an unknown roll.

A turn rolls three six-sided dice. Order does not matter (the three pips are a multiset of piece types the mover must play), so the 216 = 6³ ordered outcomes collapse into 56 distinct sorted multisets, each carrying the number of ordered rolls that map onto it as its weight:

Pattern Example Weight Count
AAA [1,1,1] 1 6
AAB [1,1,2] 3 30
ABC [1,2,3] 6 20

Total: 6×1 + 30×3 + 20×6 = 216. The probability of a multiset is therefore weight / 216.

This lives on its own (rather than inside a single consumer) because it is the exact distribution both KingCaptureProbability and an expectimax chance node must sum over — the same 56 weighted outcomes, computed once. Backed by an IArray so hot loops iterate it allocation-free.

Attributes

Source
DiceRolls.scala
Supertypes
class Object
trait Matchable
class Any
Self type
DiceRolls.type

Mix-in that equips a SearchAlgorithm with draw-awareness based on insufficient material and evaluation thresholds.

Mix-in that equips a SearchAlgorithm with draw-awareness based on insufficient material and evaluation thresholds.

Bots mixing in this trait will offer a draw when neither side can force a king capture — the win condition in Dice Chess. This happens with king + at most one bishop each, no other pieces, kings safely separated and the defending king on the opposite square colour from the enemy bishop. They will accept a draw offer in the same dead-material positions, and otherwise when the position evaluation drops below −200 centipawns (down material) — so a bot never declines a position it would itself offer.

The trait is designed to be reusable — any SearchAlgorithm can gain draw logic simply by declaring:

object MyBot extends SearchAlgorithm with DrawOfferLogic

Why separate from SearchAlgorithm

The base SearchAlgorithm defaults both draw methods to false so that simple bots remain silent. This trait packages a single, consistent draw policy that can be composed with any strategy without code duplication.

Attributes

Source
DrawOfferLogic.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
final case class EquityEstimate(whiteWin: Double, blackWin: Double, undecided: Double, rollouts: Int, standardError: Double, varianceReductionVsVanilla: Double)

Result of a Monte-Carlo pre-roll equity estimate.

Result of a Monte-Carlo pre-roll equity estimate.

The three probabilities partition every rollout's outcome mass and therefore sum to 1.0:

  • whiteWin / blackWin — probability that White / Black captures a king first;
  • undecided — residual survival mass at the ply horizon (games that were neither won nor lost within maxPlies).

Value parameters

standardError

the standard error of the whiteWin estimate (sqrt(sampleVariance / rollouts)); halve by quadrupling rollouts.

varianceReductionVsVanilla

how many times smaller the per-rollout variance is than a vanilla 0/1 Monte-Carlo with the same mean would be (mean·(1-mean) / sampleVariance). Values > 1 quantify the Rao-Blackwell gain; +Infinity means the estimate is exact (zero sample variance, e.g. a position decided on the first roll).

Attributes

Source
MonteCarloEquity.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object Evaluator

Evaluator for Dice Chess positions combining material balance and king safety.

Evaluator for Dice Chess positions combining material balance and king safety.

Prices pieces from MaterialValues — the centipawn scale this shares with PieceSafety's en-prise telemetry — and applies a significant penalty if the side's king is exposed to immediate capture. This helps greedy strategies avoid "material-greedy" blunders that lead to instant loss.

Attributes

Source
Evaluator.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Evaluator.type
final case class ExpectimaxConfig(candidateLimit: Int = ..., exactOnlyMode: Boolean = ..., searchDepth: Int = ...)

Tuning for ExpectimaxSearch.

Tuning for ExpectimaxSearch.

Value parameters

candidateLimit

how many of the mover's own turns (pre-ranked by ExpectimaxSearch's preRank, material by default) are expanded to full depth. Dice Chess routinely offers hundreds of legal turns per roll, so expanding all of them through a chance node is infeasible; this bounds the branching at the decision node. Must be positive.

exactOnlyMode

when true, the transposition table stores and reuses exact entries only. This is primarily a validation mode for comparing cached and uncached trees without fail-soft bound reuse.

searchDepth

number of full-turn plies in the expectimax tree. Depth 2 preserves the historical MAX → CHANCE → MIN → leaf search; depth 3 adds CHANCE → MAX → leaf after every opponent reply so exchanges include our recapture.

Attributes

Source
ExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Source
ExpectimaxSearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type
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.

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
Supertypes
class Object
trait Matchable
class Any
object GreedySearch extends SearchAlgorithm

Greedy bot strategy for Dice Chess.

Greedy bot strategy for Dice Chess.

Evaluates all legal full-turn paths using TurnGenerator and selects the one with the highest material score from Evaluator.evaluateMaterial. Among paths with equal scores, it prefers shorter winning paths (to end the game sooner) and breaks any remaining ties uniformly at random.

This is the primary default bot used by the dicechess.engine.api.JsApi (a separate sbt project, not linkable from here) and serves as the baseline for future expectimax-based search improvements.

Attributes

Source
GreedySearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Greedy bot strategy for Dice Chess with King Safety Heuristics.

Greedy bot strategy for Dice Chess with King Safety Heuristics.

Evaluates all legal full-turn paths using TurnGenerator and selects the one with the highest score from Evaluator.evaluate. It penalizes paths that leave its own king exposed to immediate capture.

Attributes

Source
GreedySearchV2.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object KcpFeatures

RichFeatures plus the king/queen capture probabilities — the feature that encodes the win condition of Dice Chess (capturing the king) directly, rather than through a material or activity proxy.

RichFeatures plus the king/queen capture probabilities — the feature that encodes the win condition of Dice Chess (capturing the king) directly, rather than through a material or activity proxy.

Each probability is integrated over the opponent's 216 possible dice rolls (see KingCaptureProbability), so like the rest of the set it is mover-perspective and dice-independent — it describes the next roll, not the current pool — matching the dice-free leaf the search evaluates. Kept separate from RichFeatures on purpose, so the cheap set the 2-ply search uses stays unchanged.

Attributes

Note

Cost: every capture probability is a 216-outcome DFS (millisecond-scale), far heavier than RichFeatures' two move-generation passes. This set targets training enrichment and one-ply inference only — a 2-ply search's hundreds of thousands of leaves per move would make it thousands of seconds per move.

Source
KcpFeatures.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Versioned, dice-independent 27-column feature contract (kcp-mobility-27-v1).

Versioned, dice-independent 27-column feature contract (kcp-mobility-27-v1).

Keeps KcpFeatures.extract as an unchanged 13-column prefix, followed by pseudo-legal move counts per moving piece type for the evaluation color (own_moves_p through own_moves_k), the same six for the opponent (opp_moves_p through opp_moves_k), and normalized piece diversity indices (own_pdi, opp_pdi).

All features are evaluated from the explicit perspective of color, independent of active color and dice.

Attributes

Source
KcpMobilityFeatures.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Versioned, dice-independent 31-column feature contract (kcp-mobility-pawns-31-v1).

Versioned, dice-independent 31-column feature contract (kcp-mobility-pawns-31-v1).

Keeps KcpMobilityFeatures.extract as an unchanged 27-column prefix, followed by passed-pawn counts (own_passed_pawns, opp_passed_pawns) and the rank of the most advanced passed pawn (own_passed_max_rank, opp_passed_max_rank), counted from that side's own back rank as 0 (so 1–6 for a pawn and 0 when none exists).

All features are evaluated from the explicit perspective of color, independent of active color and dice.

Attributes

Source
KcpMobilityPawnsFeatures.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Attributes

Source
KcpScratchBoard.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Probabilistic king (and queen) capture analysis for Dice Chess.

Probabilistic king (and queen) capture analysis for Dice Chess.

Given a position, enumerates all 216 possible 3d6 outcomes (grouped into 56 weighted multisets) and determines whether the opponent can capture the defender's king (or queen) on their next turn.

King-capture paths are always legal regardless of the Maximum Micro-moves Rule, so a depth-first search with early exit yields exact probabilities. Queen-capture paths respect the Maximum Micro-moves Rule; the returned probability may therefore be slightly overestimated in edge cases where a queen capture is not part of any max-length sequence.

Attributes

Source
KingCaptureProbability.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object KingThreat

Answers which rolls let the side to move capture the king on a target square within up to N actions (N = 1, 2, or 3), computed as the total weight of the matching rolls out of DiceRolls.totalOrderedRolls (216).

Answers which rolls let the side to move capture the king on a target square within up to N actions (N = 1, 2, or 3), computed as the total weight of the matching rolls out of DiceRolls.totalOrderedRolls (216).

Unlike roll-by-roll search, this explores paths across all rolls in a single traversal from the position before the roll:

  • Without dice in the position, pseudo-legal moves for every piece type are considered;
  • Each path tracking towards the target square notes the dice it spends (castling spends King and Rook, a normal move spends its mover piece's die);
  • Roll coverage is tracked as a 56-bit bitmask over the 56 distinct rolls in DiceRolls.weighted;
  • Branches whose possible dice combinations are already fully covered by shorter or existing paths are pruned;
  • Transpositions by (board Zobrist hash, spent dice key) are detected and skipped;
  • Terminal captures at maximum depth use direct attacker bitboards without allocating full move lists;
  • Once all 56 rolls are covered, the search terminates immediately with the maximum weight 216.

Attributes

Source
KingThreat.scala
Supertypes
class Object
trait Matchable
class Any
Self type
KingThreat.type

The centipawn scale every engine-side evaluation and safety metric is priced in (#510).

The centipawn scale every engine-side evaluation and safety metric is priced in (#510).

The values are the classical relative piece values scaled by 100, so a pawn is the unit and integer arithmetic carries fractional-pawn precision without floating point on a hot path.

'''Why this is a shared object and not a constant on Evaluator.''' Two consumers must agree on it by construction, not by convention: Evaluator prices positions, and PieceSafety prices the material a side has en prise — the arena's hang telemetry. Those two numbers are compared against each other in practice ("did the safety signal reduce the blunders the telemetry counts"), which only means something while both use one scale. PieceSafety is the lower-level primitive of the pair, so having it reach into the evaluator for a rook's price would invert the layering; both depend on this instead.

'''The king is priced, unlike in ordinary chess evaluators.''' Dice Chess is won by actually capturing the king rather than by mate, so a king is a capturable piece whose loss ends the game — Evaluator.evaluateMaterial gives it a value far above the rest of the army so no material gain can outweigh losing it. PieceSafety never needs it: an attacked king is check, a separate concept with its own machinery, and hangingSquares excludes kings by construction.

Attributes

Note

Deliberately NOT the scale used by the ONNX feature extractors. OnnxFeatures prices pieces at 1/3/3/5/9, because that vector is the frozen input contract of a trained model; changing its unit would invalidate the deployed weights. The two scales are proportional for the non-king pieces and are meant to stay independent.

Source
MaterialValues.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class MonteCarloConfig(rollouts: Int = ..., maxPlies: Int = ..., targetError: Double = ..., minRollouts: Int = ...)

Tuning knobs for MonteCarloEquity.estimate.

Tuning knobs for MonteCarloEquity.estimate.

Value parameters

maxPlies

ply horizon for a single rollout; survival mass still alive at the horizon is reported as undecided.

minRollouts

minimum rollouts before adaptive stopping may trigger; ignored when targetError is 0.

rollouts

hard cap on the number of rollouts (also the exact count when targetError is 0).

targetError

when > 0, stop early once the White-win standard error drops to or below this value (checked after at least minRollouts rollouts). 0 disables adaptive stopping and runs exactly rollouts rollouts.

Attributes

Source
MonteCarloEquity.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Rao-Blackwellized Monte-Carlo pre-roll equity estimator.

Rao-Blackwellized Monte-Carlo pre-roll equity estimator.

For a position with too few games in the database to read an empirical win-rate, this estimates P(White win) / P(Black win) / P(undecided) on demand. It is the Scala port of the C++ reference (https://github.com/rabestro/dice-chess-engine).

Algorithm

Each rollout walks a random game and, at every ply, integrates the exact per-ply king-capture probability p = wins / 216 from KingCaptureProbability instead of sampling a 0/1 win:

survive = 1
for each ply (side S to move):
 p = P(S captures the opponent king on this roll)   // exact, over all 216 rolls
 winsOf(S) += survive * p
 survive   *= (1 - p)
 advance to a random surviving continuation          // a sampled non-capturing turn

Because the win mass at each node is added analytically (Rao-Blackwellization) rather than as a Bernoulli sample, the estimator has far lower variance than vanilla 0/1 Monte-Carlo — EquityEstimate.varianceReductionVsVanilla reports the measured ratio.

Deviation from the reference

The reference advances the rollout to any sampled legal turn, including king-captures. A king capture is terminal, so following it produces a king-less board that is then (incorrectly) played on, a small second-order bias. As the rules source of truth this estimator instead conditions the continuation on survival: it advances only through turns that do not capture a king (the event with probability 1 - p that survive already tracks). The analytic per-ply terms — and therefore the variance self-check — are unaffected.

Attributes

Source
MonteCarloEquity.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Monte-Carlo search bot (difficulty 7).

Monte-Carlo search bot (difficulty 7).

Ranks every legal full-turn path by the Rao-Blackwellized Monte-Carlo win probability of the resulting position (MonteCarloEquity) and plays the highest-scoring one; an immediate king capture is always preferred. Where the primitive bots score only a single ply of heuristics, this estimates the full-game win probability via rollouts, so it accounts for deeper consequences.

Per-move cost scales with the number of legal turns × the rollout budget, so DefaultConfig uses a modest budget. Tests and benchmarks pass an explicit MonteCarloConfig and scala.util.Random through findBestMove(state:dicechess\.engine\.domain\.GameState,config:dicechess\.engine\.search\.MonteCarloConfig,random:scala\.util\.Random)* for reproducible, faster, or stronger play.

Attributes

Source
MonteCarloSearch.scala
Supertypes
class Object
trait Matchable
class Any
Show all
Self type
class OnnxEvalSearch(modelPath: String, extractFeatures: (GameState, Color) => Array[Float] = ..., clock: () => Long = ...) extends TimeBudgetedSearch, AutoCloseable

One-ply search scored by an externally-trained model (LightGBM, exported to ONNX) instead of a hand-tuned heuristic. Structurally identical to GreedySearch/AggressiveSearch — the only difference is the evalFn handed to SearchScoring.scorePath.

One-ply search scored by an externally-trained model (LightGBM, exported to ONNX) instead of a hand-tuned heuristic. Structurally identical to GreedySearch/AggressiveSearch — the only difference is the evalFn handed to SearchScoring.scorePath.

The ONNX session is created once per instance (session creation, not inference, is the expensive part) and is not thread-safe to call concurrently from multiple threads without external synchronization — matches every other bot in this codebase, none of which claim thread safety either.

As a TimeBudgetedSearch it honours a wall-clock deadline by scoring candidates through the model in OnnxEvalSearch.BatchSize-sized chunks, checking the clock between chunks rather than only across the whole candidate set — see findBestMove(state:dicechess\.engine\.domain\.GameState,deadlineNanos:Long,random:scala\.util\.Random)*.

Value parameters

clock

nanosecond time source (defaults to java.lang.System.nanoTime); injectable for deterministic deadline testing

extractFeatures

feature extraction function converting a position and mover perspective to an input row for the model

modelPath

filesystem path to the ONNX model file

Attributes

Companion
object
Source
OnnxEvalSearch.scala
Supertypes
trait AutoCloseable
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Source
OnnxEvalSearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final class OnnxExpectimaxSearch extends TimeBudgetedSearch, AutoCloseable

A configurable two- or three-ply expectimax bot whose leaf evaluator is an externally-trained model (LightGBM, via ONNX).

A configurable two- or three-ply expectimax bot whose leaf evaluator is an externally-trained model (LightGBM, via ONNX).

ExpectimaxSearch supplies the lookahead and OnnxEvalSearch supplies the value function, evaluated in batches at leaf decision nodes. The default two-ply tree sees the opponent's reply; searchDepth = 3 also sees our next rolled reply and therefore resolves exchanges that otherwise end on the opponent's capture.

A positive-weight rootRescore wires a second ONNX session as ExpectimaxSearch's root rescorer — see RootRescoreModel. A zero-weight configuration is treated exactly like None, including session ownership.

preRankWithModel, when true, uses this bot's own already-loaded model (batched) to pre-rank root candidates instead of material — no second session, since the model already scoring the chance-node leaves is exactly the "opinion" candidate selection should defer to. See ExpectimaxSearch's preRank parameter for why: widening candidateLimit only compensates for a crude (material) pre-ranker; a sharper one attacks the actual bottleneck.

preRankModel wires a dedicated session for candidate ordering instead — see PreRankModel for why ranking and valuing are different jobs, and why its cost is bounded by the feature schema rather than by the candidate limit. It is an alternative to preRankWithModel, not a companion: configuring both is rejected.

chanceCollapse wires a model in place of the chance-node expansion itself — the layer that costs hundreds to thousands of leaf evaluations per candidate. It is a third, independent session, and with it searchDepth, the transposition table and Star pruning have nothing left to do at the root: see ChanceCollapse.

statsSink is forwarded to the underlying ExpectimaxSearch — one RootSearchStats per move, so a production host can log how many candidates its deadline really allowed (the difference between the configured limit and the width actually searched on slow hardware).

Owns its ONNX session(s) — the main model's, the rescorer's when configured with positive weight, and the chance-collapse model's when configured; call close when done. Not safe for concurrent calls, matching every other bot here.

Attributes

Companion
object
Source
OnnxExpectimaxSearch.scala
Supertypes
trait AutoCloseable
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Source
OnnxExpectimaxSearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object OnnxFeatures

Material-only feature extraction, byte-for-byte matching the private training pipeline's features.py::material_features (with include_dice=False) — the model this feeds expects exactly these 7 features, in this exact order, computed the same way.

Material-only feature extraction, byte-for-byte matching the private training pipeline's features.py::material_features (with include_dice=False) — the model this feeds expects exactly these 7 features, in this exact order, computed the same way.

Deliberately does NOT reuse Evaluator.scoreBitboard's point values (100/300/300/500/900): that scale belongs to the engine's own hand-tuned heuristic, not to the externally-trained model, which was fit on the raw 1/3/3/5/9 values below. Reusing the bitboard-intersection idiom is fine and intentional; reusing the point values would silently retrain nothing while changing what every input means.

Attributes

Source
OnnxFeatures.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class OnnxSearchOptions(statsSink: RootSearchStats => Unit = ..., rootRescore: Option[RootRescoreModel] = ..., preRankWithModel: Boolean = ..., tt: Option[TranspositionTable] = ..., chanceCollapse: Option[CollapseModel] = ..., preRankModel: Option[PreRankModel] = ..., leafContract: Option[ModelManifest] = ...)

Search-tuning options for ONNX-backed expectimax search.

Search-tuning options for ONNX-backed expectimax search.

Value parameters

chanceCollapse

optional model replacing each root candidate's exact chance-node expansion

leafContract

the leaf model's manifest, when it came from a validated package. The hook models carry their own; the leaf's lives here because OnnxExpectimaxSearch's constructor takes the leaf as a bare path and adding a tenth positional parameter to it would be the wrong place to put a rarely-set value (#145)

preRankModel

optional dedicated model for ordering root candidates, in its own session

preRankWithModel

whether to pre-rank root candidates using the main model instead of material

rootRescore

optional second ONNX model used to rescore root candidate moves

statsSink

sink function receiving root search statistics per decision node

tt

optional transposition table for caching evaluated search states

Attributes

Source
OnnxExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object OpeningBook

The opening-book key contract shared between the engine (the consumer, via OpeningBookBot) and the analytics exporter (the producer, dicechess-analytics).

The opening-book key contract shared between the engine (the consumer, via OpeningBookBot) and the analytics exporter (the producer, dicechess-analytics).

A key identifies a position together with its dice roll and is built as:

<piece-placement> <active-color> <castling> <en-passant> <dice>
  • The first four fields are the canonical normalized FEN from dicechess.engine.movegen.Dfen.normalizedFen — byte-for-byte the analytics normalized_fen. The half-move and full-move clocks are deliberately omitted: an opening book is independent of the move counters, and dropping them lets concrete positions that differ only by clock share a single book entry. The en-passant field is canonicalised (kept only when actually capturable) so positions that differ only by an uncapturable en-passant target — common when a Dice Chess turn double-pushes mid-turn — share one entry.
  • `` is the rolled piece letters (p n b r q k), sorted ascending and cased by the side to move — upper-case for White, lower-case for Black. This is identical to the analytics dice_sorted column. Example for White rolling bishop + pawn + rook: BPR.

The value mapped to a key is the chosen continuation as a comma-separated list of long-algebraic micro-moves (e.g. "e2e4,f1c4", with a promotion suffix such as "e7e8q" where applicable). OpeningBookBot matches it against the legal turn paths by move multiset, so the stored order is not significant.

Example key: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - BPR

Any change to this format is a cross-repository breaking change and must be mirrored in the analytics exporter and its round-trip test.

Attributes

Source
OpeningBook.scala
Supertypes
class Object
trait Matchable
class Any
Self type
class OpeningBookBot(val underlying: SearchAlgorithm, val book: Map[String, String]) extends SearchAlgorithm

A decorator that consults an opening book before falling back to the wrapped algorithm.

A decorator that consults an opening book before falling back to the wrapped algorithm.

The book maps a canonical key (see OpeningBook.key) to the chosen continuation, a comma-separated list of long-algebraic micro-moves (e.g. "e2e4,f1c4"). On a book-key hit, if any legal turn path captures the enemy king, the decorator plays the shortest such immediate win path instead of the booked continuation. Otherwise, when the key is present, it plays the booked turn instantly — without spending the underlying bot's search budget — which is exactly where a heuristic or rollout-based engine is weakest and where empirical data is richest. In summary, the precedence order is capture > book > underlying. When the position's key is absent from book, it delegates to underlying.

The booked moves are matched against the position's legal turn paths by move multiset, so the order stored in the book does not matter and an entry that cannot be realised legally (a stale or corrupt book) is silently ignored in favour of the underlying bot rather than played illegally.

Construct via OpeningBookBot.decorate, which preserves the underlying bot's time-budget capability: wrapping a TimeBudgetedSearch yields a TimeBudgetedOpeningBookBot (the deadline reaches the underlying), while wrapping a plain bot yields a plain decorator — so a host's getBestMove does not advertise a time budget for a bot that ignores it.

The decorator delegates all doubling and draw policy queries (shouldOfferDouble, shouldAcceptDouble, shouldOfferDraw, shouldAcceptDraw) unconditionally to underlying, preserving the wrapped bot's custom policies, DrawOfferLogic mix-ins, and default SearchAlgorithm heuristics.

Value parameters

book

the opening book, keyed per OpeningBook.key

underlying

the bot to consult when the position is not booked

Attributes

Companion
object
Source
OpeningBookBot.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes

Attributes

Companion
class
Source
OpeningBookBot.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Validates and parses opening-book TSV data, rejecting malformed entries before bot registration.

Validates and parses opening-book TSV data, rejecting malformed entries before bot registration.

Expected TSV format (canonical OpeningBook.key mapped to comma-separated continuations):

rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - BPR	e2e4,f1c4

Attributes

Source
OpeningBookParser.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object PassedPawns

Shared-core primitive for detecting and evaluating passed pawns for an explicit perspective color.

Shared-core primitive for detecting and evaluating passed pawns for an explicit perspective color.

Uses the standard chess definition: a pawn is passed if there are no opposing pawns on the same file or on any adjacent file on any square ahead of the pawn along its direction of advance.

The evaluation perspective is the explicit color argument, independent of state.activeColor and the dice pool.

Attributes

Source
PassedPawns.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Unweighted Piece Diversity Index (PDI): presence of Pawn, Knight, Bishop, Rook, and Queen for one side.

Unweighted Piece Diversity Index (PDI): presence of Pawn, Knight, Bishop, Rook, and Queen for one side.

Each represented type contributes one, regardless of its number of pieces or their squares. Kings are excluded. This measures material diversity, not mobility or the number of dice a side can use: a present type may be blocked, and promotion can restore an absent type. The index is independent of the active color and dice pool.

Attributes

Source
PieceDiversity.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object PieceMobility

Shared-core primitive computing per-piece-type pseudo-legal move counts for an explicit perspective color.

Shared-core primitive computing per-piece-type pseudo-legal move counts for an explicit perspective color.

Moves are computed on state.withActiveColor(color) with castling dice present (King and Rook), exactly matching the move-generation operands of RichFeatures (mobility_diff). The move counts follow dice-value order: Pawn (1), Knight (2), Bishop (3), Rook (4), Queen (5), and King (6).

The sum over all six piece types equals the total pseudo-legal move count for color, preserving the invariant: sum(own_moves_*) - sum(opp_moves_*) == mobility_diff across every board position.

Attributes

Source
PieceMobility.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object PieceSafety

Boolean piece-safety primitives: which pieces stand en prise (attacked and undefended).

Boolean piece-safety primitives: which pieces stand en prise (attacked and undefended).

This is the building block behind the hang telemetry in the arena (dicechess.engine.bench.BotMatchRunner, a separate sbt project not linkable from here) and is deliberately shared so a safety-aware pre-ranker or feature extractor can later reuse the exact same notion of "hanging" — a metric and the signal it motivates must agree on definitions, or the metric can't validate the fix.

The definition is the deterministic-chess one, kept intentionally simple:

  • attack and defense are boolean (dicechess.engine.movegen.MoveGenerator.isSquareAttacked), with no exchange evaluation — a piece defended once but attacked twice does not count as hanging;
  • a king "defends" its neighbours like any other piece, even when recapturing would be suicidal against a defended attacker;
  • en passant is not modeled (the capture lands on a square the victim does not occupy);
  • kings are never "hanging" — an attacked king is check, a different concept with its own machinery (Evaluator.evaluateKingSafety, KingCaptureProbability).

Dice probabilities are also deliberately absent: whether the opponent can roll the attacker's die is a separate, composable concern (the chance of punishing a hang is 1 - ((6 - k) / 6)^3 for k distinct attacking piece types), and keeping this primitive dice-free lets one implementation serve both a raw blunder count and a dice-weighted risk feature.

Attributes

Source
PieceSafety.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object PlayableDice

The dice of a rolled position that a legal turn can still spend (#293).

The dice of a rolled position that a legal turn can still spend (#293).

A client that draws the dice dims a die once no legal turn can use it. A die is playable while at least one legal turn that begins with the actions already played spends it after them. In the start position with the dice queen, rook and knight, only a knight can move first; after b1a3 the rook can go a1b1, and every legal turn is "knight, then rook". The knight and rook dice are therefore playable at the roll and only the queen die is not. A die that is not playable stays so for the rest of the turn, since each action played only narrows the turns that can follow.

It is built on TurnGenerator.forEachLegalTurnPath and changes nothing in it. Every legal turn is replayed with the engine's own makeMove and diceAfter, and the dice each action spends are the pool before it less the pool it leaves, so castling spends the king and the rook die with no rule of its own here.

Attributes

Source
PlayableDice.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class PreRankModel(modelPath: String, extractFeatures: (GameState, Color) => Array[Float], chunkSize: Int = ..., contract: Option[ModelManifest] = ...)

A model whose only job is to order the mover's legal turns, so the search expands the best ExpectimaxConfig.candidateLimit of them rather than the best the material proxy could find.

A model whose only job is to order the mover's legal turns, so the search expands the best ExpectimaxConfig.candidateLimit of them rather than the best the material proxy could find.

It gets its own session on purpose. Ordering candidates and valuing positions are different jobs: a ranker is trained on which turn is better, a value model on how good a position is, and the two have different targets, can use different feature schemas, and are not interchangeable just because both emit one number per row. Reusing the leaf model for ranking is the cheaper option this repository already offers — preRankWithModel — and the two are alternatives, not a pair.

What bounds its cost is the feature schema, not this type. Pre-ranking sees every legal turn, which in Dice Chess is routinely hundreds and can be thousands; at that width material-7-v1 is a thousand cheap rows while kcp-13 is a thousand 216-outcome capture-probability searches, which is not viable at this seam at all. The pre-rank pass is also paid in full before the deadline is consulted — it has to be, since its output is what the anytime fallback plays — so a schema too expensive for the position's candidate count overruns the move budget before the search proper begins.

Value parameters

chunkSize

rows per session run, forwarded to OnnxEvalSearch.onnxEvalBatchChunked. A bound, not a tuned value: it keeps one [rows × F] tensor from growing with the position's branching, and the throughput-optimal value depends on the model and belongs in the host's configuration.

extractFeatures

this model's own feature extractor

modelPath

path to the pre-ranking model, independent of the main model

Attributes

Companion
object
Source
OnnxExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object PreRankModel

Attributes

Companion
class
Source
OnnxExpectimaxSearch.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type

Random bot that never declines a king it can take at once.

Random bot that never declines a king it can take at once.

It plays like RandomSearch, a uniformly random legal full-turn path, with one exception: when the very first micro-move of some path captures the opponent's King (the King is under direct attack from a piece the roll lets it move), it plays one of those paths, chosen uniformly at random.

The look is deliberately one micro-move deep. A king capture that needs a preparatory micro-move (a pawn clearing a file, a second knight jump) is not looked for, and the bot does not protect its own King. That keeps it between RandomSearch and CheckmateAwareSearch, which finds multi-move captures and also keeps its King safe.

Attributes

Source
RandomKingCaptureSearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object RandomSearch extends SearchAlgorithm

Random bot strategy for Dice Chess.

Random bot strategy for Dice Chess.

Selects a uniformly random legal full-turn path from all candidates produced by TurnGenerator. This implementation is primarily intended for:

  • Testing — provides an unpredictable opponent without evaluation bias.
  • Fairness baselines — useful for measuring improvement over pure chance.
  • Move diversity — can be used in training pipelines to generate varied game states.

Uses a module-level scala.util.Random instance that is not seeded, so results are non-deterministic across runs. For reproducible play, prefer dice, rand) with an explicit seed.

Attributes

Source
RandomSearch.scala
Supertypes
class Object
trait Matchable
class Any
Self type

The raw board as 12 binary planes of 64 squares — no hand-crafted features at all.

The raw board as 12 binary planes of 64 squares — no hand-crafted features at all.

This is the input contract of the private training pipeline's raw-board value net, whose premise is that a small MLP over the bare board has capacity a 9-feature GBDT lacks. Where OnnxFeatures / RichFeatures / KcpFeatures hand the model a summary of the position, this hands it the position.

Layout, which the Python side (board.py::encode_fen) must match bit for bit:

  • planes 0..5 are the '''mover's''' Pawn, Knight, Bishop, Rook, Queen, King, in dicechess.engine.domain.PieceType order;
  • planes 6..11 are the opponent's, same order;
  • within a plane the index is the dicechess.engine.domain.Square index seen from the mover (0 = the mover's own a1 corner), so for Black the ranks are mirrored (rank 1 rank 8) while files are left alone;
  • a square holding a piece is 1.0f, everything else 0.0f.

Both halves of that convention — own-pieces-first and rank mirroring — make the encoding mover-canonical: side to move stops being an input dimension, exactly as the own minus opponent convention does for the material features. The starting array is symmetric under this transform, so both colors encode it identically.

Unlike RichFeatures this needs '''no move generation'''. That does '''not''' make it cheaper, which is worth stating plainly because the opposite was assumed when this was written: extraction gets cheaper, but the model behind it does not. Measured on the one-ply arena over the same 400/1600-game harness, a raw-board net costs ~0.195 s/game against ~0.0375 s/game for a rich(9) GBDT — '''~5x more per evaluation''', because a 768-wide input through a 256x32 MLP outweighs the two generateAllMoves passes it saves. A 2-ply run at candidateLimit = 24 corroborates it: rich finishes 400 games in ~25 minutes, this did not finish in two hours.

So the reason to use this set is '''strength per evaluation''', not throughput: at one ply it scores 57.3% against the aggressive baseline where rich(9) scores 48.0%, while costing 1/253 of the kcp teacher's 60.8%. Anything that budgets leaf evaluations — a deeper search, or a bot under a clock — has to weigh that 5x.

Attributes

Note

Dice-free by construction, like every other feature set here: it scores an afterstate whose next roll is unknown.

Source
RawBoardFeatures.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object RichFeatures

The material feature set (OnnxFeatures) plus cheap, dice-free positional features — the signal a material-only model is blind to (it cannot tell two positions of equal material apart).

The material feature set (OnnxFeatures) plus cheap, dice-free positional features — the signal a material-only model is blind to (it cannot tell two positions of equal material apart).

Kept separate from OnnxFeatures on purpose: that 7-feature set is the input contract of the currently deployed model, so its shape must not change. RichFeatures reuses it for the material block and appends the positional columns, giving a distinct input for a model trained on the richer set. Computing features here — in the engine, the single source of truth for move generation — means training-data enrichment and inference share this exact code, with no Python/Scala re-implementation drift.

All features are from color's own perspective (own minus opponent) and independent of the dice pool, matching the dice-free leaf the search evaluates.

Attributes

Note

Cost: mobility_diff runs two full dicechess.engine.movegen.MoveGenerator.generateAllMoves passes, so this set is not cheap enough to evaluate on every leaf of a 2-ply search (hundreds of thousands per move). It targets training enrichment and one-ply / shallow inference — the "one-ply + rich features vs. two-ply + material" comparison.

Source
RichFeatures.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Versioned, dice-independent input for PDI ablations. The rich-9 prefix is unchanged; the two absolute class counts are normalized by five and ordered by the explicit evaluation perspective, not active color. Presence is not mobility or expected usable rolls. Training enrichment and ONNX inference use this extractor.

Versioned, dice-independent input for PDI ablations. The rich-9 prefix is unchanged; the two absolute class counts are normalized by five and ordered by the explicit evaluation perspective, not active color. Presence is not mobility or expected usable rolls. Training enrichment and ONNX inference use this extractor.

Attributes

Source
RichPdiFeatures.scala
Supertypes
class Object
trait Matchable
class Any
Self type
final case class RootRescore(evalBatch: (Array[GameState], Color) => Array[Int], weight: Double)

Root-level rescoring: after the search's own chance-node expectation is computed for each root candidate, blend it with a second, cheaper-at-the-root evaluator run once on the candidates' own resulting positions (before the opponent's roll) — finalScore = (1 - weight) * searchValue + weight * rescoreValue.

Root-level rescoring: after the search's own chance-node expectation is computed for each root candidate, blend it with a second, cheaper-at-the-root evaluator run once on the candidates' own resulting positions (before the opponent's roll) — finalScore = (1 - weight) * searchValue + weight * rescoreValue.

Meant for a tactically sharp but leaf-prohibitive evaluator: e.g. king/queen capture-probability features cost a 216-outcome DFS each, far too expensive under a chance node's hundreds of leaves, but cheap enough for the handful of root candidates (ExpectimaxConfig.candidateLimit).

A candidate where at least one dice roll loses our king outright (ExpectimaxSearch.LossValue) is never rescored, at any weight — that sentinel must always rank last regardless of what the rescorer thinks of the resulting position (see the root loop's blendedRootValue).

Value parameters

evalBatch

the rescoring evaluator, same batching contract as the search's own evalBatch

weight

blend weight; must be in [0, 1]. Zero is deliberately accepted as an operationally safe way to disable a configured rescorer: it is exactly equivalent to None and does not invoke evalBatch.

Attributes

Source
ExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class RootRescoreModel(modelPath: String, extractFeatures: (GameState, Color) => Array[Float], weight: Double, contract: Option[ModelManifest] = ...)

A second ONNX model that rescores ExpectimaxSearch's root candidates (see RootRescore) rather than evaluating chance-node leaves — a tactically sharp but leaf-prohibitive model (e.g. trained on dicechess.engine.search.KcpFeatures) that only the handful of root candidates can afford.

A second ONNX model that rescores ExpectimaxSearch's root candidates (see RootRescore) rather than evaluating chance-node leaves — a tactically sharp but leaf-prohibitive model (e.g. trained on dicechess.engine.search.KcpFeatures) that only the handful of root candidates can afford.

Value parameters

extractFeatures

the rescoring model's own feature extractor — need not match the main model's

modelPath

path to the rescoring model, independent of the main model

weight

blend weight, forwarded to RootRescore ([0, 1]). Zero disables rescoring before a second ONNX session is created.

Attributes

Companion
object
Source
OnnxExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Source
OnnxExpectimaxSearch.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class RootSearchStats(legalTurns: Int, candidatesSelected: Int, candidatesCompleted: Int, candidatesAbandoned: Int = ..., cutoffs: Int = ..., rollsSaved: Int = ..., probeCutoffs: Int = ..., ttProbes: Int = ..., ttHits: Int = ..., ttCutoffs: Int = ..., candidatesCollapsed: Int = ..., collapseRejected: Boolean = ...)

One move's root telemetry from ExpectimaxSearch: how wide the decision node really was.

One move's root telemetry from ExpectimaxSearch: how wide the decision node really was.

candidatesCompleted &lt; candidatesSelected means the wall-clock deadline truncated the anytime loop — the move was chosen among fewer candidates than ExpectimaxConfig.candidateLimit allowed. Persistently tiny completion counts on production hardware mean the effective search degenerates toward "play the pre-ranker's first pick", which no amount of candidate-limit tuning can fix — that evidence is exactly what this type exists to surface.

An immediate king-capture win reports 0/0 selected/completed: no candidate was pre-ranked or expanded, so counting it as a completed candidate would overstate the searched width. A forced pass (no legal turn for the roll) reports all-zero — legalTurns == 0 is what tells the two apart, keeping the sink's contract at exactly one record per findBestMove call.

Value parameters

candidatesAbandoned

candidates whose chance node was cut mid-expansion by the deadline (at most one: the search stops afterwards). Their partial expectation is discarded rather than ranked, so this counts work paid for and thrown away — a persistent non-zero value means the per-move budget is too small for even one candidate at this width.

candidatesCompleted

how many selected candidates were fully expanded through the chance node. Ranking needs comparable values, and a full expansion is one way to get one; a transposition-table hit (RootSearchStats.ttHits) and a collapsed candidate (RootSearchStats.candidatesCollapsed) are the other two, and are counted separately because they did no expansion work

candidatesSelected

how many turns survived pre-ranking (at most the candidate limit)

legalTurns

size of the full legal-turn list before pre-ranking

Attributes

Source
ExpectimaxSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
case class ScoredSequence(moves: List[Move], score: Int)

The scored result of a full-turn path evaluation.

The scored result of a full-turn path evaluation.

Value parameters

moves

the sequence of 1–3 micro-moves that make up the turn; never empty for a valid result

score

material score from the perspective of the side that played the turn. Use SearchScoring.TerminalWinScore to signal a King capture (win condition).

Attributes

Source
SearchAlgorithm.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Contract for bot strategies in the Dice Chess Engine.

Contract for bot strategies in the Dice Chess Engine.

Implementations receive the current dicechess.engine.domain.GameState and the multiset of available dice rolls, then return the chosen full-turn path wrapped in a ScoredSequence, or None when the position has no legal moves (the active player must pass).

Implementations are expected to be thread-safe singletons (e.g., Scala object).

==Resignation== No shouldResign hook exists on SearchAlgorithm by design. Resignation is an operational decision handled by the bot runtime / host application layer rather than the search strategy.

Attributes

Source
SearchAlgorithm.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
object SearchScoring

Shared scoring utilities used by all SearchAlgorithm implementations.

Shared scoring utilities used by all SearchAlgorithm implementations.

Centralises the terminal-win sentinel and the path-scoring logic so that every strategy applies them consistently.

Attributes

Source
SearchAlgorithm.scala
Supertypes
class Object
trait Matchable
class Any
Self type
enum TTBound

Attributes

Source
TranspositionTable.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class TTEntry(key: Long, value: Double, bound: TTBound, depth: Int, lossTainted: Boolean)

Attributes

Source
TranspositionTable.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class TacticianConfig(candidateLimit: Int, kingThreatCp: Int, kingDangerCp: Int, hangingWeight: Double)

Explicit experimental settings; hosts supply their own measured profile. No built-in bot is replaced.

Explicit experimental settings; hosts supply their own measured profile. No built-in bot is replaced.

Attributes

Source
TacticianSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final class TacticianSearch(val config: TacticianConfig, statsSink: TacticianStats => Unit = ...) extends SearchAlgorithm

Experimental full-turn search with cheap ordering and exact roll-coverage king threats on a bounded shortlist.

Experimental full-turn search with cheap ordering and exact roll-coverage king threats on a bounded shortlist.

Every candidate comes from the canonical maximal-dice generator. Immediate king captures bypass evaluation. Different paths to the same complete post-turn state occupy one shortlist slot; this intentionally changes tie multiplicities relative to path-based bots. The first representative is retained, making seeded ties reproducible.

Aggressive evaluation orders the states. Rescoring replaces its boolean king-exposure penalty with roll-weighted danger, adds hypothetical attacking potential, and prices hanging material using the public PieceSafety primitive. Attacking potential assumes the board is unchanged before our next roll; it does not model an intervening reply.

The work limit bounds expensive candidate queries, not total generation or wall time. This synchronous search makes no UI responsiveness guarantee and is not registered as a default opponent.

Attributes

Source
TacticianSearch.scala
Supertypes
class Object
trait Matchable
class Any
final case class TacticianStats(legalPaths: Int, distinctStates: Int, rescoredStates: Int, threatQueries: Int)

Per-call work counts. Unique states include flags, clocks, mailbox and en-passant rights.

Per-call work counts. Unique states include flags, clocks, mailbox and en-passant rights.

Attributes

Source
TacticianSearch.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class TimeBudget(targetMs: Long, hardCapMs: Long)

The result of TimeManager.budget: the ideal per-turn target and the hard ceiling it was clamped to.

The result of TimeManager.budget: the ideal per-turn target and the hard ceiling it was clamped to.

Value parameters

hardCapMs

the absolute per-turn ceiling (a fraction of the remaining clock) — the bot must never aim above this, so a single turn can never flag the clock

targetMs

the time the bot should aim to spend on this turn

Attributes

Source
TimeManager.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final class TimeBudgetedOpeningBookBot(tbUnderlying: SearchAlgorithm & TimeBudgetedSearch, book: Map[String, String]) extends OpeningBookBot, TimeBudgetedSearch

The time-budgeted OpeningBookBot, used when the wrapped bot is itself a TimeBudgetedSearch so the deadline is forwarded on a book miss. Built by OpeningBookBot.decorate.

The time-budgeted OpeningBookBot, used when the wrapped bot is itself a TimeBudgetedSearch so the deadline is forwarded on a book miss. Built by OpeningBookBot.decorate.

Attributes

Source
OpeningBookBot.scala
Supertypes
class Object
trait Matchable
class Any
Show all

Capability mix-in for a SearchAlgorithm that can search under a wall-clock deadline.

Capability mix-in for a SearchAlgorithm that can search under a wall-clock deadline.

A plain SearchAlgorithm runs to its own internal budget (a fixed rollout or node count); a TimeBudgetedSearch additionally accepts an external java.lang.System.nanoTime deadline and is expected to stop at it, always returning a legal fallback move when one exists. This lets a caller on a game clock (e.g. the JS API's timeBudgetMs) bound per-move thinking time without knowing the algorithm's internals.

The deadline path is non-deterministic by design (it depends on machine speed), so it is meant for play rather than reproducible tests.

Granularity is part of the contract

A search can only stop where it yields, so "stops at the deadline" is really "stops at the first yield point after it". An implementation must therefore keep its unit of uninterruptible work well below a realistic per-move budget, or the contract holds only on paper. ExpectimaxSearch learned this the expensive way: it once checked the clock between root candidates only, and a single candidate at candidateLimit=24 on one core measured a 6.5 s median against a 1.9 s allocation — a 3.4x overshoot the caller had no way to prevent (#496). Prefer a yield point whose cost is a small fraction of the whole search, and guard the clock read so the untimed path pays nothing.

Attributes

Source
TimeBudgetedSearch.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
final class TimeManager(val policy: TimePolicy)

Pure, platform-agnostic time manager for the bots' game clock.

Pure, platform-agnostic time manager for the bots' game clock.

This is layer (a) of time control: it maps a ClockState to a per-turn budget in milliseconds. Layer (b) — the actual search under a deadline — stays untouched in TimeBudgetedSearch; the caller turns the budget returned here into a System.nanoTime deadline. Keeping the manager here (rather than in each consumer) means the JS API, the offline arena, and any future server share one tested implementation instead of re-inventing it.

The policy is pure Long/Double math — no clocks, no randomness — so it is identical on the JVM and Scala.js and is exhaustively unit-testable without wall-clock flakiness (see TimeBudgetedSearch for why the search path cannot be).

How the budget is derived

From the spendable time (remaining minus a safety reserve) and an estimated moves-to-go:

reserve = max(ReserveFloorMs, ReserveFraction * remaining)
spendable = max(0, remaining - reserve)
target = increment + spendable / movesToGo // increment == 0 ⇒ sudden death

then clamped to [MinThinkMs, hardCap] where hardCap = MaxFraction * remaining, with a tighter panic clamp once spendable falls below TimeManager.PanicThresholdMs.

Behaviour across controls

  • Sudden death (1+0): target ≈ spendable / movesToGo, decaying as the clock drains; near zero the bot blitzes legal moves (panic floor) instead of flagging.
  • Fischer (10+10): the increment term keeps per-turn time high while the clock is healthy. As remaining shrinks the hardCap (a fraction of the remaining clock) takes over and throttles the target below the increment — deliberately conservative, since the increment is only credited after the turn completes, so a bot that overspends now can still flag before it is refunded. This trades a little time-utilisation for a guarantee of never losing on time; the constants are tunable.

Value parameters

policy

the configurable moves-to-go policy; safety limits remain common to every policy

Attributes

Companion
object
Source
TimeManager.scala
Supertypes
class Object
trait Matchable
class Any
object TimeManager

Shared safety constants plus source-compatible access to the default time manager.

Shared safety constants plus source-compatible access to the default time manager.

Existing TimeManager.budget(...) callers continue to use default, while consumers that need an explicit policy construct an instance with TimeManager(policy).

Attributes

Companion
class
Source
TimeManager.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object TimePolicies

Built-in TimePolicy implementations and id-based lookup for external configuration.

Built-in TimePolicy implementations and id-based lookup for external configuration.

Attributes

Source
TimePolicy.scala
Supertypes
class Object
trait Matchable
class Any
Self type
trait TimePolicy

The variable part of TimeManager's clock allocation.

The variable part of TimeManager's clock allocation.

A policy estimates how many turns the moving side still has to budget for. TimeManager deliberately owns the safety envelope around that estimate — reserve, hard cap, panic clamp, and caller overhead — so every policy gets the same anti-flag guarantees.

Policies are pure and platform-independent. Consumers choose one when they construct a TimeManager; deployment configuration (environment variables, Worker bindings, command-line flags) belongs to the consumer rather than this shared engine module.

Attributes

Source
TimePolicy.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
object EmpiricalV1
object LegacyLinear
class TranspositionTable(capacityPowerOfTwo: Int = ...)

Fixed-size, thread-safe Transposition Table for expectimax search.

Fixed-size, thread-safe Transposition Table for expectimax search.

Stored in flat primitive arrays to ensure zero-GC pressure on hot search paths.

Concurrency contract — racy-reader safe, not general-purpose thread-safe: the classic lockless scheme (Hyatt). A checksum over ALL entry fields is written alongside each entry; a reader that observes a mixed or torn snapshot fails the checksum and reports a miss, so concurrent readers can lose entries but never observe a corrupt one (residual false-accept probability ~2⁻⁶⁴). There is no memory-ordering guarantee, so readers may also see stale — but internally consistent — entries. Single writer recommended until the parallel-search stage (#61) revisits publication with JVM fences.

Lifetime contract: entries encode only (position, evaluator) — for a FIXED evaluator an EXACT value never goes stale, across moves or games; that is why entries carry no generation field. The table is therefore bound to one evaluator: it is owned by the search instance that created it and must never be shared between models. Call clear when reusing an instance across games only if you want fresh telemetry or replacement pressure — not for correctness.

Value parameters

capacityPowerOfTwo

table capacity, must be a power of two (default 2^18 = 262,144 entries)

Attributes

Companion
object
Source
TranspositionTable.scala
Supertypes
class Object
trait Matchable
class Any

Attributes

Companion
class
Source
TranspositionTable.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object TurnGenerator

Exhaustive turn-path generator for Dice Chess.

Exhaustive turn-path generator for Dice Chess.

TurnGenerator performs a depth-first search over all micro-move sequences achievable with the given dice rolls, and filters the result to only the legal paths under the Maximum Micro-moves Rule:

  • A path is legal if it ends with a King capture (win condition), or
  • it consumes the maximum achievable number of dice (castling spends two dice in a single move).

Search algorithms (the SearchAlgorithm implementations shipped in the dicechess-engine artifact) use this object to obtain the candidate set before scoring; servers and clients use it for rule validation.

Attributes

Note

Active color is kept constant throughout the turn — it is not toggled between micro-moves. makeMove preserves the side, so we can chain micro-moves directly. The turn is only flipped explicitly at the end by the caller.

Source
TurnGenerator.scala
Supertypes
class Object
trait Matchable
class Any
Self type