Skip to content

Randomness

Canwu draws random numbers from named, versioned random streams. Each stream belongs to the mechanic that declared it, and every draw is recorded. The same run inputs therefore always produce the same results, and a reload cannot reroll an outcome. Read this page before you add randomness to a plugin.

A boundary system draws from a declared stream; the draw is recorded, kept or rolled back with the boundary, and recomputed on replay. View diagram source.
View diagram source
flowchart LR
  System["Boundary system<br/>declares the stream"] --> Draw["random_range or<br/>random_sample_for_operation"]
  Draw --> Record["RandomDrawRecord<br/>stream, address, bound,<br/>value, purpose"]
  Record --> Commit{"Boundary<br/>commits?"}
  Commit -- "yes" --> Saved["Snapshot and<br/>boundary evidence"]
  Commit -- "no" --> Undo["Draws and stream<br/>positions roll back"]
  Saved --> Replay["Replay recomputes<br/>each draw and compares"]

A RandomStreamKey has a namespace, a name, and a version, for example:

canwu.core / knowledge-report-delay / 1

The kernel derives each stream’s seed from the run’s root seed and the stream key. Each mechanic draws from its own stream, so adding a draw to one system leaves every other system’s numbers unchanged. The stream state stores the algorithm, the derived seed, the position, and the generator state.

New streams use SplitMix64V2, which reduces a raw value to the requested range with unbiased rejection sampling. Streams that recorded SplitMix64V1 keep its modulo reduction, so old journals replay with their original values. The algorithm is part of the saved format: to change how a stream draws, publish a new stream version.

A boundary system may draw only from streams it lists in BoundarySystemContract::random_streams. When the system runs, the kernel gives it a random session holding copies of those streams. A draw from any other stream fails with UndeclaredRandomStream.

Every draw needs:

  • an upper bound greater than zero;
  • a purpose string that is non-empty and has no leading or trailing whitespace;
  • for a sequential draw, room to advance the stream position.

The system receives a bounded integer; the generator state stays inside the kernel.

A sequential draw, SimulationView::random_range, takes the next value from the stream. That is enough when a system draws in a fixed order. A mechanic that may retry, roll back, or continue across phases should use random_sample_for_operation (or random_range_for_operation, which returns only the value) instead.

An operation-keyed draw is addressed by the operation it serves. Its address combines the producing plugin, an operation kind, the application’s operation ID, a target, and a draw slot. The target is a RandomOperationTarget: an entity, an exact decision ticket version, an exact domain record version, a knowledge holder, or a canonical key. The draw also cites the evidence that caused it. The kernel computes the value from the root seed, the stream, the address, the bound, and the purpose, so the result is the same however many draws came before. Asking again with the same address, evidence, bound, and purpose returns the same value. Reusing an address with different evidence, bound, or purpose fails with RandomOperationConflict.

Random decision policy uses this path. RandomOperationTarget::DecisionTicket binds the draw to an exact ticket version, DecisionOptionWeight maps the drawn value to one of the ticket’s existing options, and the ResolveDecisionRandomly directive makes the kernel generate an ordinary decision for the next boundary. A guarded utility policy’s random tie-break uses the same path and draws only among the near-equal top options.

A controller’s policy kind (Utility, Rule, Random, Human, External, or Llm) is fixed when the controller is registered; registering the same controller ID again fails. Random decision policy only chooses among a ticket’s options. Weather, breakdowns, epidemics, and similar world events belong in domain systems that draw from their own streams.

Every draw records a RandomDrawRecord:

Field Meaning
stream and address The versioned stream, plus a sequential position or an operation address
upper_exclusive and value The requested bound and the returned value
purpose Why the mechanic drew
producer The boundary system or core system that drew
outcome What the draw decided, for example a decision selection or a report delivery
cause and correlation_id The causal source and the processing chain

Draw records and stream state are saved in snapshots and boundary evidence, and are included in hashes. When a boundary fails, its stream positions and draw records roll back with it, so a failed attempt consumes no randomness.

Reloading, alternative realities, and repeated trials

Section titled “Reloading, alternative realities, and repeated trials”

Deterministic replay removes the easy reroll. The same authoritative state, command or decision input, plugin semantic environment, and simulation time produce the same draw. Reloading a snapshot from before an event and replaying the same inputs therefore gives the event the same outcome.

To try a different command from the same snapshot, fork() the run and submit it there. The result is an alternative reality: a new causal history, distinct from an exact replay of the original run. The host application’s save policy decides whether players get manual saves, single-writer saves, or research forks.

Where first-party crates use randomness, and where they deliberately don’t:

  • canwu-movement makes no draws. An application system decides that a leg failed, a route closed, or someone seized custody, usually with its own operation-keyed draw. It reports the result through the movement_incident_v1 ingress, citing an exact evidence record.
  • canwu-correspondence draws on its own stream, canwu-correspondence / operation-resolution / 1. For an incident request such as an interception, a disaster, or a carrier seizure, the application supplies a probability_per_mille, and the plugin makes one operation-keyed draw to decide whether the incident happens. The same stream selects communication opportunities and their recipients.
  • Whether a forgery is detected, behind an authenticity finding, is decided by the application; canwu-information records the finding.
  • Capacity booking allocation follows a fixed order: priority, window start, tie-break key, admission sequence, and booking identity.
  • Tie-breaks in unit-block law stages are rules. When as many blocks vote For as Against, status-quo adds no block, and casting-seat:<seat> adds one For block if that seat itself voted For.
  1. Give each independent mechanic its own stable, named stream.
  2. Write the reason for the draw in purpose, so debug and replay output can explain the result.
  3. Review stream declarations together with the plugin version and semantic hash.
  4. Do not use wall time, thread order, or container iteration order as a seed or a tie-break.
  5. Model unknown facts, actor choices, discrete incidents, and aggregate rates as separate mechanisms; do not fill in missing facts with random draws.

Exact replay requires the stream identities, algorithm versions, root seed, sequential positions, operation addresses, every draw, and every final outcome to match. When loading a snapshot, the kernel checks each sequential stream’s state against its seed advanced by the recorded number of draws, and recomputes every operation-keyed value. Boundary replay also compares each draw’s producer, purpose, cause, and correlation.