glytch/fktry/lanes/LANE-SIM.md
LANE-SIM 3a42ab2d9e [sim] round 1 NOTES
Reports the M1 melt chain working (tick 1149) and two things the
orchestrator needs to decide: the DoD chain no longer exists after
LANE-DATA's codex landed (mosh now eats gop-crate, melt is 8 machines
deep), and the M1 factory deadlocks at ~tick 1200 on byproduct surplus
with no legal overflow. Proposes splitters as the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:25:22 +10:00

11 KiB
Raw Blame History

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 1 (goal: milestone M1 "FLOW")

Replace the stub in src/sim/index.ts with a real implementation:

  1. World + placement. Tile grid (start 64×64, world-centered). place command: validate footprint fits and tiles are free, spawn EntityState, emit placed. remove (emit removed, refund nothing yet), rotate, setPaused. Invalid commands are silently dropped (UI will preview validity later).
  2. Belts. kind:'belt' entities carry up to 2 items each as BeltItem (t: 0..1), moving at beltSpeed tiles/sec. Items transfer to the belt/machine the belt points into (dir). Items on a belt keep minimum spacing 0.45. Belt→machine: push into inputBuf if the machine's current recipe wants that item and buffer < 2× recipe need; otherwise the item waits (belt backs up — this is core Factorio feel, get it right). Machine→belt: outputs pop onto an adjacent belt facing away, one item per available slot.
  3. Crafting. Machines with recipes: when inputBuf covers inputs, consume, run ticks (progress 0..1), emit crafted, deposit outputs into outputBuf. If outputBuf holds ≥ 4× outputs, machine stalls with jammed:"output full" (emit jammed once per stall, not per tick). Extractors need no inputs. Machines with multiple recipes use setRecipe; default to recipes[0].
  4. Bandwidth (power) v1. Sum powerGen (power kind) vs powerDraw of all entities + active recipe bandwidth (note: negative recipe bandwidth GENERATES — quantizer compression pays power, per lore). If draw > gen: global speed multiplier = gen/draw for all progress and belt movement (brownout), emit brownout on/off edges, set snapshot.bandwidth accordingly. No storage/buffer tanks yet.
  5. Shipping. kind:'shipper' consumes ANY item arriving in its input, increments shippedTotal, emits shipped. Track the single active commission (first in data): fill commissionProgress, emit commissionDone once satisfied.
  6. Tests (minimum): placement collision; belt line moves an item end to end in the expected tick count; belt back-pressure (full line stops cleanly, no item loss/dupe); demux recipe consumes 2 ore → 1 luma + 1 slurry; brownout halves speed at draw=2×gen; determinism (two sims, same seed+commands, 500 ticks, deep-equal snapshots); shipping increments and fires commissionDone.

Definition of done: with the dev server running, the orchestrator can (via UI or console __fktryBus.dispatch(...)) place extractor→belts→demuxer→quantizer→ mosh-reactor→shipper and watch shippedTotal.melt climb. All tests green. NOTES written (decisions, gotchas, what you'd tackle in round 2).


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.