An open-source, high-performance, cross-platform game engine and AI search for Dice Chess, built with Scala 3 targeting JVM, JavaScript (Scala.js), and WebAssembly (WasmGC).
- Browser client prototype β a SvelteKit application in development, with local bot search in a Web Worker.
- Dice Chess TV β React Native for Vega, with remote-controlled hotseat play and on-device bots using the same npm package. Its public demo was recorded on the Vega Virtual Device; see the TV project for device-testing status.
Choose your package and application path in Build with the Engine: JavaScript/TypeScript, Scala, Java/Kotlin or WasmGC.
Dice Chess is a stochastic chess variant where players roll three six-sided dice before making their moves.
-
The Turn Structure:
- A player's turn consists of one roll of 3 dice and up to 3 micro-moves.
- The active color in the FEN does not change within the turn (during micro-moves). It only changes when the turn ends β after the playable dice have been consumed, or immediately when no legal moves exist (a forced pass; there is no voluntary pass).
-
The Dice Roll:
- Each die determines which piece type may move:
1= Pawn (β)2= Knight (β)3= Bishop (β)4= Rook (β)5= Queen (β)6= King (β)
- Each die determines which piece type may move:
-
Micro-moves:
- Each micro-move consumes one die matching the moved piece's type. Castling consumes two dice β King (
6) and Rook (4) β in a single move. - You can move different pieces or the same piece multiple times during your turn, as long as each move matches one of the remaining dice.
- Victory Condition: The game is won by capturing the opponent's king directly (no check/checkmate).
- Maximum Micro-moves Rule: Players must choose move sequences that maximize the number of dice consumed over the whole turn; any sequence that captures the opponent's king is always legal regardless of length.
- Each micro-move consumes one die matching the moved piece's type. Castling consumes two dice β King (
This project compiles from a single Scala 3 codebase into four published artifacts, all at one version per release:
- Maven rules artifact (
com.fortemate:dicechess-rules_3, from 0.11.0): the rules of the game only β board and DFEN model, move generation, legal turn enumeration, dice probabilities. Depends on the Scala standard library and nothing else. - Maven engine artifact (
com.fortemate:dicechess-engine_3): bots, evaluators, feature extractors and the Java/Kotlin facadeJvmApi. Depends ondicechess-rules_3of the same version;onnxruntimeis an optional dependency that ONNX consumers declare themselves. The JMH benchmarks, the bot arena and the CLI live in this repository but are not part of the jar. - NPM JavaScript Package (
@fortemate/dicechess-engine): ES Module for browsers and Node.js. - NPM WebAssembly Package (
@fortemate/dicechess-engine-wasm): WasmGC build running on modern WebAssembly runtimes.
What each artifact contains, what is deliberately left out and how to pick or switch a coordinate: Published Artifacts & Rules-Only Migration.
- mise (manages Java Temurin 25, Node.js 26, sbt, scalafmt, and tooling)
# Clone the repository
git clone https://github.com/fortemate/dicechess-engine.git
cd dicechess-engine
# Install toolchain and register git hooks
mise install
mise run setup
# Run full test suite and quality gates
mise run check
# Performance benchmarks
mise run bench # JVM JMH benchmarks
mise run bench:js # JavaScript (Scala.js / ES2022) on Node.js
mise run bench:wasm # WebAssembly (Scala.js / WasmGC) on Node.js
mise run bench:all # Cross-platform JS vs Wasm vs JVM comparisonInstall from npmjs.org without a token or custom registry configuration:
npm install @fortemate/dicechess-engineimport { DiceChess } from '@fortemate/dicechess-engine';
// A Dice Chess FEN (DFEN) carries the rolled dice as a 7th field: "PN" = a Pawn and a Knight die.
const dfen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 PN';
// Every legal first micro-move for this position and roll, as UCI strings
const legalMoves = DiceChess.getLegalUciMoves(dfen); // e.g. ["e2e3", "e2e4", "b1c3", ...]
// Every legal turn as a prefix tree of micro-moves; a client follows a turn by walking it
const turns = DiceChess.getLegalTurnTree(dfen); // e.g. { "e2e4": { "g1f3": {}, ... }, ... }
// The dice the rest of the turn can still spend, given the roll and the moves played; dim the others
const playable = DiceChess.getPlayableDice(dfen, ['e2e4']); // "N": the knight die can still be used
// Play the turn one micro-move at a time (each result keeps the unspent dice), then close it
const afterPawn = DiceChess.applyMove(dfen, 'e2', 'e4'); // knight die left
const afterKnight = DiceChess.applyMove(afterPawn, 'g1', 'f3'); // a leaf of the tree: the turn is complete
const nextTurn = DiceChess.endTurn(afterKnight);
// Ask a built-in bot for its turn (ids from DiceChess.getAvailableBots())
const bot = DiceChess.getBestMove(`${nextTurn} pn`, { algorithm: 'greedy' }); // Black rolled a Pawn and a Knight
console.log('Bot plays:', bot.moves, 'score', bot.score);The full surface (getLegalTurnTree, getPlayableDice, getAvailableBots, clock-aware getBestMove, doubling and draw decisions, estimateEquity) is documented in the JavaScript API Reference.
An application can register the experimental Tactician search with an explicit, externally supplied profile before selecting it by id:
import type { TacticianConfig } from '@fortemate/dicechess-engine';
function installTactician(profile: TacticianConfig): void {
if (!DiceChess.registerTacticianBot(profile, 'tactician', 'Tactician')) {
throw new Error('Invalid profile or bot id already registered');
}
}The profile contains candidateLimit, kingThreatCp, kingDangerCp and
hangingWeight; there is no default profile. Existing bot ids cannot be replaced.
Registration adds the bot to getAvailableBots(), and getBestMove accepts its id
as algorithm. The search is synchronous; the candidate limit bounds tactical
rescoring, not legal-turn generation or total elapsed time.
JvmApi ships in com.fortemate:dicechess-engine_3 (Maven Central); rules-only JVM projects use dicechess-rules_3 and call the Scala API directly.
import dicechess.engine.domain.GameState;
import dicechess.engine.jvmapi.JvmApi;
import java.util.List;
public class BotExample {
public static void main(String[] args) {
GameState state = JvmApi.parseDfen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
GameState rolled = JvmApi.withDice(state, List.of(1, 2, 4)); // Pawn, Knight, Rook
JvmApi.bestTurn(rolled, "greedy").ifPresentOrElse(
turn -> System.out.println("Best turn: " + turn.uci() + " score " + turn.score()),
() -> System.out.println("No legal turn for this roll: forced pass"));
}
}Java and Kotlin hosts can register Tactician through
JvmApi.registerTacticianBot(candidateLimit, kingThreatCp, kingDangerCp, hangingWeight, id, name)
with their own explicit profile, then pass the registered id to bestTurn.
The facade's contract β legalTurns, evaluate, thinking-time budgets, game-over queries β is documented in the JVM API Reference.
- Licensed under AGPL-3.0-only (SPDX
AGPL-3.0-only; the GNU Affero General Public License version 3, without the "or any later version" option). - Contributions require signing the Contributor License Agreement (see CONTRIBUTING.md).
- Security vulnerability reports should follow SECURITY.md.