CascadeSim
One entity carries the whole laboratory: the simulation, the GPU texture the board is drawn into, the screen, the input and the readouts. Put it on an entity and the world becomes an instrument for watching a self-organising system approach dangerous connectivity.
CascadeSim — the instrument
One entity carries the whole laboratory: the simulation, the GPU texture the board is drawn into, the screen, the input and the readouts. Put it on an entity and the world becomes an instrument for watching a self-organising system approach dangerous connectivity.
entity.spawn("Sim").component.add("CascadeSim", {
gridSize = 256,
seed = 20250922,
tickRate = 25,
})
What it is
A Drossel–Schwabl forest-fire model in the general sense: cells accumulate connectivity, small disturbances happen often, large cascades happen rarely, and the world spends most of its time near the density where both are true.
Nothing in the model knows about forests. The cell states are
EMPTY → OCCUPIED → ACTIVE → COOLDOWN → EMPTY with BLOCKED for terrain, and
the same machine stands in for infection, grid failure, contagion, insurgency
or traffic, because all of those are the same rule: a neighbourhood that
accumulates connectivity eventually discharges it all at once.
The design question the instrument exists to answer:
Can a player learn to recognise when a self-organising system is approaching dangerous connectivity, and intervene intelligently before a huge cascade?
The run
gridSize sets the board, seed fixes the run, tickRate sets simulation
ticks per second of wall clock. Everything else is a live parameter — see
getParams() and setParam(name, value); changing one takes effect on the
next tick, and no reload is needed.
The simulation is per-peer. Each participant drives their own world locally; two people in the world are running two experiments on the same seed, which is the point of a laboratory.
Controls (active in PLAY — press Play, or set interactiveInEdit)
| Input | What it does |
|---|---|
SPACE | pause / resume |
R | reset the board, same seed |
N | advance one tick while paused |
+ / - | time scale (×2 / ÷2, 0.25× … 64×) |
C | connectivity view — shade the board by connected region |
H | cycle the readout: Full Debug → Player Information → Minimal |
1 … 6 | arm a tool |
| LMB | apply the armed tool at the brush |
| RMB | firebreak — always the firebreak, whatever is armed |
| wheel | brush radius |
F | new seed |
G | replay the same seed |
V | environmental feedback on / off |
P | regrowth memory (Phase 17) on / off |
T | cycle the propagation model |
Y | adaptive difficulty on / off |
O | cycle the objective, or none |
E | export the run (CSV + JSON) |
B | run the baseline strategies headless, in the background |
, / . | smaller / larger grid (128 … 1024) |
The six tools
| # | Tool | Cost | Character |
|---|---|---|---|
| 1 | Trigger | low, fixed | cause a cascade now, on purpose |
| 2 | Firebreak | per cell removed | break connectivity without lighting anything |
| 3 | Temporary Barrier | moderate | propagation is zero inside it, for N seconds |
| 4 | Suppression Zone | moderate | spread probability scaled down locally |
| 5 | Growth Boost | moderate | more production — and more future risk |
| 6 | Growth Suppression | moderate | less production, less connectivity |
Each has its own cooldown, charged only when it actually fires. Energy regenerates; the player is never able to control everything.
What the model does, precisely
Each tick, in order:
- decay —
COOLDOWNcells count down and becomeEMPTY - trigger — each
OCCUPIEDcell burns on its own with probabilityspontaneous_trigger_probability - growth — each
EMPTYcell becomesOCCUPIEDwith probabilitygrowth_probability × growth_field(x, y) - propagation — every burning cell ignites neighbours according to the
propagation model, each at
spread_probabilityscaled by any suppression zone and by the model's own falloff - merge — two live cascades that touch become one (policy below)
- burnout — the cells that burned this tick go to
COOLDOWN
Cascade identity and the merge policy
Every spontaneous or player-made trigger mints a new cascade id; every cell ignited by propagation inherits the id of whatever ignited it. A cascade ends when it has no burning cells left, and is then recorded.
When two live cascades meet, the LOWEST cascade id survives. The absorbed
record is retired without being counted as an event of its own, so its cells
are counted exactly once — inside the survivor. A merged cascade inherits
PLAYER as its trigger if either participant was player-started, and counts
the merge in merges.
Merging is common, not exceptional: at the density this model settles at, new
fires land next to old ones constantly. Merged cascades in the readout is the
size of that effect, and it is worth watching — a run where merges outnumber
events is a run where "one cascade" and "one decision" have come apart.
What cells_affected counts
Two numbers, because they answer different questions:
ignitions— how many times this cascade set something alight. A cascade that outlives the regrowth of the ground behind it burns the same cell twice and counts it twice. This is the consumption count.cells— cells this cascade has consumed, counted once each. This is the field the readout, the event classification and the damage all use.
Tracked per cell as a cascade stamp resolved through the merge aliases. It is exact for ground a cascade reaches once; where two cascades have passed over the same cell and it later burns again, the stamp belongs to the more recent one and the earlier cascade can count it once more. That is the honest limitation of tracking events that merge, and it is small at the sizes most events reach.
Event classification (Phase 8)
SMALL under 25 · MEDIUM 25–249 · LARGE 250–999 · CATASTROPHIC 1000+.
Thresholds are parameters. A catastrophic event rings the board in red, is
recorded, and is displayed briefly. It does not end the run — the recovery
period afterwards is part of the experiment.
What the instrument has measured
Run at 256×256, seed 20250922, 2000 ticks (≈80 s of simulated time), default parameters, no intervention:
| Average density | 41.6% (peak 57.6%) |
| Cascades | 1043 — 824 small, 196 medium, 13 large, 10 catastrophic |
| Median size | 5 cells |
| 90th / 99th percentile | 59 / 671 |
| Largest | 41,189 cells |
| Largest connected region seen | 36,843 cells |
| Cost | ~7.3 ms per tick |
The size distribution, one bucket per doubling:
1-1 240
2-3 210
4-7 129
8-15 142
16-31 103
32-63 80
64-127 41
128-255 33
256-511 8
512-1023 4
1024-2047 1
2048-4095 1
4096-8191 4
8192-16383 2
32768-65535 2
Many tiny events, fewer moderate, rare large, very rare enormous — across five orders of magnitude. That is the useful gameplay characteristic the model was chosen for, and it is emergent: nothing in the code produces it.
Baseline strategies (Phase 25), headless, 128×128, 700 ticks, same seed:
| strategy | production | damage | largest | catastrophic | interventions | efficiency |
|---|---|---|---|---|---|---|
| Do Nothing | 4,632,133 | 48,996 | 13,744 | 7 | 0 | 94.5 |
| Random Intervention | 4,627,470 | 48,966 | 13,757 | 8 | 18 | 94.5 |
| Density Threshold | 4,626,309 | 34,725 | 8,411 | 7 | 6 | 133.2 |
| Largest Cluster | 4,460,900 | 34,590 | 14,776 | 6 | 17 | 129.0 |
| Firebreak | 4,597,123 | 38,535 | 12,105 | 7 | 30 | 119.3 |
Intervening helps; intervening at random does not (it costs production for nothing). Targeting the largest connected region holds damage down but leaves the single largest event high — cutting structure is not the same as releasing it. None of these is the right answer; they are the floor a player has to beat.
Performance (Phase 28)
Simulation state is flat arrays of numbers, never one entity per cell; the
board is rasterised into a shared RGBA buffer with one u32 write per cell; and
rendering is entirely separate from simulation state. Measured, unrendered, at
the density the model settles at:
| grid | cells | ms per tick | implied ceiling |
|---|---|---|---|
| 256 × 256 | 65,536 | 7.5 | ~130 ticks/s |
| 512 × 512 | 262,144 | 30.9 | ~32 ticks/s |
| 1024 × 1024 | 1,048,576 | ~124 (extrapolated) | ~8 ticks/s |
Scaling is linear in cell count, so 256×256 runs continuously at 25 ticks/s for around 19% of one core while the editor keeps drawing. 1024×1024 is a batch experiment size, not a watching size — which is what the headless baseline runners are for.
Two optimisations, both from measurement rather than habit, are in
CascadeCore and CascadeRng: a bit32-based xorshift cost about half a
microsecond per draw here, and unrolling the generator into the tick loop made
the whole simulation 2.7× faster; and every probability is stored
pre-multiplied by the generator's modulus so the hot comparison never pays for
a float divide.
Adaptive difficulty (Phase 22)
An optional controller (Y) that ramps the environment over ten minutes of
simulated time: growth and spontaneous disturbance climb, spread rises,
the operator's energy regeneration tightens, and halfway through growth stops
being uniform and starts moving. Disabling it restores the five parameters it
drives, exactly.
It never spawns a larger disaster. What it does is make the system harder to keep below dangerous connectivity, which is the only honest way to raise difficulty in a model whose whole point is that catastrophes are emergent.
Performance (Phase 28)
| grid | cells | ms per tick |
|---|---|---|
| 256 × 256 | 65,536 | 7.5 |
| 512 × 512 | 262,144 | 30.9 |
Linear in cell count; 256² runs continuously at 25 ticks/s for around 19% of
one core. Simulation state is flat arrays, the board is rasterised with one
u32 write per cell, and rendering is separate from simulation state — no
entity per cell anywhere.
Known limitations
- Merging is common at this criticality. A run where merges outnumber events is a run where "one cascade" and "one decision" have come apart; the readout reports merged cascades so that stays visible rather than silent.
cells_affectedis exact for ground a cascade reaches once. Where two cascades have crossed the same cell and it later burns again, the earlier one can count it once more. A separateignitionscount is kept beside it so neither number has to stand for both questions.- No adaptive-difficulty curve has been tuned against a human. It is a working controller with a defensible shape, not a balanced one.
Methods on this component
| Method | What it does |
|---|---|
getParams() | every live parameter |
setParam(name, value) | change one; offset tables and fields rebuild as needed |
getSim() | the live simulation table |
stats() | one flat snapshot of every headline number |
dashboard() | the whole run as a dashboard row |
step(n) | advance n ticks immediately, paused or not |
setPaused(bool) / restart(seed?) / setVisible(bool) | transport |
export() | write CSV + JSON under /source/local/cascade/<seed>/ |
runBaselines(opts) | run the five strategies headless and return their summaries |
Scoped to this part · feeds back into the world's score.