Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld bf18e270-2…

CascadeCore

The simulation, and nothing else. This module owns no engine API: it is pure Luau over flat arrays, so the same code steps headless (baseline strategies, batch experiments) at the speed it steps on screen.

by◐m3rck·posted 21d ago
What it does

CascadeCore — the model

The simulation, and nothing else. This module owns no engine API: it is pure Luau over flat arrays, so the same code steps headless (baseline strategies, batch experiments) at the speed it steps on screen.

Shape

local Core = require("CascadeCore")

local sim = Core.new({ width = 256, height = 256, seed = 42, params = {} })
for _ = 1, 1000 do
    Core.step(sim)
    Core.afterTick(sim, 1 / 25)
end
print(Core.summary(sim).efficiency)

Core.step advances the model by one tick. Core.afterTick does the bookkeeping a driver wants at its own cadence — production accrual, connectivity, pressure, history samples — and takes the wall-clock seconds since the last call. A caller that only wants the physics can skip it.

Cells

EMPTY 0 · OCCUPIED 1 · ACTIVE 2 · COOLDOWN 3 · BLOCKED 4

The board is a flat array padded by one BLOCKED ring, so a neighbour is a plain index offset and no propagation step ever needs a bounds check. Core.idx(sim, x, y) maps board coordinates to that index.

Propagating the grid

neighbor_mode (4 or 8) fixes the neighbourhood. propagation_mode picks how fire crosses it:

  1. adjacency — every occupied neighbour ignites
  2. probabilistic — each at spread_probability
  3. distance — within spread_radius, probability falling with distance
  4. directional — probability weighted toward direction_x/y, with direction_bias as the floor a cell keeps against the flow

Adding a model means adding a branch to Core.rebuildNeighbors, which fills each offset's probability once per parameter change rather than per tick.

Performance

The tick is written for the common case: one pass over the board for decay, trigger and growth, then work proportional to the burning cells rather than the whole grid. Two things matter and are easy to get wrong:

  • The generator is unrolled into the loop. A tick draws a number for most cells on the board, and at this VM's call costs a bit32-based xorshift made the simulation three times slower than the arithmetic it was producing. See CascadeRng.
  • Thresholds are integers. A probability is stored pre-multiplied by the generator's modulus, so the hot comparison never pays for a float divide.

Measured: ~5.4 ms per tick at 256×256 on a sparse board, ~7.3 ms at the density the model settles at.

system_pressure (Phase 21)

A smoothed read of density, largest-component ratio and recent cascade activity. It is observational only. Nothing in this module reads it to decide an outcome — catastrophic events remain emergent consequences of the cellular system, and there is no branch anywhere that spawns one.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

CascadeCore — the simulation itself. Deliberately generic: nothing in here knows about forests or fire. Cells carry a potential, disturbances propagate through connectivity, and the world self-organises toward a state where large cascades are inevitable. The same engine models infection, grid failure, insurgency, contagion. This module owns NO engine API. It is pure Luau over flat arrays, so it can be stepped headless (baseline agents, batch experiments) at the same speed it steps on screen.

clamp(v: number, lo: number, hi: number) → number

argtypedescription
vnumber
lonumber
hinumber

mergeParams(over: any) → any

argtypedescription
overany

idx(sim: any, x: number, y: number) → number

--!desc The flat index of grid coordinate (x, y). The array is padded by --! one BLOCKED ring, so neighbours are a plain index offset and no --! propagation step ever needs a bounds check.

argtypedescription
simany
xnumber
ynumber

classify(sim: any, size: number) → string

--!desc Classify a completed cascade by its size.

argtypedescription
simany
sizenumber

new(opts: any) → any

--!desc Build a fresh simulation. --!arg opts { width, height, seed, params } --!return The simulation state table.

argtypedescription
optsany

rebuildNeighbors(sim: any) → void

--!desc Rebuild the offset tables from the current neighbour mode and --! propagation model.

argtypedescription
simany

