/** * 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; }