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 draw, from request to replay
Section titled “A draw, from request to replay”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"]
Stream identity
Section titled “Stream identity”A RandomStreamKey has a namespace, a name, and a version, for example:
canwu.core / knowledge-report-delay / 1The 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.
Declare before drawing
Section titled “Declare before drawing”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
purposestring 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.
Operation-keyed random draws
Section titled “Operation-keyed random draws”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.
Draw evidence
Section titled “Draw evidence”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.
Randomness in first-party extensions
Section titled “Randomness in first-party extensions”Where first-party crates use randomness, and where they deliberately don’t:
canwu-movementmakes 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 themovement_incident_v1ingress, citing an exact evidence record.canwu-correspondencedraws 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 aprobability_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-informationrecords 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
ForasAgainst,status-quoadds no block, andcasting-seat:<seat>adds oneForblock if that seat itself votedFor.
Guidance for plugin authors
Section titled “Guidance for plugin authors”- Give each independent mechanic its own stable, named stream.
- Write the reason for the draw in
purpose, so debug and replay output can explain the result. - Review stream declarations together with the plugin version and semantic hash.
- Do not use wall time, thread order, or container iteration order as a seed or a tie-break.
- Model unknown facts, actor choices, discrete incidents, and aggregate rates as separate mechanisms; do not fill in missing facts with random draws.
Replay contract
Section titled “Replay contract”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.