- Choose the TypeScript package manager and monorepo tool after evaluating publish workflow and Unity/Godot needs. (npm workspaces — ADR-0004)
- Create the core package with strict TypeScript configuration. (
packages/core,@diceforge-sdk/core) - Define domain schemas for die definitions, coin flips, RNG, roll requests, and immutable result records. (
records.ts,rng/types.ts,notation/ast.ts) - Implement seeded and system RNG providers with tests. (xoshiro128** + golden tests — ADR-0005)
- Implement initial dice notation parser and resolver with tests. (grammar v1 — ADR-0006)
- Implement coin-flip resolver with tests.
- Define event serialization and schema-versioning rules. (
serialization.ts— ADR-0006) - Add lint, formatting, test, coverage, and CI workflows. (Biome, Vitest,
.github/workflows/ci.yml) - Create one headless usage example. (
examples/headless) - Record implementation decisions in
DECISIONS.mdas they are made. (ADR-0004..0006)
Remaining before tagging 0.1.0:
- Push to a Git host and verify the CI workflow runs green on Node 20 and 24. (github.com/StephanTTM/DiceForgeSDK, public since 2026-07-26)
- Publish to npm — shipped with the 0.1.0 release (see the 0.2.0 section).
- Select a web renderer/physics approach through an ADR. (Three.js + outcome-first scripted tumble — ADR-0007)
- Define renderer plugin contract and presentation lifecycle. (
InteractionPresenterin core — ADR-0008) - Build a small browser demo with dice and coin flip interactions. (
examples/web-demo,npm run demo:web) - Add reduced-motion and no-WebGL fallbacks. (DOM tile backend,
prefers-reduced-motion, aria-live announcements)
- Add one popular-framework example (React) consuming
@diceforge-sdk/renderer-web. (examples/react-demo; also swaps in@diceforge-sdk/presenter-physicsat runtime, showing the presenter contract is what the component depends on) - Theme/asset loading as an optional presentation concern. (
DiceTheme+ lazy glTF models with calibrated face tables, KayKit CC0 pack — ADR-0010) - Publish 0.1.0 to npm. (
@diceforge-sdk/core+@diceforge-sdk/renderer-weblive 2026-07-25;v0.1.0tagged; trusted publishing configured; release workflow verified green and idempotent — ADR-0009)
- Add capability discovery so an adapter can ask what a presenter supports instead of feature-detecting it. (
PresenterCapabilities+presentationSupport()in core, declared per instance by the web renderer — ADR-0014) - Custom dice definitions. (
defineDie+createDiceEngine({ dice }),4d{fate}, any face count 2..1000, event schema v2 with a v1 read path — ADR-0015) - Notation extensions beyond grammar v1 (exploding dice, rerolls). (
4d6!,4d6r1,4d6ro1; any modifier order, capped chains, extras recorded in rolled order — ADR-0016) - Replay support: re-present a stored record without re-resolving it. (
SessionRecord,createSession,serializeSession/deserializeSession,replaySession; replay draws no randomness — ADR-0017) - Plugin-author documentation and a compatibility test kit a third-party renderer can run against. (
@diceforge-sdk/testing: runner-agnostic conformance checks + the presenter-authoring guide;renderer-webruns it against itself — ADR-0014) - Decide whether the first-party dice ship as an optional
@diceforge-sdk/assets-forgepackage or stay repository-only. (shipped as a package — ADR-0013;forgeTheme(forgeAssets({ color }))needs no copying,baseUrlstill serves custom packs)
- Physics-based presenter plugin. Record the trajectory headlessly, then remap the mesh inside the collider by a symmetry so the recorded face lands where the simulation's did (ADR-0018, accepted). Every face pair on all six shapes admits such a remap (
symmetry.test.ts), and the shipped rotation tables cannot supply it.- Decide the engine and measure a real roll. (
cannon-es;npm run physics— every shape settles every time in ~0.75 s, scatters 123–269 mm at p95, remaps at 0.0000° error, 28–37 kB of trajectory per roll) - Measure a bevelled hull from the shipped
.glb. (npm run physics -- --hull=glb: simulates fine, but the remap fails on all 180 poses and it costs up to 500x more — the collider is the idealised solid and the model is cosmetic, which also frees custom art to have holes or missing faces) - Decide how the camera frames the roll, and what the simulation costs. (
--tray=<mm>: walls cap the scatter, anddieWidth × (5 + 0.8√n)settles in ~1s at 96–100% seated. There is no pre-roll latency — simulating a whole roll costs 4–44 ms for 1–40 dice; the 0.7–1.5 s is the animation's length. The first measurement claimed a fixed tray meant the camera never moves; superseded by the framing item below.) - Ship it as
@diceforge-sdk/presenter-physics. (simulateRoll+ the symmetry remap, cannon-es, tray-framed, 10 tests; the collider is the idealised solid and the model is cosmetic, so a theme needs no symmetry data of its own) - Play the trajectory. (
createPhysicsPresenter— three.js playback, remap applied before the first frame, dropped dice revealed on landing; coins, custom dice and no-WebGL delegate torenderer-web, and it passes the conformance suite with no skips) - Framing polish. (Rectangular tray shaped to the viewport — measured to cost nothing in settling — and the camera frames where the dice came to rest, centred on them, instead of the walls. Shaping the tray alone did not help: it makes the tray bigger, so the camera pulls back by the same amount.)
- Decide the engine and measure a real roll. (
- Run the visual regression suite in CI against platform-stable baselines. (a
visualjob inside the pinnedmcr.microsoft.com/playwrightimage;npm run vrt:dockerreproduces it, and a run from anywhere else is advisory rather than red)
- Tie each die value to the numeral printed on the model, in CI. (
forge-models.test.tswalks rotation → face → UV → atlas tile, and checks every tile is inked and distinct; proved by a generator-style fault that no other test caught) - Make the pre-publish tarball check repeatable. (
npm run smoke— packs, installs and uses all five packages, ~20 s) - Physics coin flips. (
simulateCoinFlip— cylinder collider whose faces are the outcomes, thrown into the shared tray; the model's calibrated pair seats it and a half-turn symmetry lands the recorded face; radius and thickness measured from the loaded model; rim landings re-thrown. 60/60 seeded flips flat on the recorded outcome at ~5 ms each; the presenter no longer swaps canvases for a coin)
- Record impacts in the simulation. (
PhysicsImpactonPhysicsRoll/PhysicsFlipfrom cannon's collide events — time, body, felt/wall/die, closing speed in m/s at real scale; deterministic per seed, asserted) - Impact-driven synthesized knocks. (
impactSchedule— pure, measured thresholds: 0.12 m/s floor, 55 ms per-body merge; Web Audio synthesis from filtered noise, lazy context in the click chain, silent without Web Audio — ADR-0020) -
soundoption on the physics presenter, default off, and a demo checkbox. (verified in-browser: sound off schedules nothing; a 3d6 roll schedules 7 knocks on one lazily-created context; a coin flip 2) - Tune the knock voices down. 1.0 blocker (ROADMAP's readiness checklist). First listen (product owner, 2026-07-28): the sound works but reads a bit high pitched. Fix shape: lower the material bandpass centres in
audio.ts(MATERIALS— felt 950 / wall 2100 / die 3100 Hz today), keeping their order so felt stays dullest and die-on-die brightest; a longer decay would also soften the read. Constants are internal (ADR-0020), so retuning is free — but tune with the product owner listening, not ahead of them.
- Reroll and explosion stories in both web presenters, matching Godot. (
visualDiceForEventreturns the settled stage withrerolledFaces/exploded/bornOf; the WebGL renderer's authored story lives inwebgl/story.ts, the physics presenter's multi-throw plan inpresenter-physics/src/story.ts— both pure and headlessly tested; three VRT scenes pin the settled stages; the React demo grew one-click story buttons) - Custom-dice stories: a custom die falls back to tiles, which show only the settled stage — its rerolls and explosions have no motion anywhere yet.
- Physics follow-up throws run in their own worlds, so a re-toss or a born die does not collide with resting dice and can settle closer than a real die could (or clip one in flight). Fix shape: seed the follow-up world with the resting dice as static bodies, and re-throw when the landing spot overlaps one.
- Product owner's live look at the web stories (throw feel of the physics re-toss pickup, celebration timing) — constants are
STORYin each story module.
- Decide how a non-TypeScript platform gets the core: a native port held to exported conformance vectors, bit for bit. (
tools/conformance/export-vectors.mjs→packages/testing/vectors/core-vectors.json, freshness-tested against the live core every run — ADR-0021) - Godot: headless engine as a GDScript addon. (
adapters/godot/addons/diceforge— RNG, grammar v1.2, resolution, custom dice, coin; 57/57 vectors pass in Godot 4.7.1 — extended 2026-07-30 with degenerate seeds and hostile parse errors from the core's adversarial probes — and one flipped rotate constant fails 16, so the gate is real. A lone-surrogate seed is deliberately vector-exempt: JSON cannot carry unpaired surrogates in interchange, measured against Godot's parser) - Godot: posed presentation — the forge models in a scene, faces from the calibrated manifest. (
presenter_3d.gd+demo/dice_demo.tscn: runtime-loads models and textures frompackages/assets-forge, poses every die on its recorded value, darkens dropped dice, shows the coin;face_up()mechanically verifies every pose against the record — 4 seeded shots, 0 failures, screenshots reviewed) - Godot: rolling motion. (authored tumble per ADR-0007 — drop, bounce, free tumble easing into exactly the calibrated pose; dropped dice dim on landing; seedable motion for reproducible captures. Real physics measured impossible in GDScript: no manual stepping on
PhysicsServer3D—tests/capability.gdasks the engine — so ADR-0018's record-then-replay waits on engine support or a GDExtension) - Godot: camera/theming polish for a real game scene, and percentile-pair + custom-dice presentation.
- Godot: distribution bundle. (
npm run godot:bundlecomposes addon + forge dice into the Asset Library layout with a zero-config presenter default; verified by unzipping into a fresh project and rolling the golden-vector record with no configuration. TheGodot bundleworkflow publishes it as an artifact and force-pushes the orphangodot-assetbranch) - Godot: Asset Library submission. (needs the human owner: an assetlib account, the listing form pointed at the
godot-assetbranch, an icon, and screenshots — the bundle branch is already the exact layout it serves) - Run the Godot conformance scene in CI. (needs a Godot binary on the runner; the scene already exits nonzero on mismatch)
- Unity: C# port of the core against the same vectors. (
adapters/unity/Runtime— RNG, grammar v1.2, resolution, custom dice, coin, schema v2 records; 57/57 vectors pass in .NET 10, and the gate is real: one changed rotate constant fails every RNG vector, and reversing the keep/drop tie-break fails the one record vector with a tie. Pure C#, noUnityEnginetypes, so it is tested headlessly withdotnet run— no Unity licence needed) - Unity: package layout and an end-to-end sample scene. (
.asmdef, UPMpackage.json, and a real import into a Unity project — needs Unity itself and the product owner at the desk. The engine is done and verified; this is the last mile.) - Unity: presentation — the forge models in a scene, posed from the calibrated manifest, as the Godot presenter does.
-
Declare the core stable. (ADR-0022, 2026-07-30: grammar v1.2, event schema v2, the RNG and presenter contracts, and the conformance vectors are frozen — changes are additive-by-ADR only. Success-counting pools deferred by decision, recorded in the ADR, so the Unity port targets a fixed contract)
-
Harden the six-times-rejected fallback pose in
simulateRoll/simulateCoinFlip. (2026-07-31. Reproduced deterministically instead of soaking for the 1-in-~300 tail: a tray far too small for its dice rejects every throw, so the fallback is reached on purpose — worst up-face margin measured at 7.6e-6, below the calibrated table's own 1e-6 precision, exactly as diagnosed. Fixed on both counts: the fallback now ranks settled throws above still-moving ones, and a die still propped up is eased onto its resting face over the last 12 frames, position following to the height a flat die rests at. Worst non-d4 margin after: 0.238, a 31,000x improvement; the coin gets the same treatment so it is never left on its rim. Five tests pin it, and reverting the one-character axis sign reproduces both old numbers exactly. Every VRT physics scene verified to take the unchanged success path, so no baseline moved.) -
Guarantee the coin tumbles on entry. (the diagnosis held — isotropic spin reads as a drop 51 times in 60 — but flooring the diameter spin only got it to 40/60: the real lever was air time, so the coin is now tossed upward as well.
PhysicsCoin.turnoverscounts horizon crossings, the retry rejects fewer than two, and a rim-spinner longer than 3 s is re-thrown instead of watched. Measured: 120/120 seeds at ≥2 turnovers, worst duration 1.5 s, ~5.5 ms per flip) -
Decide how a physics d4 reads. Found while fixing the fallback pose (2026-07-31), and it is a product question, not a bug to fix quietly. A tetrahedron resting on a face leaves its other three faces at identical elevation (Y = +1/3) by symmetry, so "which face is up" has no answer — measured, every d4 roll settles with an up-face margin of 1e-6 to 4e-6, which is the calibrated table's 6-decimal rounding, not physics. The rendered die is still correct: the remap puts the recorded numeral where the simulation's landed face was, so the numeral shown is the recorded one, and the smoke test passes because the recorded face inherits that slot structurally. But it is decided at the level of arithmetic noise, and a player looking at a settled d4 sees three slanted numerals with nothing marking which one counts. The non-physics renderer sidesteps this by posing the d4 in its calibrated orientation (a face straight up, balanced on a vertex) and viewing from a low angle — a pose gravity never produces. Options: adopt a real d4 reading convention (apex numeral, or the face resting down) and recalibrate; keep the current behaviour and document it; or have the physics presenter delegate d4s to the renderer's posed view. Needs the product owner. Note there is no physics d4 VRT scene today, so this has never been visually reviewed — add one alongside whatever is decided.
-
Extend the plugin contracts to the categories beyond presentation (physics, audio, transport) once a second implementation exists to shape them — ARCHITECTURE lists them, but nothing implements them yet.
- Theme/asset pack policy and licensing checklist. (ADR-0010,
assets/LICENSES.md, CONTRIBUTING) - Add the missing
boardgame_bits_texture.pngso the KayKit D6_A/D6_B pip styles can be offered. (d6Style: "pips-a" | "pips-b") - Calibrate d10/d12 models so themes can cover every shape. (superseded: first-party set generated for every shape, tables exact by construction — ADR-0011)
- Texture the first-party dice, then ship
forgeTheme(). (five colours, textured coin,tools/blender/build_textures.py) - Give the percentile tens die its own 00–90 texture. (
DieModelSet.tensTextureUrl, generated per colour) - Even out apparent die sizes. (
modelSilhouetteScaleequalizes each die's on-screen silhouette, measured from the loaded mesh) - Give procedural dice beveled edges, or retire them. (retired, along with the KayKit pack — ADR-0012)
- Optional multiplayer transport plugin research. (beyond 1.0)
- Success-counting pools (
7d10>=8— count dice that pass a target instead of summing; World of Darkness, Shadowrun, Year Zero). Post-1.0 by decision (ADR-0022): waits for a real integration to shape the dialect, then lands additively — target syntax errors today, per-die success flags and a group success count ride a schema bump, keep/drop rejected in pool groups, vectors grow with it. - Browser-based visual regression testing for the renderer. (
npm run vrt, 17 scenes including six for the physics presenter, Playwright + committed baselines)
Move work between sections as it changes. Mark completed tasks only after code, tests, and relevant documentation are present. Add scoped tasks rather than leaving vague implementation notes.