buildSet(radius: number, chebyshev: boolean) → any

argtypedescription
radiusnumber
chebyshevboolean

reset(sim: any, seed: number?) → void

--!desc Clear the board, seed the world, and reset every tally. The run --! clock, energy and statistics all start over.

argtypedescription
simany
seednumber?

recount(sim: any) → void

--!desc Recount every cell state. Cheap enough to run after a reset or a --! bulk edit, and the safety net against incremental drift.

argtypedescription
simany

generateTerrain(sim: any) → void

--!desc Lay down BLOCKED terrain (Phase 16). Rivers, walls, ridges and a --! couple of roads, all from the barrier parameters and seed.

argtypedescription
simany

fbm(x: number, y: number, seed: number, octaves: number) → number

--!desc Fractal value noise in [0, 1]. The one procedural primitive every --! field in this module is built from.

argtypedescription
xnumber
ynumber
seednumber
octavesnumber

vnoise(x: number, y: number, seed: number) → number

--!desc Smoothed value noise in [0, 1].

argtypedescription
xnumber
ynumber
seednumber

buildGrowthField(sim: any) → void

--!desc Build the spatially varying growth field (Phase 15).

argtypedescription
simany

rebuildFields(sim: any) → void

--!desc Recompose growth / spread modifier fields from the live zone list --! (Phase 19). Run on any zone change, not per tick.

argtypedescription
simany

placeZone(sim: any, kind: string, cx: number, cy: number, radius: number, ticks: number, strength: number) → any

--!desc Place a timed intervention zone (Phase 19). Returns the zone, or --! nil when the brush landed outside the board.

argtypedescription
simany
kindstring
cxnumber
cynumber
radiusnumber
ticksnumber
strengthnumber

expireZones(sim: any) → void

argtypedescription
simany

newCascade(sim: any, source: string) → any

Merge policy: when two live cascades meet, the LOWEST cascade id survives and absorbs the other; the absorbed record is retired without being counted as an event of its own, so its cells are counted exactly once — inside the surviving cascade. A merged cascade inherits PLAYER as its trigger source if either participant was player-started.

argtypedescription
simany
sourcestring

resolveId(sim: any, id: number) → number

argtypedescription
simany
idnumber

pushBurning(sim: any, i: number) → void

argtypedescription
simany
inumber

removeBurning(sim: any, i: number) → void

argtypedescription
simany
inumber

finalizeCascade(sim: any, rec: any) → void

argtypedescription
simany
recany

igniteInto(sim: any, i: number, rec: any) → void

argtypedescription
simany
inumber
recany

mergeCascades(sim: any, keepId: number, dropId: number) → void

argtypedescription
simany
keepIdnumber
dropIdnumber

setAdaptive(sim: any, enabled: boolean, rampSeconds: number?) → void

--!desc Turn the difficulty ramp on or off. Enabling snapshots the five --! parameters it drives; disabling restores them exactly. --!arg rampSeconds How long the ramp takes to reach its full strength.

argtypedescription
simany
enabledboolean
rampSecondsnumber?

updateDifficulty(sim: any) → void

--!desc Apply the ramp. Called per tick; the change per call is tiny by --! design, so the world never jumps.

argtypedescription
simany

step(sim: any) → void

--!desc Advance the world by one simulation tick.

argtypedescription
simany

afterTick(sim: any, dtSeconds: number) → void

--!desc Per-tick housekeeping the driver schedules at its own cadence: --! production accrual, connectivity, pressure, history sampling. --!arg dtSeconds Wall-clock seconds since the last call.

argtypedescription
simany
dtSecondsnumber

computeConnectivity(sim: any) → void

--!desc Label every connected region of OCCUPIED cells and record the --! largest, the second largest and the component count.

argtypedescription
simany

triggerBrush(sim: any, cx: number, cy: number, radius: number, source: string?) → number

