Skip to content

Actions

Everything the keeper does to the tank between hours. An action takes the state and returns a new one plus a human-readable message, applied immediately rather than queued for the next tick. A refused action returns the state it was handed, unchanged, with the message explaining why.

MechanicStatusWhy
DispatchsettledOne exhaustive switch over a discriminated union, so a new verb that forgets its case fails to compile.
Refusal disciplinesettledEvery action taking a number refuses a non-finite one before its own bounds. A comparison against NaN is false, so an unguarded bound would write it into a resource nothing downstream ever clears.
Water changesettledRemoves proportional dissolved mass, then blends temperature and both gases by volume against the tap and brings in the tap’s KH and GH. The one verb with real physics in it.
Feed · dose · root tab · stockingsettledBounded writes to a single stock, with the consequences left to the tick.
ScrubscaffoldingClears the walls’ share of every bloom’s habitat, read off the tank’s geometry and hardscape. Coverage is one figure over the habitat, so what the floor and hardscape held spreads back over the glass at once, and a second scrub takes the walls’ share of it again.
Top-offscaffoldingFills to capacity and adds the tap’s KH and GH, nothing else. The tap water it adds carries no temperature, no oxygen and no CO₂ — while the auto top-off doing the same job blends all three.
TrimsettledAn absolute size inside a unit’s own range — one plant, one family or the whole planting — and the cut leaves the tank.
Planting by floorsettledA unit goes in when its form’s full footprint, whatever its size, fits the floor the planting leaves free.
Water-change shockmissingA 90 % change costs a fish nothing beyond the temperature, hardness and gases the blend leaves behind.
Substrate vacuumingsettledA water change can vacuum the bed, taking a share of its settled mulm out of the tank. The food and waste standing in the water are left behind.
Config changes as actionsmissingEquipment settings are plain fields the host writes directly. There is no action type, no validation and no log for them inside the engine.
applyAction(state, action, config?) → { state, message }

Immediate, pure, and outside the tick loop entirely — an action never advances the clock. The passive readings — light, flow, surface, aeration — are settled off the tank it leaves, so a water change that carries green water out reads brighter at once. Past hour zero the day’s light history keeps the hours the tick recorded; at hour zero, with no day lived yet, the whole day is re-read through the water the verb leaves. The message is for the host to show; a refusal is not an exception.

Two verbs draw on the tank’s random stream: stocking a fish or a plant draws its individual variation and its id from it. The keeper’s hand is therefore part of the replay — two tanks on one seed diverge the moment their keepers act differently.

VerbInputWhat it movesRefused when
Feedgramsfood up by the amount, at the food’s compositionnon-finite or not positive
Top off—water set to capacityalready at capacity
Water changea fraction in (0, 1], optionally a vacuum share in [0, 1]see beloweither non-finite or outside its range, or the tank is empty
Dosemillilitresthe four nutrient pools, by the fertilizer rationon-finite, below the minimum or above the maximum
Root taba whole number of tabsthe bed’s nutrient store, by rootTab per tabnot a whole number from 1 to MAX_ROOT_TABS, or no bed
Scrub algae—the film on the glass walls, into waste: the film’s coverage down by the walls’ share of its habitat, what is left spread back over the glass; green water floats free of itnothing on the glass
Trim plantsa target size, optionally one plant id or one family idevery plant above the target down to it — of that family, or just the named planttarget outside [MIN_PLANTABLE_SIZE, 100], nothing above the target, plant not found
Add plantspecies, optional sizea unit founding its own family, fed and stocked, at condition 100unknown species, size outside [MIN_PLANTABLE_SIZE, 100], not enough floor for its footprint
Remove plantplant idthe plant leaves the array; a rooted one disturbs its footprint’s share of the bednot found
Add fishspecies, optional size in % of grown lengtha fish the shop raised to that size (see Livestock), with sampled sex, hardiness offset, health jitter and survival thresholdunknown species, size outside its hatchling’s to MAX_STOCKED_SIZE, or the physical mass ceiling reached
Remove fishfish idthe fish leaves the arraynot found
Sell fry—every fish that reads as fry: short of puberty and under half its grown length. A juvenile — past half but short of puberty — and an adult stayno fry in the tank

Trimmed tissue leaves the system outright, its reserve and stores with it — the keeper carried it out of the room. Scrubbed film, stores and all, rots in the water as waste, a gravel vac taking what has settled into the bed. Green water a water change takes leaves with the water.

A refused planting names the floor it lacks — “Not enough floor: 180 cm² free, Amazon Sword needs 700” — against the floor budget, which also says what a trim does to a plant.

