First Test: Up to the First Depot
The first build of the prototype, and the technical choices made for it. What the whole prototype covers is in prototype-scope.md; how the sim is organized is in simulation-architecture.md; how the client and host talk is in runtime-and-sync.md.
Scope Decided
The opening, up to the first Depot: the Crashed Ship (about 6 stacks of storage), the Scout Drone and fog, one Harvester gathering a nearby Basalite patch, and the Engineering Crew building the first Depot. It’s steps 1 and 2 of the build order: the sim with tests first, then the plain client.
The numbers come from tier0-proposals.md: Harvester 1 tile/s, crew 2 tiles/s, Scout 10 tiles/s (now about 6.7), 0.25 Basalite/s per footprint cell, and a Depot that costs 100 Basalite and takes 30 s to build. All of them are whole numbers of ticks. The test passes when the timings match balance-and-simulations.md, with the first Depot ordered at about 3:20.
Two open design questions fall inside this range. The test takes the simplest answer to each, and either is easy to change later:
- What the crew does before the first Depot (#8): it idles.
- When the crew’s Basalite leaves the network (#42): when it loads.
The Harvester spike Tentative
The first game systems, built to check that the simulation rules work. The code is in prototype/sim/src/ (harvester.ts, movement.ts, player-commands.ts, recipes.ts, game.ts); the tests are in prototype/sim/test/harvester.test.ts.
What runs: the Crashed Ship with its Harvester; a Gather command set on the ship’s slot; the brain, the movement arbiter and A* movement; collecting at 0.25 per productive cell per second; unloading after 5 s.
The timings match Run 7. Sent at 0:30 to a full footprint 15 tiles away, the Harvester brings its first 50 Basalite home at 1:55 and the second at 3:20, when the first Depot is ordered. From 17 tiles (tier0_checks.py) the second load lands at 3:28. The round trips in the throughput table come out within 2 ticks. The rules add one tick per leg, because the brain sees an arrival on the tick after it happens: two trips take 3,403 ticks instead of 3,400.
The docs didn’t settle these, so the spike chose:
- Each building has a dock: the tile where its units park, set out from and unload. The ship’s is below the middle of its bottom edge. Distances to deposits count from the dock. Since the first playtest each unit slot has a dock of its own (below); this one, the middle dock, is slot 0’s. Superseded by Patches and entrances.
- A Harvester drives to the footprint tile nearest its dock and starts collecting once it’s inside the footprint. Superseded by Patches and entrances.
- The mission lives on the building’s slot (
UnitSlots), not on the Harvester, so a rebuilt Harvester will pick it up. The ship has only the Harvester’s slot so far. - Changing the mission: a Harvester heading out turns toward the new footprint. One that’s collecting goes home first if it holds cargo.
- Unloading moves the whole cargo at the end of the 5 s, and only if all of it fits. Otherwise the Harvester keeps its cargo at the dock and tries again every tick.
- Units don’t block each other. Only buildings do. Replaced after the first playtest: ground units never share a tile (below).
- The host replaces the
PlayerCommandsqueue before every tick.runTick(world, commands)does that and runs the schedule.
Not built yet: rebuilding a destroyed Harvester. One Harvester per footprint came with the crowded hour.
The crew and the first Depot Tentative
The Engineering Crew, construction ghosts and the Depot. The code is in prototype/sim/src/ (crew.ts, construction.ts, and the new commands in player-commands.ts), with canPlace now in prototype/shared/src/placement.ts; the tests are in prototype/sim/test/crew.test.ts.
What runs: placing and cancelling a ghost; the shared canPlace check; the crew’s brain; loading the materials at the ship; driving to the ghost and building it; the finished Depot, with 6 stacks of storage and 3 empty unit slots, which blocks movement.
The timings match. With the ghost placed 10 tiles away at 0:30, the crew loads the 100 Basalite at 3:20, on the tick after the Harvester’s second load lands, and the Depot is done at 3:55. That’s Run 7’s 3:50 plus the 5 s drive that tier0_checks.py adds. Loading takes 1 tick, and the crew starts building on the tick after it arrives. Since units park on docks of their own and don’t share tiles, it’s 3:57: the crew steps down a row to pass the Harvester and Scout parked beside it, then waits about a second for the Harvester to cross its path.
The docs didn’t settle these, so this build chose:
- A crew builds from the ghost’s dock. A ghost has the docks its building will have, and the crew parks on the middle one. That keeps it outside the footprint, so it isn’t shut inside when the building appears.
- Ghosts don’t block movement. Only finished buildings do. A unit already walking through a footprint when its building appears keeps going; the next path it finds goes around.
- A crew takes the oldest ghost no other crew has, and only once its home holds the whole cost. It loads all of it in one go. Every Tier 0 job fits in one trip (tier0-proposals.md), so jobs that need two trips aren’t built.
- The crew draws from its home’s network: its home and every building connected to it by touching (the crowded hour). It takes the oldest stacks first. In the opening the first Depot stands apart from the ship, so the network is the ship alone.
- Loading is instant, and so is putting materials back. The docs give the Harvester 5 s to unload, but nothing for a crew.
- The finished building is a new entity. The ghost is despawned and the building is spawned from its own recipe, so the recipe stays the one place its components are listed. The
Builtmessage gives both ids, so the client can swap one for the other. - The crew’s load is spent when the building is finished. If the ghost is cancelled first, at any point, the crew drives home and puts it all back. That matches “cancel an unbuilt ghost: free” in resources-and-economy.md.
- Placement is refused when the footprint or any of its docks is off the map, the footprint overlaps a building or ghost, a building covers one of its docks, or the footprint covers another building’s dock. Missing Basalite never refuses it. Since Patches and entrances the same rules apply to the whole entrance instead of the docks.
- The ship’s Engineering Crew is in slot 1 of its
UnitSlots; the Harvester is in slot 0.
Not built yet: demolishing, switching buildings on and off, unlocks in canPlace, reassigning the ship’s crew to the new Depot, and rebuilding a destroyed crew. A crew that can’t reach its ghost keeps the job and tries again every 3 s, like any unit with no path (the crowded hour).
The Scout and fog Tentative
The Scout Drone and fog of war. The code is in prototype/sim/src/ (scout.ts, fog.ts, flying in movement.ts and pathfinding.ts, and the scout command in player-commands.ts); the tests are in prototype/sim/test/scout.test.ts.
Fog has three states for each tile:
- Unexplored: nothing of the player’s has ever seen it. Nothing can be built there, and a Harvester can’t be sent to a deposit there.
- Explored: seen before, but nothing sees it now. Terrain and deposits stay revealed.
- Visible: inside the vision of one of the player’s units or buildings right now.
The sim stores only what’s explored. What’s visible is worked out by fogAt from everything that has Vision, and the client will use the same function (runtime-and-sync.md). Each tick, an Explored message lists the tiles newly explored, each once.
What runs:
- The ship starts with its own 20 tiles explored, which takes in the Basalite 15 tiles from its dock (tier0-proposals.md). It was 12, with the Basalite out of sight, until the first playtest.
- The Scout flies out and back at 3 ticks a tile, over everything, with no pathfinding, and recharges 4 s per 100 tiles flown.
- Vision radii: the ship 20, the Depot 20, the Scout 12, the Harvester and crew 6. Ghosts see nothing.
- Gather and placing a ghost are refused in unexplored fog.
In the opening run, the ship already sees the Basalite. The Scout still leaves at 0:00, 40 tiles south, and is back and charged long before the Harvester is sent at 0:30, so every later timing is unchanged.
The docs didn’t settle these, so this build chose:
- Vision is a circle from the middle of a unit’s tile or a building’s footprint. A tile is seen when its center is within the radius.
- The explored layer is stored in 32 × 32 chunks of bits, made only when something in them is explored. The development checks walk every resource after every phase, and walking all 409,600 tiles of a flat array made a 4-minute run several times slower. Terrain and deposits stay flat arrays.
- There’s one explored layer, for the one player. A layer per player comes with multiplayer.
- A Scout order is one trip. It waits until the Scout is docked and charged. Orders given meanwhile queue (see below); until the first playtest a newer order replaced one that hadn’t started. Each order is still one out-and-back trip, not a patrol (units-and-missions.md).
- Range is a straight-line distance of 75 tiles from the dock (250, then 150, before the first playtest; tier0-proposals.md). Recharging is worked out from that distance, out and back, and rounded once.
- The Scout flies in eight directions in a straight line. A diagonal step takes √2 as long, rounded to the tick: 4 ticks instead of 3.
- The ship’s Scout is in slot 2 of its
UnitSlots. - A source reveals only when it moves. Each
Visionremembers the tile it last revealed from, so buildings reveal once and parked units cost nothing. - Gather needs all four tiles of the footprint explored, and placing a ghost needs its footprint and its dock explored. Superseded by Patches and entrances.
Not built yet: creatures and hives, which only show inside current vision; the hatched fog inside the noise zone; and filtering what each player is sent by what they can see.
The crowded hour Tentative
An hour of a base that never stops growing, played by a script before the client exists. It tests reserved footprints, networks of touching buildings, more scouting, and what crowding does to pathfinding. It’s the run crowded-hour (prototype/README.md); the code is in prototype/sim/src/ (mapgen.ts, random.ts, units.ts, and changes to the Harvester, crew, movement and pathfinding), with networks now in prototype/shared/src/network.ts, and prototype/sim/runs/crowded.ts.
The script looks at the world once a second and does what a busy player might. It’s a perfect player: it spends Basalite the moment there’s enough, places a ghost the moment a crew is free, and never stops to think, scroll or look elsewhere. So the hour is faster than a person’s hour would be (see Testing).
- Scouting: whenever the Scout is home with no order waiting, it’s sent 70 tiles out, just inside its 75-tile reach, each trip turned by the golden angle (about 137.5°) from the last, so the trips spread evenly.
- Depots: whenever every ghost has a crew, a new Depot ghost goes on the nearest spot that touches a building, measured from the ship’s center, and that
canPlaceallows. Spots over Basalite are skipped. It never stops, even when there are no footprints left for the new Harvesters. - Slots: each new Depot builds 2 Harvesters and an Engineering Crew.
- Footprints: each Harvester slot with no mission goes where one more Harvester adds the most Basalite per minute. Harvesters on one footprint take turns, so n of them give n times one Harvester’s rate, up to the footprint’s limit of one always collecting. A far footprint fits more before they queue, because each spends longer driving. A Harvester has to add at least half of what it would alone, or it isn’t sent. Footprints that overlap a worked one without being the same are skipped, so the script never splits a patch it’s already working.
What the sim gained for it:
- A seeded map generator, Basalite only at first (Colite and Ferrite came after the first playtest), from tier0-proposals.md: three small patches (7–12 cells since the first playtest; full patches of 2 footprints before), 15–20 tiles from the ship’s dock and 90–150° apart; a 6-footprint patch at 40–60 tiles; and past 150 tiles, a patch in about 35% of 60 × 60 blocks, with 4–12 footprints and irregular edges. Distances are straight lines from the dock to a patch’s nearest tile. Map generation has its own stream (Mulberry32), separate from any the world will keep (#47).
- Networks: every building has foundation under its own footprint, so buildings whose footprints share an edge are connected (base-network-and-inventory.md); touching at a corner doesn’t count. Networks are worked out when needed, at most once a tick, so nothing has to be updated when a building appears. A crew takes the oldest stacks from anywhere on its home’s network, and puts back a cancelled load into its home first, then the rest of the network. Harvesters still unload only at home, though since foundation a full home makes room by moving its oldest stack out to its network (below).
- Depots build their units. The player picks what each empty slot builds (a new command); the slot builds it in 30 s and the unit starts at the dock (superseded: it now starts inside; see Patches and entrances). Picking again, or picking a Scout Drone, is refused. Slots rebuild lost units the same way, though nothing can be lost yet.
- One Harvester per footprint. A Harvester heading for a footprint where another one, or one overlapping it, is collecting waits and starts when it’s free. Since the first playtest it waits on the nearest bare tile off the patch, not inside the footprint. Waiting makes no noise (tier0-proposals.md). A footprint is held only while a Harvester collects in it, not while one drives there, so a line of Harvesters can share a distant one. Reserving by mission is the player’s side, shown in the route UI; the sim only stops two from collecting at once (units-and-missions.md).
- Units find a new path when their next step has become a building. A unit with no path keeps its goal and tries again every 3 s, so it recovers if a way opens.
- Pathfinding fails fast when shut in. Before A*, the goal and then the start are flooded up to 4,096 tiles. A flood that ends sooner proves the two are apart. Without this, every failed search from outside a closed area expanded the whole map, about 400,000 tiles and 200 ms. Two large areas that are apart still cost a full search.
- Counters: searches, failures and tiles expanded, for runs to report.
What seed 1 shows (summarized from npm run runs -- crowded-hour):
With the Scout’s reach at 150 tiles, before the first playtest. The full report, now with the 75-tile reach, with a map of the base, the Scout’s trips and what was found, is on the design site under Run reports (npm run dev in site/).
| Time | Depots | Harvesters | Idle Harvesters | Crews | Units stuck | Map explored |
|---|---|---|---|---|---|---|
| 10:00 | 3 | 5 | 0 | 3 | 0 | 10% |
| 20:00 | 25 | 49 | 13 | 25 | 17 | 16% |
| 30:00 | 45 | 89 | 53 | 45 | 30 | 18% |
| 60:00 | 85 | 171 | 135 | 86 | 45 | 19% |
- The base boxes its own units in, from 12:26. The seventh Depot closed the ship’s dock off from the open ground. By the end, 45 of 257 ground units have no path, and none of them get one back. There are two kinds of trap: a dock walled in on three sides by its own building and the Depots beside and below it, and units caught outside a closed-in pocket that holds their dock.
canPlacekeeps every dock tile free but not reachable, so placement on its own can’t prevent this. How players should leave room is open question #49. - A 150-tile Scout finds only the four patches near home. All four are found by 8 minutes: three of 8 tiles 17–20 tiles out and the 22-tile patch 53 tiles out. Everything within 150 tiles is explored by about 40 minutes, and the trips after that add almost nothing: the first half of the 84 trips explored 72,302 new tiles, the second half 3,840. The far patches start past 150 tiles, so for the whole hour the base has 13 footprints.
- So footprints run out, and Harvesters sit idle. The script lets Harvesters share footprints and wait their turn, sending one only where it adds at least half of what it would alone, but 13 footprints fill quickly. From 20 minutes on new Harvesters have nowhere worth going, and 135 of 171 are idle at the end. 9,550 Basalite is collected. With the old 250-tile reach the Scout found the far patches, sharing kept every Harvester working, and the base reached 189 Depots and 22,500 Basalite.
- The script can still fragment a patch it hasn’t started. Picking by Basalite per minute sometimes takes a 2- or 3-cell footprint at an offset next to the dock over a full one farther out, and that can split a full patch.
- Pathfinding cost comes from retries. Searches that do find a path are cheap. 97% of the 30,686 searches fail, mostly units retrying every 3 s, and each costs up to two floods. The slowest tick is about 6 ms on the designer’s Mac. Retrying less often, or only when a building changes, would cut almost all of it.
The choices above weren’t settled by the docs. Picking slot units, the 3 s retry, the 4,096-tile flood and the script’s rule for sharing footprints are this build’s own.
With the 75-tile reach (after the first playtest), seed 1 ends with 69 Depots, 7,650 Basalite and 5% of the map explored, against 85, 9,550 and 19%. The Scout still finds the same four patches, and 50 units are stuck at the end.
The protocol and host Tentative
The first half of step 2: the messages between the host and the client, and the host that sends them, before any client exists. The shapes are in prototype/shared/src/protocol.ts; the “view for this player” function is prototype/sim/src/view.ts; the host is the prototype/host package (host.ts, scenarios.ts, and worker.ts, its Web Worker entry). The tests in prototype/host/test/host.test.ts play the opening through messages alone, and the first Depot is done at 3:55, as in the sim’s own tests.
What runs:
- The client sends a new game (the scripted opening, or a generated map by seed), commands, time controls (paused, speed) and a single step.
- The host sends a start message once, with the terrain, what’s explored and the state; a tick message after it runs ticks, with the tick number, command results, tiles newly explored, events and the whole state; and a time message when the time controls change.
- The state lists buildings (storage and slots, with each slot’s mission and build progress), ghosts (cost, build progress and the crew that took the job) and units (state, cargo, movement status and vision), in id order, each with what the client needs for
fogAtandcanPlace. Events are a finished building, a unit built by a slot and a Harvester unloading, from the sim’s messages. - The worker bundles on its own (about 90 kB) and runs with no Node code.
The docs didn’t settle these, so this build chose:
- At high speed, one message covers several ticks. The host runs the ticks it owes and sends one message: the results, explored tiles and events of all of them, and the state after the last. Sending the whole state for every tick at 16× would be 320 states a second.
- The host runs at most 100 ticks a call and drops time beyond that, so a slow device or a speed it can’t keep up with slows the game instead of freezing it. The worker checks the clock every 16 ms.
- Step works only while paused, and changing the time controls drops time owed but not yet run.
- Unit states and missions are shared types (
prototype/shared/src/units.ts), so the client can name a Harvester’s state without the sim. The sim’s components use them. - The opening’s map is in the sim (
openingMap), so the scripted run and the host’s opening are the same world. - The host keeps the development checks on. At 16× on the crowded hour’s map they may be too slow; that’s when to turn them off.
The client that uses them is below.
The plain client Tentative
The second half of step 2, which finishes the first test: the opening played on screen. The code is in prototype/client/ (store.ts, connection.ts, tools.ts, look.ts, the renderer in render/ and the panels in ui/); the tests are in prototype/client/test/. npm run dev in prototype/ starts it.
What runs:
- The map: terrain and fog as one pixel per tile, scaled up. Unexplored tiles are black, explored ones dim, and what’s visible now is bright. Only the changed block is uploaded each state.
- Placeholder art: buildings, ghosts and units are colored squares with a letter, from one table (
look.ts). Each has a status light: green working, amber waiting, grey idle, red stuck with no path. Ghosts and buildings making a unit show a progress bar. - Panels: a top bar (clock, Basalite in every building, pause, step, speeds 1–16×, new game on the opening or a new map), a build bar (the Depot, with its shortfall in red, which never blocks placing), a panel for the selected building or ghost (a unit opens its building’s), and short notices for refusals and finished work.
- Commands by tapping: place and cancel a ghost, send the Scout, give a Harvester slot a Gather mission, and pick what a Depot’s empty slot builds. Each tool checks the tap with the shared function the host uses (
canPlace,inScoutRange, the footprint’s fog and deposit), shows a green or red preview under the mouse, and refuses a bad tap at once with the reason. - The camera: drag to pan, pinch or scroll to zoom, and a press that barely moves is a tap. It starts on the ship’s dock and keeps the middle of the screen in place when the screen changes size.
- Phones: the selection panel sits at the bottom on narrow screens, and the starting zoom shows about 16 tiles across a phone and 40 across a wide screen.
- The wall is checked:
npm run buildbuilds the client and the host’s worker as separate bundles, then fails if the client’s holds any sim code. The host’s worker is about 52 kB.
Checked in the browser: playing the opening by tapping (the Scout south, Gather on the Basalite, the Depot ghost at the scripted spot), the Depot was built and a Harvester ordered from its slot; the phone layout; the production build; and 16× keeping up, at 321 ticks a second, on a generated map with the development checks on.
The docs didn’t settle these, so this build chose:
- Panels may read small objects through
useView, which compares by JSON, not only plain values anduseShallow. A building’s panel reads the building, and re-renders only when something in it changes. - The terrain isn’t copied into each new state. It’s large and never replaced during a game, so the store marks explored tiles in place, counts the changes, and queues the new tiles for the renderer.
- The player’s own ghost shows at once, drawn from the pending command, and a cancelled ghost disappears at once. Other commands show their effect with the next state.
- A building is placed centered on the tapped tile; a Gather footprint takes the tapped tile as its top-left.
- Tapping a tile where units share a spot picks the next one each time. Since the first playtest units rarely share one: only a unit appearing where another stands.
- In development the worker is served from its source, by path; the client still never imports it.
- The client’s lint is typescript-eslint’s recommended rules plus React’s hooks rules, without the sim’s bans on
as,anyand!, since the client holds no read-only world.
Not built yet: the production overlay, network totals per network, demolishing and switching buildings on and off (the sim doesn’t have them yet), and telling the player why a unit is stuck.
After the first playtest Tentative
The designer played the opening for about 3 minutes in the browser (Nov 2026). What changed:
- The ship sees 20 tiles, and the Scout reaches 75 at about 6.7 tiles/s (tier0-proposals.md). Finding the first Basalite took 2–3 minutes of full-range trips; a long reach teaches sending the Scout to the edge every time; and it felt fast.
- Scout orders queue. An order given while the Scout is out or recharging flies after the ones before it, up to 5. Its panel says how many are waiting. The queue is
ScoutOrder.queue; each queued trip keeps its number so the brain can tell which it has taken. - The game saves. A reload had wiped the game. The host sends a save every 5 seconds of game time, when the time controls change, and when a game starts. The worker can’t use localStorage, so the page keeps it and sends it back at start. A save holds every entity’s components, the resources but the terrain (rebuilt from the scenario), the tick, the next id and last tick’s messages, so a loaded game plays on exactly as the original (a test checks this). Loading finds each component by name through the schedule, plus a short list of components no system touches; a test fails if a new component is in neither. A save carries
SAVE_VERSION, and a save from another version is thrown away and a new opening started. “Opening” and “New map” ask before replacing a game in progress. - Phones hide the building’s panel while a tap is being aimed (sending the Scout, Gather, placing), so the hint and the map have the room. It comes back after the tap.
- Tapping a unit opens its building’s panel (player-and-platform.md), with the unit’s slot highlighted, what it carries under it, and the unit outlined on the map. The camera doesn’t move. Where units share a tile, the first tap picks the one drawn on top. Units no longer have a panel of their own.
- The Scout’s range circle follows its reach; it had been drawn at 150 tiles.
- Small Basalite patches are 7–12 cells (tier0-proposals.md): a 3 × 2 core, lying either way, with cells added within a tile of it. A spot touching more of the patch is likelier to be picked, so patches come out round, never stringy, and fit in 5 × 4 tiles. The core keeps one footprint full. The near patches on generated maps are small patches, and the opening’s first patch is one too, always the same shape (10 cells), with its full footprint where the 2 × 2 was. The 2 × 2 was too small to read on screen. Bigger patches keep their blocks of footprints.
- Colite and Ferrite are on the maps, so the Scout has things to find. Harvesters gather them like Basalite, in stacks of 50; nothing uses them yet. The opening adds four patches out of the ship’s sight but in the Scout’s reach: two more small Basalite patches (32 tiles up and right, 50 left), 4 footprints of Colite 30 tiles up and left, and 4 of Ferrite 40 tiles right. Generated maps follow tier0-proposals.md: 4 footprints of Colite at 25–40 tiles, Ferrite at 30–45 (4 footprints) and 80–130 (8), and past 150 tiles a block holds Basalite 35% of the time, Ferrite 25% and Colite 20% (Cuprite’s 20% stays empty until it exists). On screen Colite is a dark purple and Ferrite rust, and the top bar shows each resource once there’s some.
- Units look busy at work. A collecting Harvester walks round its footprint’s 4 tiles, and a building crew walks round the ghost’s edge, standing on each tile about 0.6 s. It’s only drawn: in the sim the unit stays on one tile.
- Each unit slot has its own dock, in a row below the building: slot 0 on the middle dock, then left, then right. A Depot’s three docks run along its bottom edge. A unit parks on its own dock, is built there and unloads there, and each dock is drawn as a pad. Distances and the Scout’s reach still count from the middle dock, and a crew builds a ghost from it. A dock for units from elsewhere, such as an Outpost’s unload dock, takes one unit at a time; the rest queue. Superseded by Patches and entrances.
- Ground units never share a tile, with each other or with a landed flyer (a Scout on its dock). A unit whose next tile is taken waits. If the unit in the way is heading for the same tile, it waits its turn in line; otherwise it looks for a way around once a second, treating every ground unit as a wall, and keeps waiting if there’s none. A new path goes around units standing in the way if it can. Units move in id order, so one stepping off a tile frees it for the next on the same tick. A way around may be at most 20 tiles longer than the straight line (or the path it replaces), which keeps those searches cheap. Saves went to version 2. On the crowded hour’s seed 1 the base did better: 79 Depots, 9,500 Basalite and 14 units stuck at the end, against 69, 7,650 and 50, with fewer tiles searched (31 million against 50).
- A Harvester waits for a footprint off the patch, on the nearest bare tile toward home, not inside the footprint.
Playing it beat the math. Three minutes on screen settled things the hand-run sims had only theorized about. Keep tuning by playtest; runs check the sim and answer questions only long games can (#149 in the decision log).
Next, probably: flying the Scout directly on a timed battery that brings it home on its own (open question #50). That would replace the queue as the main way to scout.
Not changed: the Scout already flew straight with no pathfinding. Still to come: queueing on purpose, queues for Harvester missions, and the Scout reach upgrade.
Foundation and shared networks Tentative
The next part of Tier 0 after the first playtest: foundation, so a Depot built apart from the ship can share its stock (base-network-and-inventory.md). The code is the Foundation recipe in prototype/sim/src/recipes.ts, the crew’s batches in crew.ts, makeRoom in storage.ts, and the foundation tool in prototype/client/src/tools.ts; the tests are in prototype/sim/test/foundation.test.ts.
What runs:
- Foundation tiles, 10 Basalite each and 1 s to build: three times faster than the 3 s that 0.3 s per material gives, which was too slow for a corridor (tier0-proposals.md). A tile is its own entity, with no storage or vision, and doesn’t block movement. Networks now join buildings through foundation as well as by touching, so a strip of foundation makes a Depot and the ship one network: either one’s crew builds with the other’s Basalite.
- Placing it: foundation goes on any explored tile that isn’t a building or foundation already, docks and deposits included. A building may go on foundation or a foundation ghost.
- A crew takes foundation in batches. When the oldest ghost it can pay for is foundation, it takes the nearest unclaimed foundation ghost to that one, then the nearest to that, while it can carry them (100 Basalite, so 10 tiles) and the network can pay. It loads once, builds them in turn, standing on each tile, and goes home after the batch. Each finished ghost spends only its own cost; what’s left of a cancelled one comes home and goes back on the network.
- A full receiving building makes room, first in, first out (base-network-and-inventory.md). When a Harvester unloads at a full home, the home’s oldest stack moves to the rest of its network, topping up partial stacks before opening new ones, until the cargo fits. A stack moves only whole, and keeps its age. With nowhere to put it, nothing moves and the Harvester waits, as before.
- Laying it on screen: the build bar’s Foundation tool lays a block of ghosts from where a drag starts to where it ends, so a straight drag is a line. The preview marks the tiles it will lay in green and those it skips in red, and the bar shows the count and cost. A tap lays one tile. Either way the tool is then put away, as after placing a building; a drag that lays nothing keeps it in hand. While it’s in hand, one finger drags foundation and two still pan and zoom. One drag lays at most 400 tiles. Finished foundation is drawn as slate plates under buildings, and a finished tile doesn’t get a “Building finished” notice.
- A build queue. Crews used to take the oldest ghost they could pay for. Now a planned building’s panel has Build next, which puts it at the top of the queue, above anything put there before; the panel then says “Next to be built”. Crews take ghosts from the top down, then oldest first. A ghost at the top that can’t be paid for yet holds back that resource: no crew spends it on anything lower down, so it builds up for the one in front. Without that, early foundation kept eating the Basalite a waiting Depot needed. The button goes once a crew has taken the ghost. The rank is
GhostPriority, set by a newprioritizeGhostcommand. - Saves went to version 3 (5 since the build queue).
Twice as fast Tentative
After the second playtest (Nov 2026): 1× felt far, far too slow and 2× felt good, so every speed and rate doubled and every time halved, and 1× now plays what 2× did (balance-and-simulations.md). The clock still shows real time.
- Harvester 2 tiles/s, 0.5 per productive cell per second, 2.5 s to unload, 15 s to build. Crew 4 tiles/s, 0.15 s per unit of material (a Depot in 15 s), 15 s to build; foundation 0.5 s a tile. Scout 10 tiles/s and 2 s of recharge per 100 tiles. Units walk round their work twice as fast on screen too.
- The Scout isn’t quite doubled. Speeds are whole ticks a tile, and doubling 3 ticks would need 1.5, so it’s 2 ticks: 10 tiles/s rather than 13.3.
- Not halved: how long a blocked unit waits before looking for a way around (1 s). Half a second doubled the searches in the crowded hour and left the base worse off. Nor the 3 s retry for a unit with no path, which is only about cost.
- The opening now: the scripted player sends the Harvester and places the Depot at 0:15 instead of 0:30. The first 50 Basalite lands at 0:57 and the second at 1:40, and the Depot is done at 1:58, half of Run 7’s times plus the 1 s of traffic. Left alone, the ship is full at 5:55.
- The crowded hour ends with 90 Depots, 10,200 Basalite and 12 units stuck, against 79, 9,500 and 14: twice as much happens in the same hour.
Words on screen are a player’s, not the design’s. No “footprint”, “ghost” or “target”: a Harvester is “Driving to Basalite” or “Mining Basalite”, a crew is “Building a Depot” or “Laying foundation”, a building waiting for a crew is “planned”, time is never shown as text (a unit being made or a building going up shows a progress bar), and refusals say what’s wrong in plain terms (“there’s a building in the way”, “you haven’t explored there yet”). What a unit carries shows as small storage slots, like a building’s, rather than “Carrying 30 Basalite”, on every unit’s row in its building’s panel, all the time. Picking a row selects the unit, moves the camera to it and readies its default mission (player-and-platform.md); the “Mine…” and “Send…” buttons are gone.
Not built yet: auto-foundation when placing a building near a network (open question #11), foundation HP and being destroyed, removing foundation, and network totals per network in the top bar (it still adds up every building).
Patches and entrances Tentative
After the third playtest (Nov 2026). Harvesters crowding one patch queued in a line back to the base, along the row of docks under a row of Depots. Units ended up on other buildings’ docks, so a building’s docks showed units that weren’t its own.
- Gather picks a patch, not a 2 × 2 footprint. The player taps any ore tile; that tile names its patch, every tile of the same ore joined to it side to side or corner to corner. The Harvester drives toward the tapped tile. Within 4 tiles of the patch it takes the nearest tile no other Harvester is mining or heading for, and swaps if that tile is taken first or a nearer one frees up. With every tile taken it heads for the nearest anyway and waits in line behind the one there. There’s no turn-taking: a crowded patch slows down through traffic. Gather needs only the tapped tile explored. On screen the patch is shaded while aiming and while its building or Harvester is selected, and the Harvester’s own tile is outlined.
- A tile gives 2 a second, what a full footprint gave, so one Harvester keeps its timings and a patch of 9 tiles takes 9 at once. A mining Harvester stands still on screen.
- Units live inside their building. [Superseded: an entrance is now one tile, One entrance tile.] A building’s entrance is the whole row of tiles along its bottom edge, just outside it: 3 for a Depot, 6 for the ship. A unit going home drives to the nearest entrance tile and is inside the moment it stands on any of them: no queue for the door and none for unloading. Inside it takes up no tile. Going out, it comes out on the free entrance tile nearest where it’s going, and waits inside while every one has a ground unit on it. A blocked unit on its way in tries the nearest free entrance tile when it looks for a way around. Units are built inside, and the ship’s start there. The entrance tiles are ordinary ground that any unit drives over. The middle one is still the building’s dock: distances and the Scout’s reach count from it, and a crew builds a ghost from it.
- Entrances are kept clear. Placement is refused when the entrance would be off the map or covered by a building, or when the footprint covers another building’s or ghost’s entrance. Foundation may go on an entrance.
- On screen [Superseded: one arrow, on the one entrance tile], each entrance tile has an arrow pointing out of the building or ghost, and the placement preview shows them. A unit inside is drawn in its building’s bottom row, slot 0 in the middle, then left, then right, and is tapped there.
- A Harvester whose home is full says “Waiting for room to unload” with the amber light once unloading is done and its cargo didn’t fit. It looked busy before.
- Saves went to version 8. The first Depot is done at 1:57: the crew comes out at the end of the ship’s entrance nearest the ghost and has nobody to pass. The crowded hour collects 53,700 Basalite against 22,600, and 24,000 path searches fail against 457,000. It builds 133 Depots against 211, leaves 40,200 Basalite unspent, and its slowest tick is 64 ms against 9; neither is explained yet.
The rest of Tier 0’s buildings Tentative
The Storage Silo, Barracks, Sword Bot and Stone Wall, with the numbers in buildings.md and tier0-proposals.md, twice as fast. Discovery reveals, the Evolite sample and the study are left for later, so all five buildings are on the build bar from the start. The code is sword-bot.ts and the recipes in prototype/sim/src/recipes.ts; the tests are in prototype/sim/test/tier0-buildings.test.ts.
- Storage Silo: 67 Basalite, 2 × 2, 24 stacks, built in 10 s. It has no units, so it has no entrance and no vision, and it packs against anything: Silos against each other, or right on top of a Depot. (Before crews flew, the tile below its middle was kept clear while it was planned, for the crew to build from.) It joins networks like any building, so a full Depot or ship it touches moves its oldest stacks into it.
- Stone Wall: 3 Basalite a tile, laid by dragging like foundation, and built 33 to a trip, the crew hovering over each. It blocks movement and never joins networks. It goes on bare ground only: not on foundation, ore, or a building’s entrance, and nothing is built on it.
- Barracks: 100 Basalite, 3 × 3, 4 slots that build only Sword Bots (and only a Barracks builds them). It stores nothing, but has an empty storage so it joins networks. Units inside are drawn in its bottom row, and a fourth in the row above.
- Sword Bot: 5 tiles/s, vision 10, 22.5 s to build. With nothing to fight yet, its one mission is Guard: picking it, or Send all to guard on the Barracks, then tapping a tile, sends it there. Bots sent to one spot take the nearest free tiles around it (up to 3 tiles out), so they don’t queue for it. Recall brings them home. HP, engaging and patrols come with aliens.
- Saves went to version 9. The crowded hour and the opening’s timings are unchanged.
Flying crews Tentative
After the fourth playtest (Nov 2026). Planning roads just so a crew can reach a building it builds once wasn’t a fun puzzle, and keeping a tile clear under a planned Silo stopped it going on top of another building.
- Engineering Crews fly, like the Scout: straight over buildings, walls and units, taking up no tile. They still go in and out of their home by its entrance. The code is the
engineeringCrewrecipe inprototype/sim/src/recipes.ts. - A crew builds from over the middle of the building, or over the tile for foundation and walls, and still circles the edge on screen while it works.
- Nothing is kept clear for construction. A planned building keeps only its entrance clear, if it has units; a Silo keeps nothing.
- The road puzzle is for the units that work outside: Harvesters and Sword Bots still drive, and still need a way out of the base (base-network-and-inventory.md).
- The first Depot is done 0.2 s later (2349 ticks rather than 2345): the crew now flies to the Depot’s middle, 8 tiles across and 2 up, with two diagonal steps, rather than 8 tiles straight to its old dock. The crowded hour hasn’t been rerun.
- Saves stay at version 9. Loading an older save makes its crews fly. A walking crew sent to a build spot in a building’s middle got shut inside when the building appeared.
- Two units meeting head-on no longer dance or lock. One leaving a Depot and one coming home could meet, both step aside the same way on the same tick, meet again, and step back, for minutes. Now, when each one’s next step is the other’s tile, only the higher id looks for a way around; the lower waits. With no way round (every entrance tile at its home taken, or its ore tile), the higher steps to one side, or back, to let the lower by. Two heading for the same tile that meet head-on no longer count as waiting in line, which had each wait for the other forever. Before this, a row of Depots feeding one small patch locked up within a minute; the tests in
prototype/sim/test/crowding.test.tsplay it for 5 minutes. - A Harvester standing on a free tile of its patch mines it. Two could stand on each other’s claimed tiles, each waiting for the other to move.
- Tooltips are drawn by the game, from a
data-tipon anything that has one, after the pointer rests 0.35 s: the name in bold, then what it is, then its facts as bullets. The map has them too: whatever is on the tile under the pointer, while nothing is in hand. The browser’s owntitletooltips didn’t show in every browser. With a mouse only; phones get none yet. - The copy pass: every description is a few words plus facts (“Storage: 6 stacks”), with no tips. Distances show in meters (a tile is 4 m). Crews “fly to” builds, costs read “Cost: 100 Basalite”, and refusals are shorter (“that’s unexplored”).
- Bases have names and totals. Every building has a
Basenumber, kept by thenameBasessystem inprototype/sim/src/bases.tsby the rules in base-network-and-inventory.md; the tests are inprototype/sim/test/bases.test.ts. A building’s panel shows “Base storage: Crashed Ship” (or “Base 3”) with the base’s total of each resource instead of the building’s own stacks. The top bar no longer adds up every building. An older save gets its numbers on the first tick. - Escape puts away the tool in hand, or else closes the panel. The Opening button is gone; New map stays, and a first visit still starts on the opening.
Smooth motion Tentative
Units jumped a tile at a time: a Harvester every half second, a Scout 10 times a second. Now they glide (runtime-and-sync.md). The code is stepOf in prototype/sim/src/view.ts and aimUnits and glideUnits in prototype/client/src/render/renderer.ts; the tests are in prototype/sim/test/harvester.test.ts and scout.test.ts.
- A unit’s view says where it’s stepping and how far along it is (
step: the next tile and 0–1), from its path and the ticks spent toward the step. It’s only for show and isn’t saved: the sim still moves whole tiles, and the rules and timings are unchanged. - A waiting unit doesn’t edge forward. A ground unit whose next tile holds a unit that isn’t stepping off it (one standing still, waiting itself, or coming the other way) has no step, so it stays on its own tile rather than drawing into the other.
- The screen runs about one state behind. On each new state, every unit glides from where it’s drawn to where the state has it, over the time between states (measured, so it works at every speed) plus a fifth, so uneven states don’t leave it standing for a frame. A unit that moves more than 1.5 tiles between states, such as going in or out of its building, jumps.
- Flyers are drawn on a straight line. A flyer’s line is a staircase of straight and diagonal steps, so gliding from tile to tile zigzagged up to half a tile either side, and the Scout looked like it bounced. Its
stepalso names where its line ends, and the client draws it on the straight line from where it set off to there, as far along as its steps have taken it. Sent somewhere new mid-flight, it sets off again from where it’s drawn. Ground units are drawn on their tiles: their paths really turn. Measured in the browser, the Scout now strays about 0.1 tile from its line at most. - Crews at work and mining Harvesters keep their own animations on top.
- Measured in the browser at 1×: the Scout moves 3–7 pixels a frame and stands still for one frame in about 30, against a tile’s jump 10 times a second before.
Turning buildings Tentative
A building’s entrance was always along its bottom edge, so a base could only open downward. Now the player turns a building before placing it. The code is Facing, entranceOf and dockOf in prototype/shared/src/balance.ts, the facing on Footprint and Ghost in prototype/sim/src/components.ts, and turnBuilding and unitTile in prototype/client/src/tools.ts; the tests are in prototype/sim/test/crew.test.ts and prototype/client/test/tools.test.ts.
- A building faces down, left, up or right: the side its entrance is on (One entrance tile). Placement keeps that tile clear.
- Only the size stays. Every building the player places with an entrance is square (the Depot and Barracks are 3 × 3), so turning one moves only its entrance. A Storage Silo has no entrance and doesn’t turn. The Crashed Ship never turns.
- Turning on screen: with a Depot or Barracks in hand, R or the Rotate button on the bar along the bottom turns it a quarter turn clockwise, and the preview’s arrow follows. The next building starts turned the same way, so a row of them is quick to lay.
- Buttons show their keys: “Rotate (R)” and “Cancel (Esc)” on the bar, and “Cancel (Del)” on a planned building’s panel. Touch screens, which have no keys, show just the name.
- Units inside sit in the row along the entrance, slot 0 in the middle.
- Saves stay at version 9. Buildings and ghosts in an older save face down.
One entrance tile Tentative
A row of arrows along every building’s side looked like a wall of arrows. Now a building has one arrow and one entrance tile, and the ship’s ramp is on its side. The code is entranceOf and SHIP_FACING in prototype/shared/src/balance.ts, wallLines in prototype/client/src/render/renderer.ts, and the tests in prototype/sim/test/crowding.test.ts.
- A building’s entrance is one tile, just outside the middle of the side it faces. Units go in and out only there, one at a time; a unit inside waits while another stands on it, and a little traffic is fine. It’s still the building’s dock, and placement keeps only it clear.
- The Crashed Ship’s entrance is its rear ramp, in the middle of its right-hand short side. Its side is 4 tiles, so the ramp is the third tile down. Ships in older saves keep facing down.
- One arrow marks each entrance, on the building, its plan and the placement preview.
- Stone walls are drawn as lines, not blocks: a line through the middles of neighboring wall tiles, joined diagonally only where no side neighbor joins them, and a post on each tile. Planned walls are the same, fainter, and so is the drag preview.
- The opening now: the crew comes out of the ramp and flies 10 tiles across and 2 up to the first Depot, which is done at 1:57 (2359 ticks, against 2349).
- The crowded hour’s test takes about 66 s, against 51 s, as units queue for one tile; its invariants hold. The hour itself hasn’t been rerun.
- Saves stay at version 9.
Project Decided
- The code lives in
prototype/at the root of the repo, next todesign/andscripts/. - npm workspaces Tentative:
prototype/shared,prototype/sim,prototype/hostandprototype/client. The shared check functions, such ascanPlace, live in the shared package; the sim and the client both depend on it, and the client never depends on the sim Decided (simulation-architecture.md). The host package runs the sim and is the single-player worker’s own bundle (The protocol and host); the client is the plain client. A Node server for multiplayer will wire the same host to WebSockets. - Tooling: Vite for the client, Vitest for tests, TypeScript in strict mode with
noUncheckedIndexedAccess, and exhaustive switches. - TypeScript 6.0, not 7 Tentative. TypeScript 7 (the native port) is out, but typescript-eslint supports only up to 6.0, and the lint rule that bans
asandanyin the sim needs it (ecs-core.md). Move up when typescript-eslint does.npm run checkinprototype/runs the type check, the lint and the tests.
Client
What’s built is in The plain client.
- Panels are React Decided, and the client’s state lives in a Zustand store Decided. The worker’s messages write the latest state into the store, next to the commands the host hasn’t answered yet and client-only state (the selection, the open panel, the overlay). A panel reads the values it shows through a selector and re-renders only when they change, so 20 states a second don’t redraw panels whose numbers stayed the same. Because React updates elements in place, a button isn’t replaced under the player’s finger. Rules for the client’s code:
- Pixi is created once and lives outside React. The renderer reads the same store each frame. No
@pixi/react. - Selectors return plain values (numbers and strings), or a few of them through
useShallow. Each state arrives as new JSON, so a selector that returns an object from it re-renders on every tick. - Lists use stable keys (entity ids), so rows update in place.
- Considered: plain DOM (the earlier decision; every panel needs its own update code), Preact (smaller, with signals, but Claude writes React more reliably), and Astro (for pages built ahead of time, not a live client). See the decision log, #141.
- Pixi is created once and lives outside React. The renderer reads the same store each frame. No
- Renderer: PixiJS v8 Tentative. Three.js is the alternative Open (see open-questions.md):
- For Three.js: Claude knows it best, it’s the library the later art pipeline uses anyway, and it allows rendering 3D models live (lighting, day/night shading) instead of baking them into sprites.
- For Pixi: labels, grid lines, status lights and lettered squares are each a line of code; the screen maps directly to the world; and sprites cost less phone battery than lit 3D meshes. It’s the most used 2D renderer for web games (about 5.4 million npm downloads a month in October 2026, against Phaser’s 2.2 million). Phaser is a whole game framework, with its own loop, scenes and physics that this game wouldn’t use.
- What decides it is whether the final look is baked sprites or live 3D. The renderer only draws the view it’s given, so switching later touches only the client’s drawing code.
- Camera: hand-written pan and pinch-zoom Tentative. The
pixi-viewportlibrary does this, but it hasn’t had a release since November 2024.
Sim
- The ECS core’s API is in ecs-core.md. The access check is part of the first test Decided.
- Seconds become ticks by fixed rules Decided; see simulation-architecture.md.
- Entity ids Decided: increasing integers, never reused. Loops run in id order unless the design says otherwise.
- Random numbers Decided: a small, well-known seeded generator stored in the world. Which algorithm, and whether map generation gets its own stream, is chosen when it’s written.
- Map Tentative: 640 × 640 tiles. Terrain and deposits are stored as one flat array per layer, read through a
tileAt(x, y)function; what’s explored is stored in chunks (see The Scout and fog). No chunks yet. Chunks are for enormous maps, where they let the game generate, store and send only the parts that matter (tier0-proposals.md); Factorio’s are 32 × 32 tiles and Minecraft’s 16 × 16. ThetileAtfunction lets chunks replace the arrays later. - Pathfinding Decided: A* in four directions on the tile grid, run only when a goal changes. Only buildings and walls block movement.
- Time controls Decided: pause, step one tick, and a speed multiplier, in the host from the first build. Checking timings such as the 3:20 Depot needs fast-forward. These control the host, not the world, so they aren’t game commands.
- No saving Decided in the first test.
Boundary
- Commands Decided: place a ghost, cancel a ghost, demolish, switch a building on or off, send the Scout on a trip, and give the Harvester a Gather mission. Picking what a slot builds was added for the crowded hour Tentative.
- Messages between the worker and the page Tentative are plain objects that can be written as JSON, with no class instances, so the same messages can go over a WebSocket later. Their shapes are in
prototype/shared/src/protocol.ts(The protocol and host). - What gets sent Tentative: terrain once at the start, because it never changes. Each tick then carries the entities (units, buildings, ghosts) and the tiles newly explored. What’s visible right now is worked out by the client from unit positions and vision ranges, with a function from the shared package. See runtime-and-sync.md.
Testing Decided
- The Python cases are ported to TypeScript tests by hand. The test suite has no Python; the scripts in scripts/sim/ stay as the reference the ports are checked against.
- Timing tests run the sim with no screen and check the opening against balance-and-simulations.md.
- Runs come in two kinds Tentative. Both are games played with no screen, and both are deterministic: the same code and seed give the same game.
- Scripted runs send fixed commands on fixed ticks, such as the opening. Their results can be checked to the tick, so tests pin them. As the rules grow, each pinned number has to be updated whenever a rule it depends on changes, so they’re kept for the few results the design depends on, like the first Depot at about 3:20.
- Adaptive runs are played by a script that reads the world and reacts, like the crowded hour. A rule change shifts every decision after it, so the numbers aren’t pinned. Tests play the first 15 minutes with the development checks on and check what must always hold: no two Harvesters collect in one footprint, every slot’s unit exists, and all Basalite unloaded is in storage, in a crew’s cargo, or spent. The full run’s report is for people to read, to see what a change does to a whole game.
- Runs take no player time Tentative. A run’s script decides instantly, and the scripted opening’s 30 s pause is the only time it spends deciding. A real player takes time to choose a spot, scroll the map, read a building, or deal with something else, while resources sit unspent and crews wait. So a run’s clock is a lower bound: “the first Depot at 3:20” or “189 Depots in an hour” is what a player gets with no time spent thinking. Reading a run as a real session needs that gap in mind. If it matters, scripts could get a reaction delay or a limited number of actions a minute.
- Reports on a website Tentative. The designer is building this alongside an AI, so seeing what happened in a long run matters as much as the tests. Rather than asking the AI how a game went, they want to open it and look.
- Every run writes a report.
npm run runswritesprototype/reports/<run>.json, which is committed, so the latest copy is in the repo. Change a rule on a branch, rerun the runs, and that branch’s site shows what changed. The design site renders each one under Run reports. - What a report holds, for any run: where things stand every 5 minutes (buildings, units by state, stock, the share explored by distance, patches found, path searches, units with no path); each deposit patch the first time it’s seen, and by what; each Scout trip and the new tiles it explored; and the final state, listing every building and unit. The run’s own timeline and summary come along as text.
- Snapshots of every entity’s components, without the tile layers, are written every 5 minutes to
<run>.snapshots.json. They’re 1–2 MB for the crowded hour, so they stay out of git. They’re there to dig out something nobody thought to report, and a step towards replays. - The report’s shapes are in the shared package (
report.ts), next to the protocol. The site’s page reads the JSON and knows only those shapes, never the sim, the same wall the client has (simulation-architecture.md). - Not yet: a debug channel that systems write notes to, saying why they decided something (“Harvester 12 waited 40 s at its footprint”). Snapshots and the existing messages show what happened; a one-off probe answers why for now. When a bug needs it, each system would get its own buffer through its parameters, merged after each phase and off by default, so it doesn’t break the one-writer rule.
- Every run writes a report.
- Replays, later Open. Record a run (it’s deterministic, so the commands and seed may be enough) and play it back in the client, at up to 16× speed, skipping around. This comes well after the client.