Design: the table picker is option 1u of the clickable design (design/); 9j adds an installed set’s table and 8a pins one to a saved-roll group.
The table is the dice tray: the box the dice tumble in. Two things about it are fixed and one is exchangeable.
Simulation units are millimetres.
| Property | Value |
|---|---|
| Long side | 240 mm, always (a real dice tray, regardless of phone size) |
| Short side | 240 mm × screen aspect ratio, clamped to 0.40–0.75 (Pixel 10a: 20:9 → ~108 mm) |
| Wall height | 60 mm as drawn; the collision box is closed to the ceiling at 200 mm |
| Inner corner radius | 12 mm (dice do not wedge into sharp corners) |
| Floor friction / restitution | from the table look, clamped (see below) |
The two heights are not a contradiction. Sixty millimetres is the rim the renderer draws — the wall you can see. The collision walls run the full two hundred up to the ceiling, because “dice cannot leave the table no matter how hard the phone is shaken” is not true of a box with a lid two hundred millimetres up and sides only sixty: a die thrown hard leaves through the gap and never comes back. The rounded corners are only as tall as the rim, which is as far up as a die can wedge into anything.
Orientation follows the phone: portrait phone → portrait table. Rotating the device mid-roll does not rotate the table; the current roll finishes first.
The physical size of the phone’s screen is deliberately not used to scale the table. A 6.3” and a 6.9” phone get the same table; only the aspect ratio differs. This keeps rolls comparable and keeps the capacity numbers stable across devices.
Before any physics body is created, the roll is checked against the table:
floorArea = longSide × shortSide
footprint(d) = π × r(d)² r = bounding-sphere radius of die d at scale 1
required = Σ footprint(d) over all dice in the roll
(including the dice a first explosion could add)
scale = min(1, sqrt(0.30 × floorArea / required))
if scale < 0.40 → roll refused
if diceCount > 100 → roll refused (engine hard cap, independent of scale)
else → all dice are spawned at `scale`
r(d) comes from the shape, not from a guess. A die’s size_mm is its
nominal size, quoted the way a dice maker quotes one: the edge length for a
polyhedron, the diameter for the coin. The bounding-sphere radius is then a
fixed ratio of it, one per catalogue shape:
| Shape | radius ÷ nominal size | Shape | radius ÷ nominal size | |
|---|---|---|---|---|
coin |
√(¼ + 1/64) ≈ 0.5154 | dodecahedron |
(√3/4)(1+√5) ≈ 1.4013 | |
tetrahedron |
√6/4 ≈ 0.6124 | enneagonal-trapezohedron |
≈ 0.7184 | |
cube |
√3/2 ≈ 0.8660 | icosahedron |
√(10+2√5)/4 ≈ 0.9511 | |
octahedron |
√2/2 ≈ 0.7071 | pentagonal-trapezohedron |
≈ 0.7477 |
So a “16 mm d6” is a cube with 16 mm edges and a bounding radius of 16·√3/2 ≈ 13.9 mm, which is where every number in the worked table below comes from. For the coin, nominal size is its diameter — a cylinder has no edge to measure — and for the two trapezohedra it is the long apex edge.
A trapezohedron has no closed form worth writing down. It is fixed by its own
two conditions and they leave no freedom at all: its kite faces have to be
flat, which forces the apex to sit 2/(1 − cos(π/n)) − 1 times the ring
height up, and every corner has to be on one sphere, which is what a fair die
is — an insphere touching every face, a circumsphere through every corner. The
ratios above are what falls out.
That is: dice may collectively cover at most 30 % of the floor with their bounding circles, and they may shrink to 40 % of their nominal size to get there. Both numbers are tunable constants and both are covered by the golden determinism tests.
Worked example on a Pixel 10a table (240 × 108 mm ≈ 259 cm²):
| Roll | Required at scale 1 | Result |
|---|---|---|
1d20 |
7.3 cm² | scale 1.0 |
8d6 (16 mm d6) |
48 cm² | scale 1.0 |
20d6 |
121 cm² | scale 0.80 |
60d6 |
362 cm² | scale 0.46 |
80d6 |
483 cm² | scale 0.40 — the limit |
100d6 |
604 cm² | refused: “100 dice don’t fit on the table; up to 80 do” |
500d6 |
— | refused |
The refusal message always says the largest count that would fit, and offers to open the outcome graph instead, which has no such limit.
Why refuse rather than batch or grow the table: a physics engine with hundreds of convex bodies packed into a small box tunnels, jitters and explodes. The result would not be a roll of the dice, it would be a bug. The outcome graph already answers “what does 500d6 look like”; the table answers “what did these dice do”, and that only means something when they had room to do it.
A table look is a [[table]] entry in a package’s diceset.toml
(docs/dice-sets.md). A package can contain only tables — a “table pack”.
[[table]]
id = "green-felt" # slug, unique within the package
name = "Green felt"
floor_texture = "tables/felt.png" # optional, relative, inside the package
floor_tiling = [3, 6] # texture repeats across short/long side, default [1, 1]
wall_texture = "tables/oak.png" # optional
wall_tiling = [8, 1]
floor_color = "#1f5e3a" # tint; the whole colour when no texture
wall_color = "#5a3a1e"
roughness = 0.9 # 0..1
metallic = 0.0 # 0..1
friction = 0.6 # floor + walls, clamped to 0.2..1.0
restitution = 0.2 # clamped to 0.0..0.6
sound = "felt" # felt | wood | glass | stone | plastic (built-in impact sound sets)
light = "warm" # neutral | warm | cool | dim (built-in lighting presets)
Rules:
floor_tiling lets a 512×512 felt tile cover the floor without a
screen-sized image.The bundled package ships felt-green, felt-black, oak, dark-glass
and plain (a neutral grey that is easy on the eyes and on the battery — it
is what power-saving mode’s result screen echoes).
“Use a photo as table” in settings takes any image from the system picker,
downsizes it to 2048 px on the long side, and writes a [[table]] entry
into the user’s personal package (mine). It is then a normal table and can
be exported with the rest of mine.