Skip to content

Overview

A freshwater aquarium modelled as an hourly simulation: water chemistry, the nitrogen cycle, plants, algae, livestock and equipment. tick(state) returns a new state and mutates nothing, so whatever hosts the engine keeps ownership of persistence, scheduling and rendering.

One shape runs through all of it. Subsystems compute rates; the resource layer integrates those rates into stocks. A subsystem answers a biological question — how much of a compound this biomass moves in one hour — and returns a mass. It never writes a stock of its own.

The repository holds the engine and the dashboard that drives it. Both are maintained here; only the engine is published.

EngineDashboard
What it isThe simulation itself — pure TypeScript, no framework, no renderingA React app that runs one tank and exposes every number in it
Lives insrc/simulation/src/ui/
Ships asaquarium-simulator on npm, MITDeployed to engine.fishroom.app
Reads asA library any host advances one hour at a timeA dashboard — step, inspect, act, and edit the tunables live

The dashboard is not a demo. It is how a tank gets read by hand, which is what makes a mechanic arguable before it is a constant.

PropertyValue
Packageaquarium-simulator — npm, MIT
Entry pointtick(state, config) → state, pure and immutable
TickOne hour
ResolutionThree tiers per tick: immediate → active → passive
RandomnessSeeded and serializable; Math.random appears nowhere in src/
Published surfaceThe built dist/ only — the dashboard and the CLI stay in the repo
Terminal window
npm install aquarium-simulator
import { createSimulation, tick } from 'aquarium-simulator';
let state = createSimulation({ tankCapacity: 60 });
for (let hour = 0; hour < 24; hour++) {
state = tick(state);
}
console.log(state.resources.ammonia); // mg

Only tankCapacity is required. Every other parameter — heater, lid, substrate, lighting — falls back to a default.

SectionAnswers
ConceptsThe four ideas every subsystem is built on. Read these first.
SubsystemsOne page per mechanism, each in the same fixed anatomy.
UIWhat the dashboard shows, and how it is laid out.
ReferenceLookup tables: constants, presets, exported surface.

Every subsystem page carries the same five sections, in this order, and closes with a pointer to the directories it lives in.

SectionContents
What it modelsTwo or three sentences. The scope, and nothing else.
StatusEach mechanic badged settled, scaffolding or missing, with one clause of why.
How it worksShort subsections per mechanic. Stocks and rates as tables.
Key tunablesA pointer to the page’s tunables in the reference, and any module constants with their values.
SeamsWhat this subsystem reads and writes, and which neighbour owns the other end.

In the source, a crystallized decision and a proof-of-concept look identical. The badges are the sorting, and they are the first thing to read on any subsystem page.

BadgeMeansHow to treat it
settledThe commitment is deliberate and the tests pin the mechanism.Changing one is a design conversation, not a refactor.
scaffoldingA stand-in the abstraction does not cover — a switch, a clamp or a shortcut. Its tests pin a placeholder.Don’t build on it, defend it, or preserve its behaviour.
missingAbsent — and the absence distorts the mechanics next to it.Read it as a known bias in its neighbours, not as a gap to be filled in passing.

The sorting test is the design philosophy itself: rates into stocks is settled; bands and switches are scaffolding. Two caveats hold. A band edge where a rate starts from zero is a rate, not a switch. And death at condition 0 is a real discontinuity.

Mechanics are named on purpose. A redesign renames things, so a status row naming a mechanic that no longer exists is self-evidently stale, and should be resolved rather than trusted.