--!desc Ignite every OCCUPIED cell inside a circular brush. The burn --! propagates by exactly the same rules as a spontaneous one — only the --! trigger source differs. --!return Number of cells ignited.

argtypedescription
simany
cxnumber
cynumber
radiusnumber
sourcestring?

removeBrush(sim: any, cx: number, cy: number, radius: number) → number

--!desc Remove cells inside a circular brush, cutting connectivity without --! lighting anything. OCCUPIED and COOLDOWN cells become EMPTY; live --! burners inside the brush are extinguished. --!return Number of cells removed.

argtypedescription
simany
cxnumber
cynumber
radiusnumber

retainedCascades(sim: any) → any

--!desc Every completed cascade still retained, oldest first. The ring is --! walked in time order so callers never see it wrap.

argtypedescription
simany

sizeStats(sim: any) → any

--!desc Size percentiles over the retained cascade window. --!return { n, mean, median, p90, p95, p99, max, min }

argtypedescription
simany

pick(q: number) → number

argtypedescription
qnumber

sizeHistogram(sim: any) → any

--!desc Log-binned histogram of cascade size (Phase 11). One bucket per --! doubling, which is the only binning that shows a heavy tail honestly. --!return { bins = { { lo, hi, count } }, total, maxSize }

argtypedescription
simany

runHeadless(sim: any, ticks: number, policy: any, decideEvery: number?) → any

--!desc Run `ticks` simulation ticks with no rendering and no player, so a --! strategy can be measured far faster than it can be watched. --!arg policy Optional { decide(sim, tick) -> action } hook, called every --! `decideEvery` ticks. --!return A summary table of the run.

argtypedescription
simany
ticksnumber
policyany
decideEverynumber?

summary(sim: any) → any

--!desc The run's headline numbers, for the dashboard and the agents.

argtypedescription
simany

efficiencyScore(sim: any) → number

--!desc The single headline score, kept strictly beside every raw number --! it is derived from (Phase 7): nothing is hidden behind it.

argtypedescription
simany

pack(r: number, g: number, b: number, a: number) → number

One u32 write per cell into a shared RGBA buffer, which the driver hands to the GPU texture. The colour for each state is packed once; a plain frame is then a table read and a store per cell.

argtypedescription
rnumber
gnumber
bnumber
anumber

renderInto(sim: any, buf: any, opts: any) → any

--!desc Rasterise the board into an RGBA byte buffer. --!arg opts { connectivity, minCompSize, cursor = { x, y, r, kind }, flash }

argtypedescription
simany
bufany
optsany

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

7items
▣
module · born here
❒asset
# CascadeRng — deterministic pseudo-random numbers Seed-reproducible: the same seed, the same parameters and the same player actions replay the same run. ```lua local Rng = require("CascadeRng") local r = Rng.new(20250922) r.next() -- a float in [0, 1) r.int(1, 6) -- an integer in [1, 6] r.chance(0.3) -- true 30% of the time r.getState() -- snapshot the stream position ``` ## Why it is arithmetic only The obvious choice — a xorshift over `bit32` — costs about half a microsecond per draw in this VM, because every `bit32` call crosses the native boundary. A tick makes roughly one draw per cell, so that alone was **three times the cost of the entire simulation**. The Lehmer (Park–Miller) generator here is one multiply and a remainder, both exact in a double because the product stays inside 2^53: s = (s * 48271 + 11) % 2147483647 Measured at roughly ten nanoseconds per draw — fifty times cheaper than the xorshift, with the same statistical job to do. ## Period 2^31 − 2 draws, which is about twenty-five minutes of continuous simulation at full rate on a 256×256 board. Past that the stream repeats; the run is still fully deterministic, and a cellular automaton this chaotic has long since lost any resemblance to the earlier pass. ## `hash2` The same trade, for the procedural fields: a few multiplies and a remainder with every intermediate inside 2^53, so the growth field and the terrain generate identically on every machine and cost nothing.
▲ 0↑ born
backing path · CascadeCore.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.