Skip to content

Hidden rolls

tilttek/secret-rolls

Rolling out of sight

$hidden = $rolls->roll(
    'scene:12',                          // the scene: 1 to 128 letters, digits, dots, colons, underscores or hyphens
    '2d6+1',                             // a Pool, or an input in the notation of the locale
    roller: 'seat:gm',                   // who rolled, an opaque reference, public
    context: 'The rope bridge holds?',   // what the roll was for, sealed with the result
    rounding: RoundingMode::NegativeInfinity,
    locale: 'en',
);

roll() draws a seed of 32 bytes from the secure randomizer, rolls the expression with DiceRoller::rollVerifiable() — the seed, the roll's identifier as the client seed, nonce 0 and the rounding of its divisions —, keeps the HiddenRoll sealed in the store and appends the entry of its commitment to the scene's journal. The HiddenRoll returned is the game master's: $hidden->opening->totals() gives the totals, $hidden->opening->result the whole result as PoolResult::toArray() writes it.

The identifier of a roll is derived from its scene and its position in the journal: 32 hexadecimal digits, the first half of the SHA-256 of ["tilttek-secret-rolls/1", scene, position]. It is known before the roll is made, which binds the client seed of the roll to its place in the journal.

The roller and the context are text of 1 to 320 and 1 to 1 024 bytes without a control character. The roller is public, in the entry of the commitment: give an opaque reference, a seat or a member identifier, never a name or an address. The context is sealed with the result and revealed with it.

What the table sees

MomentThe table seesThe store keeps sealed
The roll is madeThe entry committed: the roll's identifier, its commitment, the roller and the timeThe opening: expression, rounding, seed, result, context
The roll is revealedThe entry revealed: the whole openingThe opening, still
The scene is closedThe entry closedThe openings of the rolls withheld

The commitment tells nothing of the result: the 32 secret bytes of the seed are hashed with it.

Revealing

$rolls->reveal('scene:12', $roll, $otherRoll);   // entries `revealed`, in the order given, each roll once

A roll is revealed once; a roll the scene did not commit, or whose opening the store no longer keeps, is refused, and nothing of the call is appended when one roll is refused. hidden($roll) gives the game master the sealed roll again.

Closing a scene

$rolls->close('scene:12', $roll);        // reveals the rolls given — a partial reveal —, then closes
$rolls->close('scene:12');               // reveals none, then closes
$rolls->closeRevealingAll('scene:12');   // reveals every roll not revealed yet, then closes

A closed scene takes no more rolls and no second closing. The rolls it did not reveal stay withheld, listed by SceneJournal::withheld(), their commitments in the journal: reveal() still publishes them after the scene, when the game master chooses to.

Limits

A scene takes a roll while its journal holds fewer than 4 999 entries (SceneJournal::MAX_ROLLS), so that a journal — each roll committed and revealed once, and the closing — never holds more than 9 999 entries, and reading or checking it stays bounded. The expression is read within the limits of the DiceRoller; an opening's expression is at most 4 096 bytes.