Narratomidocs
State and logic

Randomness

The specification of Narratomi's seeded generator, what replays identically, and how the Check beat consumes it.

Chance in a Narratomi story comes from one place: a seeded pseudo-random generator whose entire state is a single 32-bit number carried in the variable snapshot. Nothing on the roll path reads the clock, the crypto pool, or a global generator. That is what lets a save resume mid-sequence and get the rolls the reader would have got.

This page is the specification. If you are writing a runtime of your own against a downloaded bundle, reproduce it exactly or your playthroughs will diverge from ours.

The generator

mulberry32. One unsigned 32-bit word of state, four integer operations, the same result on every JavaScript engine (Math.imul is an exact 32-bit multiply).

function nextRoll(state) {
  const next = (state + 0x6d2b79f5) | 0;
  let t = Math.imul(next ^ (next >>> 15), 1 | next);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return { state: next, roll: ((t ^ (t >>> 14)) >>> 0) / 4294967296 };
}

roll is in the half-open interval [0, 1). state is both the seed and the position: after n rolls it is the position n steps along, and it round-trips through JSON as an ordinary number, so there is no second field to keep in step with it.

Seeding

The seed is drawn once, when a playthrough starts, from crypto.getRandomValues(new Uint32Array(1))[0]. That is the only entropy in the whole system. It is stored in the variable snapshot under the reserved key @rng, which no author can declare (a variable name starting with @ is a compile error).

There is no seed field in the editor. Rolling back to a known seed is a testing affordance in the runtime API, not an authoring surface.

Two moments can mint a seed:

  • Starting a story. Every new playthrough gets a fresh seed, so two playthroughs of the same story do not share a roll sequence.
  • Resuming a save that has no numeric @rng. Only very old saves, written before seeded randomness existed, are in this state. Such a save reloads to different rolls each time. Every save written since then resumes the sequence it left.

How the Check beat uses it

A Check beat is a roll against odds you author, routing to one of two edges. When the runtime reaches one, in order:

  1. It reads @rng from the snapshot (a missing or non-numeric value reads as 0) and takes one step of the generator.
  2. It writes the new position back to @rng. One check consumes exactly one step.
  3. It resolves the beat's chance: either a literal between 0 and 1, or the live value of a bound number variable, clamped to 0 and 1. A chance bound to a variable that is missing or is not a number reads as 0.
  4. The check passes when roll < chance. So chance 0 never passes and chance 1 always does.
  5. Play crosses successEdgeId or failureEdgeId immediately.

The runtime never rests on a check beat: it rolls and crosses in the same pass, so no state you can hold points at an unrolled check, and re-rendering cannot roll again. Settling is a pure function of the state handed to it, so running it twice on the same state produces the same roll and the same route. Only moving the story forward consumes another step.

The runtime also reports which way the last transition's check landed, which is how the player's transcript can say "The check succeeded." Where play landed cannot answer that question, because both edges may lead to the same scene.

Binding a check's chance to a variable that is not a number is CHECK_BINDING_TYPE_MISMATCH at compile time. Without that check it would read as 0 and the roll would silently always fail.

What replays identically

Given the same bundle, the same starting seed, and the same sequence of player decisions, a playthrough is deterministic. Every roll, every route, and every variable value comes out the same. That is the guarantee saves rely on: a save carries the whole snapshot, @rng included, so loading it and playing on gives the reader the rolls they would have got without reloading.

What is not guaranteed:

  • A new playthrough. New game means a new seed. Nothing about the previous run's rolls survives, by design.
  • A different bundle. Publishing a new version can change how many checks sit between two points, which shifts every later roll along the sequence. Saves are pinned to the bundle version that wrote them.
  • Anything driven by the player's own timing. A timed choice that lapses routes differently from one answered in time. That is real, and it is not random; it just is not replayable from the seed alone.
  • Cross-playthrough carryover of the generator. @rng is per playthrough. The browser's persistent state is built by walking declared variables, and no declared name can start with @, so the position can never leak into the next run.

If you want something random that is not a check

There is no random operation on the Set beat and no shuffle. The Check beat is the only consumer of the generator. To pick between three flavors of a scene at random, write a check (or a chain of them) and route the edges.

On this page