Design: statistics (option 1w), history (1x), per-set filtering (5b–5c), sessions (6c) and saved-roll statistics (8b) are in the clickable design (design/).
Players care about how their dice behave even though every face is equally likely. dInfinity records enough to answer “how many natural 20s have I rolled this campaign?” without turning into a spreadsheet.
Every completed roll writes one RollHistory row and updates aggregate
counters. Rolls that are cancelled (app closed mid-tumble) are discarded.
setId + dieId)Dice with duplicated face values (a d6 labelled 1,2,3,1,2,3) count by face value, so lowest/highest are 1 and 3.
Dropped dice (from kh/dl etc.) are still counted in the per-die stats —
the die was thrown and landed on that face — but flagged in the history row.
The same counters rolled up by sides, so “all my d20s” is one line even if some rolls used the brass set and some the doodled one. Sets with non-standard dice appear only in the per-die view.
docs/probability.md so the player can see how their Fireballs compare to
expectationA session is a user-defined bucket (“Tuesday campaign”). All stats above are also available filtered by session. The current session is selectable from the home screen; by default the active saved-roll group’s name is used as the session, and “Unfiled” when no group is active.
Counts of in-flight corrections, re-thrown dice and forced settles (see
docs/physics-and-rendering.md). Hidden behind a developer toggle.
Room database. Version 1 is these three tables and nothing else; saved rolls,
sessions and the installed-set registry arrive with the screens that need them
(docs/TODO.md, Step 4), each as a migration. That is what “migrations from
day one” buys: a database that has only ever been created and never migrated
is one whose first migration gets written under pressure.
Every version’s schema is exported to data/schemas/ and checked in, and
SchemaTest asserts there is one for every version and a migration for every
step between them — so a version bump without a migration fails the build
rather than a player’s phone. There is no destructive fallback: a player’s
natural-20 count is not something to throw away because a schema moved.
roll_history(id, timestamp, session_id, saved_roll_id?, group_id?, formula, total,
seed, input_blob, breakdown_json, anomalies)
die_stats(set_id, die_id, sides, face_value, count, dropped_count,
PRIMARY KEY(set_id, die_id, face_value))
die_summary(set_id, die_id, sides, throws, sum, sum_sq, hi_streak, hi_streak_max,
lo_streak, lo_streak_max, last_rolled_at)
Streaks and sums are updated in the same transaction as the history insert.
roll_history is capped at 50,000 rows by default (oldest pruned); aggregates
are never pruned. Uninstalling a dice set keeps its rows.
seed and input_blob (the quantised shake samples, or the default throw
parameters) are kept so a roll can be reproduced exactly when a bug report
needs it. They are internal: no screen shows them, and the export leaves
them out. Reproducing a stored roll is a developer action
(docs/physics-and-rendering.md, Debug tooling), not a feature of the app.