StepEffect
Dissolved massammonia, nitrite, nitrate, phosphate, potassium, iron, kh and gh each scaled by 1 − amount
Green waterits budget scaled by 1 − amount, leaving with the water; its condition is the population’s and stays
Temperatureblended against tap by volume
Oxygenblended against tap water at saturation for the tap’s temperature
CO₂blended against atmospheric
KHthe refill brings tapKh per litre, so KH moves toward the tap by the share changed — and pH with it
GHthe same, at tapGh
Waterrefilled to capacity, so a change is also a top-off
Bedwith a vacuum share, the bed’s mulm scaled by 1 − vacuum; off by default

What it does not touch: standing food and waste, the two bacterial colonies, the bed’s nutrient store, the film on the surfaces, and the other organisms. Without the vac, a change dilutes the products of the nitrogen chain while leaving every gram of the material still feeding it.

StockResolution a verb writes at
Nutrientsmilligrams, from a dose in millilitres or a count of root tabs
Waterwhole tank — top-off and water change both end at capacity, never in between
A bloom’s coveragea share of the standing coverage — a scrub’s the walls’ share of its habitat, a water change’s the share changed — never an absolute figure
Plant sizean absolute target, not a delta
Organismsone at a time, except selling fry, which is the whole cohort

Equipment settings — the heater’s target, the lid, the light’s schedule and rating, the filter and powerhead, the doser’s amount and schedule — are ordinary fields on state.equipment that the host writes directly. They are not actions: no dispatch, no message, no refusal.

The engine ships two helpers for the changes with a consequence behind them. Swapping the bed scales both colonies by the share of colonisable surface that went out with it, and takes the floor’s share of the film. Writing doser settings clamps the amount into the doser’s own range. Every other setting is a plain assignment, and the colony feels it only when the next tick truncates it to whatever surface is left. A light change reaches the substrate on the next tick, and the day’s light catches up as Light describes.

Tunables are a separate surface again. TunableConfig is validated against each leaf’s declared range — see state & persistence.

The fertilizer formula and the root tab are listed in the reference under Nutrients; the action bounds are module constants.

ConstantMeaningValue
Dose bounds, manualFloor and ceiling on a hand dose0.1–50 ml
Dose bounds, automaticFloor and ceiling on a scheduled dose0.5–10 ml
MAX_ROOT_TABSMost tabs one push takes10
MAX_FISH_VOLUME_FRACTIONShare of the water stocked fish may occupy — a plausibility ceiling, not a husbandry one0.5
STOCKED_FISH_SIZESize a fish is stocked at when the caller says nothing, in % of grown length80 %
MAX_STOCKED_SIZELargest a shop sells: grown length is an asymptote no fish reaches90 %
MIN_PLANTABLE_SIZELeast a planting or a trim may name; the most is a full unit1 %
DEFAULT_PLANT_SIZESize a plant goes in at when the caller says nothing50 %
Water change presetsThe steps the dashboard offers; the action itself takes any fraction0.1 · 0.25 · 0.5 · 0.9

The stocking ceiling blocks the physically impossible rather than the unwise. A tank crammed far past its sane bioload is allowed, and the vitality engine plays out the ammonia and the oxygen crash; only a tank that would hold more solid fish than water is refused. Breeding is exempt, so a bred cohort can push past the ceiling and simply lock further stocking out.

NeighbourReadWritten
Water & gasesoxygen, co2, kh and gh for the blendAll four on a water change; kh and gh alone on a top-off; kh on a scrub
Nitrogen cycle—ammonia, nitrite and nitrate diluted proportionally; nitrate raised by a dose
EnvironmenttapWaterTemperature and tapKh as the thing being blended toward; water and tank.capacity for every refillwater and temperature
Plantsplants for the floor budget, and the trim target and familyThe array, and each budget directly
Livestockfish for the mass ceiling and the fry countThe array; food for the fish to eat next tick
AlgaeEach bloom’s coverage, and the share of its habitat on the glass and in the columnThe film’s budget, down by the walls’ share into waste and the water; green water’s, by the share of water changed
Equipmentsubstrate.type — whether there is a bed to tabsubstrate.nutrients, raised by a root tab; settings are written by the host
Alerts & logging—Every applied action appends a user entry; a scrub files itself under scrub. A refusal logs nothing
State & persistenceThe draw stream, for every new organism’s idThe advanced stream position

src/simulation/actions/ — the union, the dispatcher and one module per verb. The two equipment-side helpers live in src/simulation/equipment/.