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:
- A dice roll that yields 1–3 die values.
- A sequence of 1–3 micro-moves — each micro-move must be made by the piece type matching the consumed die.
- 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
- Self type
-
AggressiveSearch.type
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 Objecttrait Matchableclass Any
- Self type
-
BotRegistry.type
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 Objecttrait Matchableclass Any
- Self type
-
BudgetedRules.type
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow 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
- Self type
-
CheckmateAwareSearch.type
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 (
0for 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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
Attributes
- Companion
- class
- Source
- OnnxExpectimaxSearch.scala
- Supertypes
-
trait Producttrait Mirrorclass Objecttrait Matchableclass Any
- Self type
-
CollapseModel.type
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 Objecttrait Matchableclass 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
- Known subtypes
-
object MonteCarloSearch
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> 1quantify the Rao-Blackwell gain;+Infinitymeans the estimate is exact (zero sample variance, e.g. a position decided on the first roll).
Attributes
- Source
- MonteCarloEquity.scala
- Supertypes
-
trait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 Objecttrait Matchableclass Any
- Self type
-
Evaluator.type
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 → leafsearch; depth 3 addsCHANCE → MAX → leafafter every opponent reply so exchanges include our recapture.
Attributes
- Source
- ExpectimaxSearch.scala
- Supertypes
-
trait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
Attributes
- Companion
- class
- Source
- ExpectimaxSearch.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
ExpectimaxSearch.type
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
candidateLimitcompensates 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
findBestMovecall. 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
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
- Self type
-
GreedySearch.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
- Self type
-
GreedySearchV2.type
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 Objecttrait Matchableclass Any
- Self type
-
KcpFeatures.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 Objecttrait Matchableclass Any
- Self type
-
KcpMobilityFeatures.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 Objecttrait Matchableclass Any
- Self type
Attributes
- Source
- KcpScratchBoard.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
KcpScratchBoard.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 Objecttrait Matchableclass Any
- Self type
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 Objecttrait Matchableclass 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 Objecttrait Matchableclass Any
- Self type
-
MaterialValues.type
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).0disables adaptive stopping and runs exactly rollouts rollouts.
Attributes
- Source
- MonteCarloEquity.scala
- Supertypes
-
trait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow 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 Objecttrait Matchableclass Any
- Self type
-
MonteCarloEquity.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
-
trait TimeBudgetedSearchtrait DrawOfferLogictrait SearchAlgorithmclass Objecttrait Matchableclass AnyShow all
- Self type
-
MonteCarloSearch.type
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 AutoCloseabletrait TimeBudgetedSearchtrait SearchAlgorithmclass Objecttrait Matchableclass AnyShow all
Attributes
- Companion
- class
- Source
- OnnxEvalSearch.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
OnnxEvalSearch.type
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 AutoCloseabletrait TimeBudgetedSearchtrait SearchAlgorithmclass Objecttrait Matchableclass AnyShow all
Attributes
- Companion
- class
- Source
- OnnxExpectimaxSearch.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
OnnxExpectimaxSearch.type
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 Objecttrait Matchableclass Any
- Self type
-
OnnxFeatures.type
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 analyticsdice_sortedcolumn. 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 Objecttrait Matchableclass Any
- Self type
-
OpeningBook.type
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
- Known subtypes
Attributes
- Companion
- class
- Source
- OpeningBookBot.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
OpeningBookBot.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 Objecttrait Matchableclass Any
- Self type
-
OpeningBookParser.type
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 Objecttrait Matchableclass Any
- Self type
-
PassedPawns.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 Objecttrait Matchableclass Any
- Self type
-
PieceDiversity.type
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 Objecttrait Matchableclass Any
- Self type
-
PieceMobility.type
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 Objecttrait Matchableclass Any
- Self type
-
PieceSafety.type
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 Objecttrait Matchableclass Any
- Self type
-
PlayableDice.type
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
Attributes
- Companion
- class
- Source
- OnnxExpectimaxSearch.scala
- Supertypes
-
trait Producttrait Mirrorclass Objecttrait Matchableclass Any
- Self type
-
PreRankModel.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
- Self type
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
- Self type
-
RandomSearch.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..5are the '''mover's''' Pawn, Knight, Bishop, Rook, Queen, King, in dicechess.engine.domain.PieceType order; - planes
6..11are 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 else0.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 Objecttrait Matchableclass Any
- Self type
-
RawBoardFeatures.type
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_diffruns 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 Objecttrait Matchableclass Any
- Self type
-
RichFeatures.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 Objecttrait Matchableclass Any
- Self type
-
RichPdiFeatures.type
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 toNoneand does not invokeevalBatch.
Attributes
- Source
- ExpectimaxSearch.scala
- Supertypes
-
trait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
Attributes
- Companion
- class
- Source
- OnnxExpectimaxSearch.scala
- Supertypes
-
trait Producttrait Mirrorclass Objecttrait Matchableclass Any
- Self type
-
RootRescoreModel.type
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 < 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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow 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 Objecttrait Matchableclass Any
- Known subtypes
-
object AggressiveSearchobject CheckmateAwareSearchtrait DrawOfferLogicobject MonteCarloSearchobject GreedySearchobject GreedySearchV2class OpeningBookBotobject RandomKingCaptureSearchobject RandomSearchclass TacticianSearchtrait TimeBudgetedSearchclass ExpectimaxSearchclass OnnxEvalSearchclass OnnxExpectimaxSearchShow all
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 Objecttrait Matchableclass Any
- Self type
-
SearchScoring.type
Attributes
- Source
- TranspositionTable.scala
- Supertypes
-
trait Enumtrait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
Attributes
- Source
- TranspositionTable.scala
- Supertypes
-
trait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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 Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all
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
-
trait TimeBudgetedSearchclass OpeningBookBottrait SearchAlgorithmclass Objecttrait Matchableclass AnyShow 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
- Known subtypes
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
incrementterm keeps per-turn time high while the clock is healthy. Asremainingshrinks thehardCap(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 Objecttrait Matchableclass Any
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 Objecttrait Matchableclass Any
- Self type
-
TimeManager.type
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 Objecttrait Matchableclass Any
- Self type
-
TimePolicies.type
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 Objecttrait Matchableclass Any
- Known subtypes
-
object EmpiricalV1object LegacyLinear
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 Objecttrait Matchableclass Any
Attributes
- Companion
- class
- Source
- TranspositionTable.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
TranspositionTable.type
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.
makeMovepreserves 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 Objecttrait Matchableclass Any
- Self type
-
TurnGenerator.type