The implementation plan. Steps run in order; within a step the boxes are
roughly ordered too. Finished items are deleted, not ticked — git history
and docs/STATUS.md are the record, and a step that is wholly done is
removed. Keep the file a plan, not a diary.
Everything here builds the specification in README.md, docs/ and the
prototype in design/. Where a step says device, it cannot run
on CI (see .claude/CLAUDE.md).
Everything a machine can check, on every PR. Emulator and device suites are Step 5 and stay off CI.
java-kotlin analysis once the bundle supports Kotlin 2.4.20 — the matrix entry is commented out in .github/workflows/codeql.yml with the build steps kept readyDEPENDABOT_METADATA_TOKEN Dependabot secret so the metadata commit starts the checks by itself. Without it the automation still works, and the pull request shows an Approve workflows to run banner to press (docs/build-setup.md)VerifyDeviceTestResultsTask and the ignoreFailures on connectedDebugAndroidTest once AGP stops failing runs on devices whose adb serial contains a colon (docs/build-setup.md)Done. A pull request shows a green check per concern, Sonar decorates it
with coverage, and v0.0.1 produced a signed dInfinityApp-0.0.1.apk. The
three boxes above are not blocking anything: two wait on other projects and one
is optional.
The shared layer every screen sits on. Built bottom-up, each piece tested to completion before the screens start, because a bug here is a bug in every screen.
AndroidExternalSurface handing its surface to TrayDriver. Everything under it is built and runs on both devices — the engine, the material, the lights, the meshes, the camera, the blend between simulation states, the roll stepped from a frame clock (LiveRoll) and the thread that draws it frame by frame onto a real surface (TrayDriver). What is left is the composable, and it arrives with the roll screen (4.1)atlases argument of FilamentDiceRenderer; until something fills it, dice are drawn in their own colours. Belongs with 4.4, and brings the two texture checks below with itdocs/physics-and-rendering.md). A d4 needs three per triangle, one at each corner, because its values belong to corners — the same rule the face designer follows (docs/dice-sets.md, “The d4”)input/shake)dicesets/format cannot do from bytes alone: a file that passes the header check but will not actually decode, and an atlas with empty cells. Both belong wherever textures are first decoded (docs/dice-sets.md, “Validation”)Done when a formula can be parsed, planned, simulated headless and scored from a unit test, with no UI in the picture, and Sonar reports ≥ 80 % on these modules.
One section per screen. Each is a vertical slice: state, UI, tests, and the
device check it needs. The design option ids (1a, 9c, …) are the labels on
the canvas — open design/ beside the code.
Every screen follows the same four steps, so they are written out once here rather than repeated below:
ViewModel over the Step 3 foundations. No logic that
belongs in core/ leaks into the screen.feature/rollHome. Design 1a–1j, 2a, 3a–3c, 4a, 4b, 6d, 6f, 9a, 9c,
1z. Spec: docs/dice-notation.md, docs/tables.md,
docs/physics-and-rendering.md.
The state behind the screen is built: RollMachine turns a typed formula into
a throw and a throw’s faces into a result, refuses what the table cannot hold
before a body exists, and throws an exploding die again through the
simulator. What is left below is the screen itself.
1b), table look applied — a Compose AndroidExternalSurface handing its surface to TrayDriverdocs/dice-notation.md)1h) — tap adds, long-press removes, count badges; set dropdown (4a)2a) with live validation, error squiggle over the offending range (6f, 9c), rolling blocked while invalidinput/shake1f): total, per-group subtotals, dice, modifiers, dropped dice struck through, natural max in the accent6d) — Down / Nearest / Up for this throw only500d6) with the largest count that would fit1z) — no renderer created, result appears at once9a): built-in set only, Unfiled group, no saved rollsDone when every example in docs/dice-notation.md can be typed, rolled
and read here, and the same seed gives the same result with the renderer on
and off.
feature/graphDesign 1k–1m, 2c, 7a. Spec: docs/probability.md.
1k) of the exact PMF, mean line, ±1σ band7a)core/probability exactly, not approximatelyfeature/savedDesign 1n–1p, 1r, 6e, 7b, 9b, 9d, 9f, 9g. Spec:
docs/dice-notation.md (Saved rolls).
1o), favourites first then by recent use; icons in the roll’s colour (9d)1r): live-validated formula with mean and range, icon, colour, group, favourite, per-roll table pin (7b)6e), naming the clash — no merge, nothing deleted9b)feature/setsDesign 1s, 1t, 5a, 6a, 6b, 8c, 9h, 9i. Spec: docs/dice-sets.md.
5a), bundled set protected6a): author, license, source with commit, dice rendered from the set, set-as-default6b): the report with file:line replaces the dice grid, folder kept for an update1t)9h, 9i)8c)feature/tablesDesign 1u, 9j. Spec: docs/tables.md.
feature/designerDesign 1v, 4c, 8d. Spec: docs/face-designer.md.
docs/dice-sets.md, “The d4”). Three guides rather than one, and the two triangles sharing an edge have to agree along it: a die drawn otherwise reads as a different number depending which way it is looked at. Make that hard to do by accident, not a warning afterwards4c)8d): triangle, kite, pentagon, circlefeature/statsDesign 1w, 5b, 5c, 8b, 9e. Spec: docs/statistics.md.
5b), roll-up across sets (5c)8b, 9e)feature/historyDesign 1x. Spec: docs/statistics.md.
feature/sessionsDesign 6c. Spec: docs/statistics.md.
feature/settingsDesign 1q, 1y, 2d. Spec: README.md, docs/architecture.md.
1q), grouped Play / Look back / Customise / AppDInfinityApplication with a real container once more than settings hangs off it2d)Reserved, and none of it can run on CI. This is where the app either convinces or does not: a roll has to look like dice landing, not like an animation of a random number. Runs first as soon as 4.1 renders, then again after every physics change.
docs/build-setup.md), so a regression is caught before the phoneThe two failures to hunt, per docs/physics-and-rendering.md:
a die visibly moved after it stopped, which is worse — it turns a roll into an arrangement in front of the player’s eyes.
docs/physics-and-rendering.md)docs/physics-and-rendering.md as the budgetDone when every target above is met on the Pixel 10a and the user agrees
the dice look right. Numbers that turn out wrong become the new numbers in
docs/physics-and-rendering.md and docs/tables.md — the docs follow the
device, not the other way round.
examples/ dice set: every catalogue shape, commented, blank atlases (the built-in set has no export)v1.0.0Written down so the format need not change later. Not v1 scope.
rhombic-triacontahedron (d30) firstdocs/dice-sets.md, “Shapes after v1”)docs/architecture.md, decision 46) means a set could ship its own .mat rather than only values for the built-in one — iridescent dice, a proper glass d20, a table that is actually brushed metal. v1 does not allow it, and the reason is not effort: a shader is code, it runs on the GPU, and “the app never runs anything from the repository” is a rule of the format (docs/dice-sets.md). Turning it on needs a decision about what a shader from a stranger may do — a compile that never finishes is a hung GPU, and a driver is a large attack surface — plus a limit on compile time, a cap on instruction count, and a refusal that is as legible as the validator’s other refusals. Until then a set varies a material’s parameters, which is what roughness, metallic and the colours already arecore/probability hand-rolls its convolution and its FFT rather than
taking a library, which .claude/CLAUDE.md names as a “complex part”.
The judgement was that the exact PMF is the domain logic and that
pulling in a general maths library for ninety lines of transform is a
worse trade than owning them — but it is a trade, and it is worth a
second opiniondocs/face-designer.md (4.6)docs/dice-notation.mdsdk in app/src/test/resources/robolectric.properties to 37 when Robolectric supports itdocs/architecture.md, decision 39)/dev/kvm unconditionally, so a
machine without nested virtualisation cannot open the project at all.
Confirm that is the right default rather than building the image
without an emulator and asking for the device only when one is wanted
(docs/build-setup.md).thumbnail is not imported; decide whether a preview image belongs in the repo