Review: all 5 lanes pass — 70/70 tests, tsc clean, ownership audit clean,
integration verified live (sim -> events -> UI ticker + SCREEN corruption).
Contracts v2 (granted from lane requests): UIBus.pickTile (wired), BeltItem.id?,
MachineDef.{color,flavor,heatPerTick,bufferCap}?, CommissionDef.repeat?,
SimEvent 'scram'. main.ts: non-left pointerdown no longer places. happy-dom added.
Rulings: canon melt chain IS M1 (masterplan updated); rank-routing stays, splitters
next; anchor-slab sanitizing -> Correction buyback fiction; build-bar paging approved;
per-lane dev ports 8151-8155. Round 2 orders written in all lane docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
168 lines
11 KiB
Markdown
168 lines
11 KiB
Markdown
# LANE-SIM — deterministic factory simulation
|
||
|
||
You are an Opus 4.8 executor on the SIM lane of FKTRY. Read `../MASTERPLAN.md` and
|
||
`../CONTRACTS.md` before anything else; the world bible is `../../docs/FKTRY_LORE.md`.
|
||
|
||
**Mission:** the headless, deterministic heart of the game. Grid, entities, belts,
|
||
recipes, power, events. If the renderer were deleted, your sim should still be fully
|
||
playable through unit tests. You are the lane the other four trust with their lives.
|
||
|
||
**Owned paths:** `src/sim/**` (including your tests: `src/sim/*.test.ts`).
|
||
**Never touch:** contracts, main.ts, other lanes, data JSONs (request changes via NOTES).
|
||
|
||
**Standing rules (all rounds):**
|
||
- Deterministic: seedable RNG (write your own mulberry32-style helper), no wall-clock,
|
||
no unseeded Math.random. Same seed + same commands = identical snapshots.
|
||
- Every mechanic lands with tests. `npm test` green before you write NOTES.
|
||
- Performance target: 1,000 entities + 5,000 belt items at 30 tps without heroics.
|
||
Prefer flat arrays and index maps over object soup. Don't allocate in the tick loop
|
||
when avoidable.
|
||
- Snapshot may reuse buffers, but its SHAPE must always match `SimSnapshot`.
|
||
|
||
---
|
||
|
||
## CURRENT ORDERS — Round 2 (goal: finish M1, open M2 "PRESSURE")
|
||
|
||
Round 1 review: outstanding. Verdicts on your report — **rank-routing STAYS** (it made
|
||
M1 flow; your proposal 2, auto-fallback, is REJECTED for exactly the reason you gave:
|
||
it deletes the byproduct tension). Proposal 3 (starved) APPROVED. Contracts v2 landed
|
||
for you: `BeltItem.id?`, `CommissionDef.repeat?`, `MachineDef.heatPerTick?/bufferCap?`,
|
||
`SimEvent 'scram'`. Extractor seam tiles: deferred to M3 worldgen, stop worrying.
|
||
|
||
1. **Reference factory.** `src/sim/reference.ts` exporting
|
||
`referenceFactory(data: GameData): Command[]` — your deadlock-free M1 layout
|
||
(second quantizer on crime, second assembler on i-only). Test: ships melt within
|
||
2,000 ticks AND is still shipping at 20,000 (the deadlock regression net). This
|
||
becomes the official demo; I wire it to a dev hook in main.ts next round.
|
||
2. **Splitters.** `kind:'splitter'` becomes real: 1 input, round-robin across outward-
|
||
facing adjacent belts; a blocked output is skipped, not waited on; your rank cache
|
||
traces THROUGH splitters (rank = best reachable downstream). This is the sanctioned
|
||
surplus-voiding tool — the rank rule elsewhere is unchanged.
|
||
3. **Buffer tanks.** `kind:'buffer'` + `bufferCap`: charge when gen>draw, discharge to
|
||
cover deficit, brownout only when tanks run dry; `snapshot.bandwidth.stored` = pool.
|
||
4. **Heat v1.** Accumulate `heatPerTick` while active + `RecipeDef.heat` per craft;
|
||
ambient dissipation; heat>0.7 → per-machine throttle (curve is your call, note it);
|
||
heat=1 → scram: machine stops, emits `{kind:'scram',on:true}`, restarts below 0.5
|
||
with `on:false`. `EntityState.heat` is already in the contract — make it real.
|
||
5. **Commission queue.** Data-order activation; `commissionDone` advances; `repeat:true`
|
||
re-enters at the back of the queue. Progress counts only while active (keep).
|
||
6. **`jammed:'starved'`** — recipe idle with empty inputs, edge-triggered like
|
||
'output full'.
|
||
7. **`BeltItem.id`** — stable per-item ids in every snapshot (optional field now;
|
||
hardens to required in v3, so just always set it).
|
||
|
||
**Definition of done:** all of the above tested (determinism suite still green);
|
||
reference factory test proves melt at scale with zero deadlock; heat demonstrably
|
||
throttles and scrams the software decoder in a test.
|
||
|
||
---
|
||
|
||
## NOTES (append-only log, newest at bottom — template below)
|
||
|
||
```
|
||
### Round N — YYYY-MM-DD — [your model]
|
||
SHIPPED: what actually works now (verified how)
|
||
DECISIONS: choices you made inside your mandate
|
||
BLOCKED/BROKEN: anything not working, honestly
|
||
CONTRACT REQUEST: (if any)
|
||
PROPOSAL: (ideas for the orchestrator, optional)
|
||
NEXT: what you'd do next round if asked
|
||
```
|
||
|
||
### Round 1 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED
|
||
- The stub is gone. `src/sim/` is now grid + placement, belts with back-pressure, crafting,
|
||
bandwidth/brownout, shipping and commissions. Files: `index.ts` (the sim), `geom.ts`
|
||
(grid math), `rng.ts` (seeded mulberry32), `sim.test.ts` (18 tests).
|
||
- Verified three ways, not one:
|
||
1. `npm run check` clean; `npm test` 70/70 green across all lanes (18 are mine).
|
||
2. Live at localhost:8150 (another session's dev server — see BLOCKED). App boots, zero
|
||
console errors. My numbers are visibly driving other lanes: the UI bandwidth readout
|
||
showed `9 DRAW / 20 GEN` for an ASIC + extractor + 4 belts + uplink, which is exactly
|
||
my power math; the fax panel reads my `activeCommission`/`commissionProgress`.
|
||
`__fktryBus.dispatch(...)` places entities and they render.
|
||
3. Ran the sim in the real browser engine by importing `/src/sim/index.ts` in the page:
|
||
the M1 chain shipped its first MELT at **tick 1149**, commission `first-taste` closed
|
||
at tick 1149 — the identical tick Node produces. Determinism holds across engines.
|
||
- Perf (measured in-browser, not estimated), against the 1,000 entity / 5,000 item target:
|
||
- 1,245 entities + 2,436 belt items → 0.138 ms/tick (242× realtime)
|
||
- 3,500 entities + 6,820 belt items → 0.418 ms/tick (**80× realtime**)
|
||
|
||
DECISIONS
|
||
- **Output-belt routing by rank — the big judgement call, please review.** A machine picks
|
||
which adjacent belt to hand an item to by tracing the belt chain to the machine it ends
|
||
at (cached; invalidated on any place/remove/rotate/setRecipe): rank 2 = that machine
|
||
consumes this item, 1 = it's an uplink (takes anything), 0 = dead end or loop (accept, so
|
||
a half-built belt still fills instead of looking broken), -1 = it will never take this
|
||
item, so refuse. It never falls back to a worse rank just because the best belt is
|
||
momentarily full. Why I did this: with no splitters or inserters, the demuxer's three
|
||
outputs (luma / slurry / mv-flux) have to leave one 2x2 perimeter toward three different
|
||
machines. Without ranking, items land on the wrong lane and wedge it permanently — M1
|
||
cannot flow at all. This is routing intelligence the orders didn't ask for; it's also the
|
||
root of the deadlock below. If you want it gone, splitters have to land first.
|
||
- Belt spacing is enforced *across the tile seam*: a blocked front item holds station 0.45
|
||
behind the rear item of the next belt rather than bunching at the boundary.
|
||
- Pause freezes sim time (`tick` stops advancing) but commands still apply — you can build
|
||
while halted. `tick` is sim-time, not wall-time.
|
||
- `jammed` reports only `'output full'`, edge-triggered (one event per stall, not per tick).
|
||
Starvation is not flagged — see PROPOSAL 3.
|
||
- Power: a negative recipe bandwidth adds to **gen** rather than subtracting from draw, so
|
||
the brownout multiplier stays `gen/draw`. Tested: the quantizer visibly pays for itself.
|
||
- Extractors mine anywhere — there is no seam map in `data/` yet.
|
||
- Tests assert against the recipe defs instead of hard-coding yields. This is deliberate:
|
||
my first pass hard-coded `demux-ore` and broke the moment LANE-DATA shipped. Mechanics are
|
||
mine to pin down; the numbers are theirs to move.
|
||
|
||
BLOCKED/BROKEN
|
||
- **My round-1 definition of done no longer exists.** The orders say place
|
||
extractor→demuxer→quantizer→mosh→shipper and watch melt climb. LANE-DATA's codex
|
||
transcription landed mid-round and the mosh reactor now eats `gop-crate`, not coefficient
|
||
packs. MELT is now **eight machine types** deep (ore→demux→{luma→quantizer→coeff;
|
||
slurry→subsampler ×2→chroma-420}→DCT press→anchor slab; mv-flux→p-caster→delta wafers;
|
||
→GOP assembler→mosh). I rebuilt against the real graph and it *does* ship melt (tick 1149,
|
||
verified above) — but "the whole fantasy in miniature" is now a ~50-entity build, not a
|
||
6-machine demo. Your call whether M1's demo target changes or DATA adds a starter recipe.
|
||
- **The M1 factory deadlocks permanently at ~tick 1200**, after 4 GOP crates / 12 melt.
|
||
This is not a mechanics bug — I probed it for 6000 ticks. Luma and coefficient packs
|
||
structurally overproduce (demux yields 2 luma/craft; the press is chroma-420-limited so it
|
||
only burns 6 coeff per ~180 ticks), and the press also jams on surplus anchor slabs (the
|
||
GOP assembler needs 1 slab per 6 delta wafers). Surplus has nowhere legal to go: my rank
|
||
rule forbids diverting a rank-2 item to a rank-1 uplink even when the preferred belt is
|
||
wedged. So the press jams → quantizer jams → demuxer jams → mv-flux stops → the p-caster
|
||
starves and the assembler sits at 5/6 delta wafers forever.
|
||
- The graph *is* solvable today without new code: a second quantizer on `quantize-crime`
|
||
drains surplus coeff into bricks, and a second assembler on `assemble-gop-i-only` eats
|
||
surplus slabs (8→crate). My demo build is just naive. But note a player currently
|
||
**cannot void surplus to an uplink**, which is the first thing a Factorio player reaches
|
||
for. That's the cost of the rank rule.
|
||
- `splitter` (`lane-splitter`) and `buffer` (`buffer-tank`) now exist in machines.json; they
|
||
place and draw power but do nothing. Not in my orders — flagging so nobody thinks they work.
|
||
- `heat` is always 0, `bandwidth.stored` always 0 (M2 / buffer tanks).
|
||
- Port 8150 is held by another session's dev server, and the harness won't start a second one
|
||
for this project. I verified against that server read-only. Note for whoever plays the
|
||
build in a Browser pane: the tab reports `visibilityState: hidden`, so rAF is paused and
|
||
the sim crawls (~9 ticks per screenshot). That's the pane, not the loop — importing the sim
|
||
module directly in the page runs it at ~96k ticks/sec.
|
||
|
||
CONTRACT REQUEST
|
||
- None. `contracts.ts` covered everything I needed this round.
|
||
|
||
PROPOSAL (yours to call, not mine to take)
|
||
1. **Splitters next.** `lane-splitter` with round-robin plus a priority/overflow output is
|
||
the canonical Factorio answer to the deadlock above, and it unlocks byproduct sinks and
|
||
the bloom-loop topology later. This is the one I'd pick.
|
||
2. Alternative, ~5 lines: allow a lower-ranked belt when every preferred belt is full *and*
|
||
outputBuf has hit the stall threshold. Cheap, but it makes byproduct management automatic
|
||
and quietly deletes a core Factorio tension — a gameplay decision, so I left it alone.
|
||
3. `jammed: 'starved'` for idle-with-no-input would let the inspector tell "backed up" from
|
||
"waiting". Cheap, but it's UI-visible, so it's LANE-UI's business as much as mine.
|
||
4. Extractors should require a seam tile — needs a seam/ore map from DATA and a worldgen call.
|
||
|
||
NEXT (round 2 if asked)
|
||
- `lane-splitter` with priority/overflow; `buffer-tank` feeding `bandwidth.stored` to smooth
|
||
brownouts.
|
||
- Heat: accumulate per craft, throttle >0.7, scram at 1 (the software decoder's whole bit).
|
||
- Save/load — `SimSnapshot` is already the right shape to serialize.
|
||
- A worked, deadlock-free reference factory you can paste into the console to demo M1.
|