Skip to content

Show the working in a UI

A roll that prints only its total asks to be trusted. This guide renders one that shows every die it threw, and marks the ones that did not count.

The examples use the package, and every one of them works the same on a roll that came back from the API — the result is the same shape either way. Get yours from roll() or from a request, then pick up here.

The one difference is keptFlags. Over HTTP it arrives as a field on the group; in the package it is a function you call, because the package does not build the list unless you ask for it.

Each entry in dice is one group — one run of dice with the same number of sides. Two things matter here: results, every die in the order it was thrown, and a list of flags aligned to it one for one.

import { keptFlags, roll } from 'opendice'
const result = roll('4d6kh3')
const group = result.dice[0]
group.results // [5, 4, 5, 3]
keptFlags(group) // [true, true, true, false]

On a result from the API that last line is group.keptFlags instead.

Walk results and read the flag at the same index. Never filter — the position is the point.

function renderGroup(results, flags) {
return results.map((die, i) => ({ die, kept: flags[i] }))
}
{
renderGroup(group.results, keptFlags(group)).map(({ die, kept }, i) => (
<span key={i} className={kept ? 'die' : 'die die--dropped'}>
{die}
</span>
))
}

Style die--dropped however you like — dimmed, struck through, half opacity. What matters is that it stays where it was rolled.

A formula that sets a floor or a ceiling puts that bound in results beside the dice, so 1d6min3 comes back with two entries for one die. The flags still line up one for one and what they mark still adds up to total, so the loop above renders it correctly without changing.

What breaks is anything that reads results.length as a number of dice, or labels the entries “die 1” and “die 2”. Read the count from the formula, or leave it off — an exploding die rolls more times than it was asked for anyway.

1d8+1d4+3 comes back as two groups and a modifier. Render each group in turn, then the flat numbers.

const parts = result.dice.map((group) => renderGroup(group.results, keptFlags(group)))
result.modifiers // [3] — each flat number, in the order written
result.modifier // 3 — their sum
result.total // 12

Use modifiers rather than modifier when you are showing the working. A roll written +1 -6 has a modifier of -5, which is true and says nothing about where it came from.

A group can also subtract. Check sign, which is 1 or -1, before you print a + in front of its total.

naturalHigh and naturalLow are set only when a group kept exactly one die and that die showed its highest face, or a 1. A group that kept a bound instead of a die sets neither, so 1d20min20 never claims a natural 20.

group.naturalHigh // true on a kept, single d20 showing 20

They are deliberately not set when several dice were kept. A top face among four dice is not a critical on its own, and deciding otherwise is your game’s business rather than this API’s — see What Rollful does not know.