Public task and development documentation. Official evaluation boundary.
Simulation contract (simulator version 0.2.0)
This document is normative. If the code and this document disagree, it is a bug. The V1 fairness contract, the model-visible observation and the rationale for each frozen rule are in v1-fairness-spec.md.
Coordinates and neighbourhood
- A position is
[row, col], and[0, 0]is the top-left cell. northis row − 1,southis row + 1,westis col − 1,eastis col + 1.- The neighbourhood is von Neumann (4-connected). Diagonal moves and diagonal fire spread do not exist.
- Canonical order is row-major (sort by row, then by column). Person and exit IDs sort as plain strings by code point, so
"P10" < "P2".
State
A simulation state snapshot contains: step, every person (id, position, status),
the burning cells, the trapped set, the RNG stream positions, terminated and
termination_reason. Snapshots are frozen dataclasses. Two equal snapshots
serialise to identical canonical JSON. A simulation state is evaluator-private. A
decision model sees only the observation (see
v1-fairness-spec.md).
The person status is one of active, evacuated or casualty. A resolved
person stays in the record with its last position, but it does not occupy a
cell any more and must not receive actions.
At the start of every step, no active person stands on an exit or on a burning cell.
Phase order
Each simulator step runs these phases in this order:
| # | Phase | Rule |
|---|---|---|
| 1 | Observation | Simulator observation step returns the pre-step model-visible observation: step, static map, episode rules, people, burning cells and allowed actions. It never includes the seed, RNG state, hidden scenario metadata or the trapped set. |
| 2 | Actions | The mapping must have exactly one entry per active person. Values are stay/north/south/east/west (or action). Anything else raises an invalid action error, and the state does not change. |
| 3 | Movement validation | stay gives stayed. A move off the grid gives blocked_boundary. A move into an obstacle gives blocked_wall. A move into a burning cell gives blocked_fire if movement.enter_fire = "blocked". Otherwise the move continues. |
| 4 | Collision resolution | See Collisions. |
| 5 | Movement | All moves resolved as moved happen at once. |
| 6 | Evacuation | Every active person on an exit that was not burning at the start of the step is evacuated and recorded with the exit ID. |
| 7 | Fire propagation | The fire model returns newly ignited cells for step number t (1-based). |
| 8 | Casualties and trapping | Every still-active person on a burning cell becomes a casualty. The cause is entered_fire if the cell was burning before phase 7, else fire_spread. Then the trapped set is recomputed. |
| 9 | Termination | If no person is active, the reason is all_resolved. Else, if t >= termination.max_steps, the reason is max_steps. all_resolved wins when both hold. A terminated simulator raises a simulation error on simulator step. |
Consequences of the order:
- Evacuation (6) comes before fire spread (7). A person who reaches an exit in the same step that the fire reaches it gets out.
- Casualties (8) come after fire spread (7). A person who does not move away from a cell that ignites this step dies.
Collisions
Collision resolution uses these rules in this order. Each rule only looks at movers that are still unresolved.
- Occupied by a non-mover. If the target holds a person who is not trying to
move (
stay, or blocked in phase 3), the move givesblocked_occupied. - Same target. If two or more movers target one cell, the lowest ID wins.
Every other mover gives
blocked_conflict. This is decided on intent only. A loser is not re-awarded the cell if the winner fails later. - Chains. A winner moves if its target is empty, or if the target's occupant
moves out successfully in the same step. People can follow each other in one
step (
a b . E: both move east). If the chain ends at a person who does not move, every mover in the chain givesblocked_occupied. - Cycles. Swaps (
A <-> B) and longer rotations are never allowed. Every member givesblocked_cycle.
A blocked person stays in its cell. The outcome does not depend on the order in which actions are submitted.
Exits are normal cells for movement. Each exit cell therefore lets at most one person through per step: two people who target the same exit are a normal conflict. Because people leave in phase 6, an exit is always empty at the start of the next step.
Entering fire. movement.enter_fire is either blocked or casualty. With
blocked, the move into fire is refused (blocked_fire). With casualty, the
person moves and dies in phase 8 (entered_fire). This includes a burning exit:
a burning exit never evacuates anyone.
Fire
Contract:
- The model returns only cells that are not burning and that are flammable. The simulator checks this and raises a simulation error on a violation.
- Walls never burn, and fire never passes through them. Spread is only into flammable 4-neighbours of burning cells.
- Exits burn only if
fire.exits_flammable = true. A burning exit stays burning and cannot evacuate anyone. Withfalse, exits never ignite. Validation then rejects an ignition point on an exit. - Cells burn forever (no burn-out in V1). People do not block fire.
- Spread happens only on steps where
step % fire.spread_interval == 0. - Randomness comes only from the passed
rng, which is the simulator'sfirestream. The model draws in canonical order.
V1 models:
deterministic: on a spread step, every flammable neighbour of a burning cell ignites.stochastic: on a spread step, each (candidate cell, burning neighbour) edge takes one Bernoulli(spread_probability) draw. The order is candidates row-major, then sources row-major. A candidate ignites if any of its edges succeeds. All draws are always taken, so the stream position depends only on the geometry. Withp = 1the result equalsdeterministic. Withp = 0there is no spread.
Stochastic fire (frozen V1 contract)
Frontier. Before a spread step t, let B be the burning set. A
candidate is a cell c that is not in B, is not a wall, is not a
non-flammable exit, and has at least one 4-neighbour in B. An edge is a
pair (c, s) with c a candidate and s a burning 4-neighbour of c. A
candidate has k edges, with 1 <= k <= 4.
Draw order. Edges are visited in canonical order: candidates in row-major
order, then, for each candidate, its sources in row-major order. Each edge
takes exactly one bernoulli(p) from the simulator's fire stream, with
p = fire.spread_probability. c ignites in step t if at least one of its
edges succeeds. Every edge of every candidate is drawn, also after an earlier
edge of the same candidate succeeded.
Probability. The draws for the k edges of c are consecutive outputs
of the specified PRNG stream, so under the model of independent Bernoulli(p)
draws:
P(c ignites at step t | B) = 1 - (1 - p)^k
p = 0gives no spread.p = 1gives exactly thedeterministicresult on spread steps.- "Independent" means sequential draws from the specified SplitMix64 stream. It is a modelling statement about the generator, not a claim of physical independence between neighbouring flames.
- Exactly:
bernoulli(p)isu / 2**53 < p, whereu = next_u64() >> 11takes the values0 .. 2**53 - 1. So exactlyceil(p * 2**53)of the2**53values succeed. Forp = 0this is none, and forp = 1it is all.
Draw-consumption rule. Let E_t be the number of edges of the frontier of
B before step t. The fire stream advances by exactly
draws(t) = E_t if t % spread_interval == 0
draws(t) = 0 otherwise
for every p in [0, 1], including p = 0 and p = 1. No candidates means
zero draws. The deterministic model never draws. Actions, people, action
mapping order and the construction order of B cannot change the stream
position. SplitMix64 adds a fixed constant to its state per draw, so the draw
count between two recorded rng_state values can be computed exactly; the
tests do this for every step of recorded runs.
Each pins the scenario hash, seed, ignited cells per step, draws per step,
final burning set and final fire RNG state. The same values must come out on
every platform and Python version.
Not in V1
V1 fire semantics, the scenario schema and the model-visible rules view are frozen. V1 does not support, and fire config and rules view have no fields for:
- delayed ignition events after step 0 (all ignitions burn at step 0);
- wind or other directional spread probabilities;
- burn-out (cells burn until the end of the run, R8);
- smoke or visibility effects;
- material-specific flammability (every non-wall cell is flammable, except
exits when
exits_flammableis false); - fire that depends on people or actions.
These are future-version features. Each one changes simulator output or the model-visible rules, so it needs a new simulator version and, where the rules change, a new observation version and scenario schema version. Issue #7 mentions optional directional parameters and delayed-ignition behaviour; the decision is to defer both to a future version to keep the frozen V1 contract.
Randomness
- The only generator is SplitMix64. It is fully specified in code, and a test checks it against the reference output.
- Stream seeds are
sha256("emberbench/v1/{seed}/{label}")[:8](big-endian). V1 has one stream,fire. A new subsystem gets a new label, so it does not shift the existing streams. bernoulli(p)always consumes exactly one draw and compares a 53-bit dyadic float withp. It uses no other floating-point arithmetic.- The stream positions are part of simulation state (for audit and resume). They are not part of observation.
- The fire stream is exogenous: the fire model never sees people or actions. For the same scenario and seed, every action sequence gives the same fire trajectory on the steps that the runs share.
Trapping
trapped is the set of active people with no path to a non-burning exit. A
path may cross any cell that is not a wall and not burning. Other people are
ignored. This is an evaluator diagnostic for later metrics. It is not in the
observation. It does not change the person's status or end the run.
Replay
Run record stores the header (simulator_version, observation_version,
schema_version, scenario_id, scenario_hash, seed), the initial state, and for each step
the actions, resolved movements, ignitions, evacuations, casualties and the
post-step state. The replay check checks the simulator version, the
observation version and the scenario hash. It then replays the recorded actions and requires a byte-identical
canonical encoding. It reports the first step that diverges.
Reviewed public benchmark contract. Apache License 2.0. Download Markdown