A misplaced machine is finally undoable by mouse. The REMOVE button sits
with the tabs rather than on a page, because it's a mode, not a machine, and
it has to be reachable from any page. It stays armed between clicks —
demolition is usually plural — and clicking open ground does nothing, so a
stray click never reads as a broken tool. Toast: "UNIT RECLAIMED. THE FLOOR
REMEMBERS."
LANE-RENDER: the hover hook is the __remove ghost sentinel. The UI has no
Renderer reference and the only channel to the ghost is selectedBuild(),
which main.ts feeds to setGhost — so while remove is armed, bus._sel reads
{def: '__remove', dir: 0}. Key the demolition tint off that def id. It's safe
because sim's place handler ignores unknown defs, so main.ts's click-to-place
fires a place for __remove and nothing happens; the UI dispatches the real
remove itself. Exported as REMOVE_DEF and pinned by a test.
A magic string shared between two lanes via a NOTES file is not a design;
contract request filed for UIBus.setGhostMode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
166 lines
7.5 KiB
TypeScript
166 lines
7.5 KiB
TypeScript
/**
|
|
* The reference factory — the official deadlock-free M1 build.
|
|
*
|
|
* Round 1 shipped melt and then wedged solid at ~tick 1200: luma, coefficient packs and
|
|
* anchor slabs all overproduce, and with no splitter the surplus had nowhere legal to go,
|
|
* so every machine upstream jammed in turn. This layout fixes that with the two tools the
|
|
* graph actually gives you:
|
|
*
|
|
* - a splitter feeding TWO quantizers. When the press is full, quantizer A1 jams, its
|
|
* lane backs up, the splitter skips it, and every luma bar goes to A2 -> the crime
|
|
* quantizer -> macroblock bricks -> uplink. The demuxer never stalls.
|
|
* - a splitter on the slurry line with one branch to an uplink, so surplus chroma is
|
|
* voided instead of backing up into the demuxer.
|
|
*
|
|
* Nothing here is priority routing: splitters just skip blocked outputs, and that alone
|
|
* makes the factory self-balancing. Each consumer takes what it can swallow and the
|
|
* surplus keeps walking.
|
|
*
|
|
* ore x3 -> demuxer -+-> luma -> [split] -+-> quantizer A1 -> coeff -> DCT press --+
|
|
* | +-> quantizer A2 -> coeff -> crime -> bricks
|
|
* +-> slurry -> [split] -+-> subsampler 4:2:2 -> 4:2:0 ----------+
|
|
* | +-> uplink (void) |
|
|
* +-> mv-flux -> p-caster -> delta wafers ------------+ |
|
|
* v v
|
|
* GOP assembler A <- slab <- DCT press <---+
|
|
* |
|
|
* +-> GOP crate -> mosh -+-> MELT -> uplink
|
|
* +-> slab -> assembler B
|
|
* (i-only) -> mosh
|
|
*/
|
|
import type { Command, Dir, GameData } from '../contracts';
|
|
|
|
const N: Dir = 0, E: Dir = 1, S: Dir = 2, W: Dir = 3;
|
|
|
|
export function referenceFactory(data: GameData): Command[] {
|
|
// Fail loudly and specifically if DATA moves under us — a missing id here would
|
|
// otherwise surface as a mystery deadlock hours later.
|
|
const machine = (id: string): string => {
|
|
if (!data.machines.some((m) => m.id === id)) {
|
|
throw new Error(`referenceFactory: data/machines.json has no machine "${id}"`);
|
|
}
|
|
return id;
|
|
};
|
|
const recipeId = (id: string): string => {
|
|
if (!data.recipes.some((r) => r.id === id)) {
|
|
throw new Error(`referenceFactory: data/recipes.json has no recipe "${id}"`);
|
|
}
|
|
return id;
|
|
};
|
|
|
|
const cmds: Command[] = [];
|
|
let placed = 0;
|
|
|
|
/** Places a machine and returns the entity id the sim will assign it. */
|
|
const P = (def: string, x: number, y: number, dir: Dir = N): number => {
|
|
cmds.push({ kind: 'place', def: machine(def), pos: { x, y }, dir });
|
|
return ++placed;
|
|
};
|
|
const b = (x: number, y: number, dir: Dir): void => { P('belt', x, y, dir); };
|
|
const runX = (x0: number, x1: number, y: number, dir: Dir): void => {
|
|
const s = x0 <= x1 ? 1 : -1;
|
|
for (let x = x0; ; x += s) { b(x, y, dir); if (x === x1) break; }
|
|
};
|
|
const runY = (y0: number, y1: number, x: number, dir: Dir): void => {
|
|
const s = y0 <= y1 ? 1 : -1;
|
|
for (let y = y0; ; y += s) { b(x, y, dir); if (y === y1) break; }
|
|
};
|
|
const setRecipe = (entity: number, recipe: string): void => {
|
|
cmds.push({ kind: 'setRecipe', entity, recipe: recipeId(recipe) });
|
|
};
|
|
|
|
// ---- power: ASIC base + software-decoder burst + tanks.
|
|
//
|
|
// Round 2 ran four naked software decoders. Once DATA gave them real heat they all
|
|
// scrammed in lockstep and the factory blacked out, which is why melt stopped arriving.
|
|
// The fix is the shape the codex always implied: cool inflexible ASICs hold the floor,
|
|
// one hot decoder bursts on top of them, and the tanks ride out its downtime.
|
|
//
|
|
// Sized against the WORST case, not the average: the quantizers and subsamplers hand
|
|
// back ~30/s of compression bandwidth, but not while they're between crafts. Assume
|
|
// none of it. Four ASICs (80/s) against ~107/s draw leaves a 27/s hole for the ~16.7s
|
|
// the decoder is down = ~450 bandwidth-seconds, and three tanks hold 900. Two ASICs
|
|
// fewer and it survives only while compression happens to be running — measured that,
|
|
// it bottomed out at 2.2 of 600, which is not a margin, it's a coincidence.
|
|
for (let i = 0; i < 4; i++) P('decode-asic', -30 + i * 3, -14);
|
|
P('software-decoder', -18, -14);
|
|
P('buffer-tank', -15, -14);
|
|
P('buffer-tank', -12, -14);
|
|
P('buffer-tank', -9, -14);
|
|
|
|
// ---- mining. Three seams keep the demuxer at its 45-tick cadence (it eats 3 ore).
|
|
P('seam-extractor', -30, 0);
|
|
P('seam-extractor', -30, -3);
|
|
P('seam-extractor', -30, 3);
|
|
runY(-2, -1, -28, S); // north seam joins the trunk
|
|
runY(3, 1, -28, N); // south seam joins the trunk
|
|
runX(-28, -27, 0, E);
|
|
P('demuxer', -26, 0);
|
|
|
|
// ---- mv-flux -> delta wafers. This lane is the whole factory's bottleneck (a GOP
|
|
// crate needs 6 wafers = 12 mv-flux = 12 demux crafts), so nothing here overproduces.
|
|
runY(-1, -3, -25, N);
|
|
P('p-caster', -26, -5);
|
|
runX(-26, -11, -6, E);
|
|
runY(-6, -1, -10, S);
|
|
|
|
// ---- luma -> two quantizers behind a splitter. A1 feeds the press; when the press is
|
|
// satisfied A1 jams, the splitter skips it, and everything goes to A2 -> crime.
|
|
runX(-24, -23, 0, E);
|
|
P('lane-splitter', -22, 0);
|
|
|
|
runX(-21, -20, 0, E);
|
|
P('quantizer', -19, 0); // A1: quantize (default)
|
|
runX(-17, -16, 0, E); // coefficient packs -> press
|
|
runY(-1, -2, -19, N); // hf dust -> uplink
|
|
P('shipper', -19, -4);
|
|
|
|
runY(1, 3, -22, S);
|
|
P('quantizer', -23, 4); // A2: quantize (default)
|
|
runX(-21, -20, 4, E); // coefficient packs -> crime
|
|
const crime = P('quantizer', -19, 4);
|
|
setRecipe(crime, 'quantize-crime');
|
|
runY(6, 7, -23, S); // A2's hf dust -> uplink
|
|
P('shipper', -24, 8);
|
|
b(-19, 6, S); // crime's bricks + dust -> uplink (two lanes: crime
|
|
b(-18, 6, S); // outruns a single belt when A2 is at full tilt)
|
|
b(-18, 7, W);
|
|
P('shipper', -20, 7);
|
|
|
|
// ---- chroma. The splitter's void branch is what keeps surplus slurry from backing up
|
|
// into the demuxer and starving the mv-flux lane.
|
|
runY(2, 9, -26, S);
|
|
P('lane-splitter', -26, 10);
|
|
runY(11, 12, -26, S); // void branch
|
|
P('shipper', -27, 13);
|
|
runX(-25, -24, 10, E); // working branch
|
|
P('subsampler', -23, 10); // 4:2:2 (default)
|
|
runX(-21, -20, 10, E);
|
|
const sub420 = P('subsampler', -19, 10);
|
|
setRecipe(sub420, 'subsample-420');
|
|
b(-17, 10, E);
|
|
runY(10, 3, -16, N); // chroma 4:2:0 climbs to the press
|
|
b(-16, 2, E);
|
|
|
|
// ---- the press: 6 coefficient packs + 1 chroma 4:2:0 -> one anchor slab.
|
|
const press = P('dct-press', -15, 0);
|
|
setRecipe(press, 'press-anchor-slab');
|
|
runX(-12, -11, 0, E);
|
|
|
|
// ---- assembly -> mosh -> MELT.
|
|
P('gop-assembler', -10, 0); // assemble-gop (default)
|
|
runX(-7, -6, 0, E);
|
|
P('mosh-reactor', -5, 0);
|
|
runX(-2, -1, 0, E); // melt -> uplink
|
|
P('shipper', 0, 0);
|
|
|
|
// Recovered anchor slabs come back out of the reactor. Rather than sell them, feed the
|
|
// i-only assembler: 8 slabs -> another GOP crate -> more melt.
|
|
runY(3, 4, -5, S);
|
|
const iOnly = P('gop-assembler', -6, 5);
|
|
setRecipe(iOnly, 'assemble-gop-i-only');
|
|
runY(5, 3, -3, N);
|
|
|
|
return cmds;
|
|
}
|