Skip to content

Alerts & logging

Eight threshold checks run at the end of every tick, after every effect has landed — one per kind of bloom among them. Each holds a flag: it writes one line when the reading crosses into trouble, stays quiet while the condition persists, and clears silently when the reading comes back. Alongside them the log collects everything the tank did that a reader would want to see afterwards.

MechanicStatusWhy
The latchsettledOne line per crossing, a flag that lives in state, and no repetition while the condition holds. It does exactly what it says.
Log entry shapesettledFree text for humans with its volumes, temperatures and coverages kept as quantities, a typed event discriminator for code, and a count wherever an entry accounts for more than one organism.
Water-quality thresholdssettledFree NH₃, nitrite, nitrate and oxygen alert at their tolerance edge, and hardiness moves each fish’s own harm edge out from it. Ammonia reads the free fraction, so the total it takes moves with pH and temperature.
Water-level thresholdsettledAlerts where its harm starts, read off the tunable that starts it, so retuning the harm moves the alert with it.
Bloom thresholdssettledA bloom harms nothing; it takes light. Each kind alerts on its level (The alerts), so the keeper is told which kind to answer and with which verb, and a bare tank alerts on coverage by the same rule.
CO₂ thresholdscaffoldingThe line keepers treat as too much, with nothing behind it — no organism in the engine reads dissolved CO₂ as a stressor.
Behaviour on a drained tankscaffoldingConcentrations read 0 when there is no water and the water-level check requires water > 0, so the alert layer goes silent exactly when the tank is empty. Fish loads follow the same zeroes, so a drained tank clears them as clean water would and charges no flow — its water-level stressor carries it.
ClearingmissingA condition resolving writes nothing. The log records that a tank got into trouble and never that it got out.
RetentionmissingThe array is appended to and never trimmed, so a long-running tank grows its log without bound.
Log persistencemissingLogs are not saved. A reload starts a fresh history while the latch flags survive it, so an alert already tripped never re-fires its line for the same episode.
ChannelLives inShapeSurvives a save
Alert flagsstate.alertStateone boolean per conditionyes
Logstate.logsan append-only array of entriesno

The flags are what make an alert stateful. The check compares the reading against its threshold, compares that against the flag, and emits a line only on the transition into trouble.

AlertFires whenWhere damage actually starts
Water level lowbelow waterLevelStressThreshold of capacity, and only while some water remainsat the line: fish take water-level damage below it
High ammoniafree NH₃ above 0.02 ppmpast the line for every fish, further out the hardier it is
High nitriteabove 0.5 ppmthe same
High nitrateabove 80 ppmthe same
Low oxygenbelow 4 mg/Lthe same, below the line
High CO₂above 40 mg/L, the level keepers treat as too muchnowhere — nothing reads it
Green water · Filmthe kind’s level passes 1: it covers more than 30 of its habitat, or takes more than 25 % of the plants’ light — its share at each plant’s mean leaf, weighted by leaf areanowhere — a bloom takes light, and a plant starves only past its own edge

The four water-quality alerts fire before any fish takes damage; water level fires exactly where its damage starts; the blooms and CO₂ have no harm of their own behind them. Each bloom alert states the figure further past its line and names the action its habitat is taken out by: a water change for green water, a scrub for film. Ammonia reads the unionized fraction of the total, so the same test-kit reading crosses it sooner the higher the pH and the warmer the water:

pH at 25 °CTotal ammonia at the line
6.5≈ 11 ppm
7.0≈ 3.6 ppm
7.5≈ 1.1 ppm
8.0≈ 0.37 ppm

Every surface bands the total-ammonia reading at that moving line.

The log is an event stream, not a time series. A quiet tank running for a thousand ticks writes nothing at all: no readings are sampled, no summary is emitted per hour, and nothing is written unless something happened.

SourceWritten when
simulationThe tank is built; a fish or plant dies; a brood laid, a hatch or a birth; an offshoot
userAny applied action — a refused one logs nothing
scrubThe scrub action, filed under its own channel
evaporation · nitrogen-cycle · gas-exchange · algaeThe alert lines, each under the system whose reading tripped

Severity has two values. Every alert line and every death is a warning; everything else is info.

FieldHolds
tickThe hour it happened, which is what makes the log reconstructable
sourceThe system that emitted it
severityinfo or warning
messageFree text, for humans only; {0}, {1}… stand for its quantities
quantitiesPresent where a line states a volume, a temperature or a bloom’s coverage, kept in litres, °C and its 0–⁠100 mass
eventPresent only on the lifecycle moments a caller reacts to
countPresent where one entry accounts for many organisms

A figure the reader has a unit for — a water change’s volumes, the tank and room a run was created with, a heater’s target — stays a quantity rather than text, so each reader renders the line in its own units: logText(entry, format), metric unless handed a format.

Eight typed events exist: eggs-laid, eggs-hatched, fry-born, fish-died, plant-died, plant-propagated, algae-died and fry-sold. Four of them carry a count — a laying, a hatch, a live birth and a fry sale — because the number is the point. Deaths, offshoots and a bloom’s die-back are one entry per organism, so they need none; a die-back states the coverage it took instead, so a reader can weigh a crash against a thin bloom fading. Only a bloom there was any of to see logs one: spores starving in a dark tank die back as dust, unlogged.

The discriminator is the contract. A caller that parses the free text is reading a field that exists to be rewritten.

Every line carries the tick it happened at, so the array is a reconstructable timeline of everything the tank did. It is also the only such record: the state is a snapshot of one hour, carrying no more past than the last day’s light.

That makes two absences load-bearing. The array is never trimmed, so it grows for as long as the tank runs. And it is not part of the persisted state, so saving and reloading gives a tank with its full biology intact and no memory of how it got there.

waterLevelAlertLine fires at a tunable, waterLevelStressThreshold, listed under Livestock. The rest are module constants; the four water-quality lines are the fish tolerance edges, defined with Livestock.

ConstantMeaningValue
FREE_AMMONIA_EDGEFree (unionized) NH₃ at which the alert fires0.02 ppm
NITRITE_EDGENitrite at which the alert fires0.5 ppm
NITRATE_EDGENitrate at which the alert fires80 ppm
OXYGEN_EDGEDissolved oxygen below which the alert fires4 mg/L
HIGH_CO2_THRESHOLDDissolved CO₂ above which the alert fires40 mg/L
PLANT_LIGHT_LINEShare of the plants’ light a bloom alerts past25 %
BLOOM_COVERAGE_LINECoverage a bloom alerts past30
NeighbourReadWritten
Water & gasesoxygen and co2, as concentrations—
Nitrogen cycleammonia, nitrite and nitrate, converted from mass to ppm against standing water—
Environmentwater against tank.capacity—
LivestockwaterLevelStressThreshold, the level’s line—
AlgaeEvery kind’s mass directly — algae is an organism, not a resource—
Plants · LightThe share of the planting’s light each kind takes—
Plants—Receives a plant-died entry per plant lost and a plant-propagated entry per offshoot
Algae—Receives an algae-died entry per die-back of a bloom that showed, naming the kind and stating the coverage it took
Livestock—Receives death entries and one per brood laid, hatch or birth
Actions—Receives one entry per applied action
State & persistence—alertState is persisted; logs are not

src/simulation/alerts/ — the registry and one module per condition; src/simulation/core/ — the entry shape and the typed event union.