From dbb3615a64ae8e352bcefbc5bd0ea12f8aa8d889 Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 17 Jul 2026 20:04:00 +1000 Subject: [PATCH] [data] the composite seam + a tech tree that can actually be bought MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 4 orders, all five. The headline is a bug I authored: the v4 gating rule (any id in a TechDef.unlocks is locked until that tech completes) turned three rounds of tech authoring into a total bootstrap deadlock. Measured: melt not producible at tick 0 (first-taste impossible), 0 of 24 techs reachable — every pack chain gated behind packs you cannot make. disc-artifact-bottling cost 10 analog packs and unlocked the only machine that bottles analog packs. Fixed as a shape, not a patch, and both rules are now tests: - the M1 melt chain is starting equipment (so first-taste is possible, and RENDER's era derivation correctly reads it as starting equipment) - one pack is craftable at tick 0 — SPATIAL, made from the quantizer's own sins, so your first research is a study of your own damage 23/23 nodes now reachable; six emptied disc nodes deleted (no lane references tech ids). Also: - §10 THE COMPOSITE SEAM transcribed: alpha/hard-matte/blue-hole/fringe-lace/ green-spill/mixed-pixel/roto-chatter/ghost-feather/sodium-light (forward canon), matte bath, optical printer, chroma keyer, roto bench, neural keyer (powerDraw 240 — the codex's "appetite that dwarfs everything"). - ARCHAEOLOGY LAB canonized (kind: lab), ungated — a lab behind research is a lab you can never build. - ASIC cooler live: the round-3 spec of 0.02 would have meant "never heats, free 100/s". Retuned to 0.0018, sane under both readings of the contract. - accents where output-derivation can't work; CODEX_REF regex widened for §10. Co-Authored-By: Claude Opus 4.8 --- fktry/data/CANON-DELTA.md | 132 ++++++++++++++++++++++++++++++++++++ fktry/data/commissions.json | 8 +++ fktry/data/items.json | 10 +++ fktry/data/machines.json | 44 +++++++++--- fktry/data/recipes.json | 27 ++++++++ fktry/data/tech.json | 61 ++++++++--------- fktry/data/validate.test.ts | 113 +++++++++++++++++++++++++++++- fktry/lanes/LANE-DATA.md | 97 ++++++++++++++++++++++++++ 8 files changed, 451 insertions(+), 41 deletions(-) diff --git a/fktry/data/CANON-DELTA.md b/fktry/data/CANON-DELTA.md index 4a20fd0..53b581e 100644 --- a/fktry/data/CANON-DELTA.md +++ b/fktry/data/CANON-DELTA.md @@ -99,6 +99,34 @@ SIM builds auras. Gated behind `broadcast-asic-cooling` so it can't appear early - Its own `coolPerTick: 0.02` is inert today (nothing heats it) and is there to state the intended aura strength in data rather than in prose. +## Added in round 4 + +### §10 THE COMPOSITE SEAM — transcribed + +The codex grew §10, so most of this round is transcription rather than invention. Every +item and machine in §10 is now data. Deviations and judgement calls only: + +| Call | Why | +|---|---| +| "Footage" = `anchor-slab` | §10's keyers key *footage*, but the codex never says what footage *is* as an item. The anchor slab is already "a complete, self-sufficient picture" (§2) — the only thing in the game you could point a keyer at. Every keyer takes slabs. | +| MATTE BATH's chemical = `static-canister` | §3 already says static canisters are "brine for preserving other artifacts". The bath needed a developer; the brine was sitting right there. | +| `ghost-feather` is made **only** by the neural keyer | §10: "every era below the neural keyer ruins it, so demand never dies." So the earlier machines *consume* it and ruin it — `bathe-feather` (feather → hard matte, "the machine does not apologize") and `key-hair` (feather → roto chatter). Both are jokes with a real cost, and both are the codex verbatim. | +| NEURAL KEYER `powerDraw: 240` | §10 says "24 VRAM — a bandwidth appetite that dwarfs everything else in the game", and "you don't unlock the neural keyer; you unlock the power plant it demands". 240/s is 12 decode ASICs, or ~9 software decoders after duty cycling. The whole M1 reference chain draws 96. It dwarfs the game, as ordered. | +| `sodium-light` has no recipe | §10: The Correction hoards the prisms. Forward canon; on the FORWARD_CANON allowlist. `sodium-requisition` commissions it anyway, with 30 BLUE HOLES accepted "if you lose your nerve". | +| ROTO BENCH is gated (disc), not free | §10 calls it "the economic floor under every keyer... the bench never does [fail]". Tempting to leave ungated, but ALPHA is "the rarest resource in the game" and an ungated bench hands it to you at tick 0. Gated cheaply (10 spatial) so the floor arrives early but the Composite Seam stays a mid-game fault. | + +Optical printer ratios hurt, as ordered: 4 anchor slabs + 2 colour plates → **1** alpha and +4 fringe lace, over 240 ticks. It is the worst alpha-per-slab in the game and it fringes +every single run, which is exactly what the iron lung is for. + +### THE ARCHAEOLOGY LAB (`kind: 'lab'`) — canonized + +The codex has no lab: §6 only says "TECH TREE = MEDIA ARCHAEOLOGY. You research *downward* +(excavating older strata for analog secrets) and *upward* (deriving newer codecs)." So the +lab is the dig site — named for the verb the codex already uses, not a generic "research +station". Ungated starting equipment, because a lab behind research is a lab you can never +build (there is now a test named exactly that). + --- ## The numbers, and the arithmetic behind them @@ -119,6 +147,49 @@ idle draw (every machine's powerDraw) 57 /s **Units (RULED round 3):** `gen`/`draw` are bandwidth per **second**; `stored` and `bufferCap` are bandwidth-**seconds**. My round-2 assumption held. +### Round 4: the tech tree was a total bootstrap deadlock + +The v4 gating rule — *any id in a `TechDef.unlocks` is locked until that tech completes* — +turned three rounds of tech authoring from decoration into a lock on the entire game. I +had written every node assuming gating didn't exist. Measured against the rule: + +``` +TICK 0, nothing researched: + melt producible? false <- first-taste, the opening commission, impossible + analog-pack producible? false + spatial-pack producible? false + temporal-pack producible? false + container-pack producible? false +RESEARCH CLOSURE: 0 of 24 techs reachable — every node stranded behind its own output +``` + +Every pack chain was gated behind packs you could not make. `disc-artifact-bottling` cost +10 analog packs and unlocked the only machine that can bottle an analog pack. The M1 melt +chain itself (mosh reactor, subsampler, `press-anchor-slab`) was gated, so the game opened +on an unbuildable factory and an unfillable commission, with no way to research out of it. + +**The fix is a shape, not a patch.** Two rules now hold, and both are tests: + +1. **The M1 melt chain is starting equipment** — ungated, so first-taste is possible at + tick 0. Ungating it also fixes RENDER for free: they derive a machine's era from the + tech that unlocks it and treat unmapped machines as starting equipment, which is exactly + what the M1 chain now is. +2. **One pack must be craftable at tick 0**, or research can never begin. That pack is the + **SPATIAL PACK** — macroblock bricks + ringing halos + slurry, all of which fall out of + the quantizer the player already owns. Your first research is a study of your own + damage, which is the most in-voice thing in the tree. Everything else chains off it: + spatial → analog → temporal → container → fortress. + +Result: **23 / 23 nodes reachable**, unlock order tells a story (phosphor seams → field +loom → analog packs → telecine → temporal → container → fortress), and the only items +unreachable with the whole tree bought are the four declared forward-canon. + +Six disc nodes were **deleted**, not repurposed, because their whole content became +starting equipment: `disc-artifact-bottling`, `disc-chroma-subsampling`, +`disc-macroblock-quarrying`, `disc-ringing-skim`, `disc-subsampling-420`, +`disc-mosh-reactor`. Grepped first — no lane references tech ids (RENDER reads +`unlocks` generically, never a specific node). Recorded here per the deprecation rule. + ### Round 2's heat was decorative — what went wrong Worth writing down, because it's a trap anyone can fall into. I sized round 2's @@ -194,6 +265,67 @@ than it spends, recharging 300 in **4.5 s** of the 7.25 s up-phase. That is the sentence — **a buffer tank is what makes software decoders survivable** — and it now closes. +### The ASIC cooler goes live: `coolPerTick 0.02 → 0.0018`, `coolRadius: 2` + +Round 3 spec'd the aura at 0.02 while it was inert. **The moment it became mechanical that +number was catastrophic:** the decoder banks 0.0033/tick, so a 0.02 aura means net +−0.0167 — it never heats at all, and a 2×2 cooler turns a 2×2 decoder into a permanent +free 100/s. A spec'd number and a live number are different objects; this is the second +time this round that a number only became wrong once something read it. + +Retuned to **0.0018**, which lands well under either reading of the contract's "this +machine's coolPerTick *also* applies" — deliberately, because SIM hasn't built it yet and +I don't want the design resting on my guess of an ambiguous sentence: + +``` + throttle scram period duty avg gen +decoder alone 10.1 s 14.5 s 23.9 s 30 % 25.8 /s ++ cooler, ADDITIVE 46.7 s 66.7 s 39.3 s 85 % 72.1 /s ++ cooler, REPLACING 15.6 s 22.2 s 20.4 s 55 % 46.4 /s ++ cooler at 0.02 (r3) never never — 100 % 100.0 /s <- broken either way +``` + +Both readings are playable and both are worth the tech, so **SIM can implement either and +the design holds**. Per tile, which is the real question when both machines are 2×2: + +``` +2 decode ASICs 40.0 /s over 8 tiles = 5.00 /s per tile +1 software decoder, bare 25.8 /s over 4 tiles = 6.44 /s per tile +decoder + cooler (additive) 77.1 /s over 8 tiles = 9.64 /s per tile +``` + +So the pairing is the best power in the game per tile (1.9× ASICs) — and it costs a tech, +two machines, and the floor space to keep them adjacent. That is the reward for engineering +around heat instead of suffering it, which is what the cooler is *for*. + +### Research: is the first node affordable off a first-taste-era factory? + +Worked path for the bootstrap pack, one bottler, one quantizer: + +``` +bottle-spatial-pack = 2 macroblock-bricks + 1 ringing-halo + 2 chroma-slurry, 60 ticks + 2 bricks <- quantize-crime x1 (1 coefficient pack, 20 ticks) + 1 halo <- skim-ringing x1 (3 coefficient packs, 40 ticks) + 4 coefficient packs <- quantize x4 (4 luma, 120 ticks) <- 6 mdat ore +THE QUANTIZER IS THE BOTTLENECK: 20 + 40 + 120 = 180 ticks per spatial pack + = 10.0 packs / minute on one quantizer +``` + +Against the early nodes: + +``` +broadcast-phosphor-seams 5 spatial = 0.5 min +reel-silver-seams 8 spatial = 0.8 min +disc-roto-bench 10 spatial = 1.0 min +broadcast-field-loom 15 spatial = 1.5 min <- opens the ANALOG PACK +``` + +A player who has shipped their first melt already owns a quantizer and can afford the first +four nodes inside two minutes of production. That's the intent: **the opening research is +paid for with the quantizer's own sins** — you study your own damage. Later nodes scale to +tens of minutes and demand the analog/temporal/container chains, which is where the floor +space goes. + ### `heat` on the hex splicer: `coolPerTick 0.001`, 0.08 per splice Net 0.035 per craft after the 45 ticks of cooling → 28.6 crafts → **42.9 s** of continuous diff --git a/fktry/data/commissions.json b/fktry/data/commissions.json index 6c01e45..5b7e709 100644 --- a/fktry/data/commissions.json +++ b/fktry/data/commissions.json @@ -43,10 +43,18 @@ "flavor": "One (1) GIBBS WRAITH, willingly.", "wants": { "gibbs-wraith": 1 }, "rewardBandwidth": 900 }, + { "id": "feather-proof", + "flavor": "3 GHOST FEATHERS. Gradient intact, tip genuinely transparent, no hard edge anywhere on the object. We have been sold nine hard mattes by nine suppliers who each insisted it was the same thing. It is not the same thing. Do not send a matte.", + "wants": { "ghost-feather": 3 }, "rewardBandwidth": 1100 }, + { "id": "container-integrity-review", "flavor": "4 CONTAINER PACKS and 20 EXPLOSIVE HITS, shipped in that order and not the other order. The last supplier shipped in the other order. There is no last supplier.", "wants": { "container-pack": 4, "explosive-hits": 20 }, "rewardBandwidth": 1400 }, + { "id": "sodium-requisition", + "flavor": "One (1) beam of SODIUM LIGHT, 589nm, in its prism. We are aware who holds the prisms. We are aware what they call it when you take one. We are prepared to call it something else. 30 BLUE HOLES accepted as consolation if you lose your nerve.", + "wants": { "sodium-light": 1, "blue-hole": 30 }, "rewardBandwidth": 1800 }, + { "id": "sky-blue-mislabeled", "flavor": "600 MACROBLOCK BRICKS, sky-blue, MISLABELED REC2020. Purpose: none of your business.", "wants": { "macroblock-bricks": 600, "gamut-paint-rec2020": 6 }, "rewardBandwidth": 2400 }, diff --git a/fktry/data/items.json b/fktry/data/items.json index 4d2213c..ec67b06 100644 --- a/fktry/data/items.json +++ b/fktry/data/items.json @@ -50,6 +50,16 @@ { "id": "mosquito-swarm", "name": "MOSQUITO SWARM (LIVE)", "codex": "5:mosquito-swarms", "tier": 2, "color": "#d0ffb0" }, { "id": "gibbs-wraith", "name": "GIBBS WRAITH", "codex": "5:gibbs-wraiths", "tier": 2, "color": "#bfe0ff" }, + { "id": "alpha", "name": "ALPHA", "codex": "10:alpha", "tier": 1, "color": "#b8e8f0" }, + { "id": "hard-matte", "name": "HARD MATTE", "codex": "10:hard-matte", "tier": 1, "color": "#e8e8e8" }, + { "id": "green-spill", "name": "GREEN SPILL", "codex": "10:green-spill", "tier": 1, "color": "#4fff30" }, + { "id": "mixed-pixel", "name": "MIXED PIXEL", "codex": "10:mixed-pixel", "tier": 1, "color": "#a04fd0" }, + { "id": "blue-hole", "name": "BLUE HOLE", "codex": "10:blue-hole", "tier": 2, "color": "#2f5fd0" }, + { "id": "fringe-lace", "name": "FRINGE LACE", "codex": "10:fringe-lace", "tier": 2, "color": "#fff0c0" }, + { "id": "roto-chatter", "name": "ROTO CHATTER", "codex": "10:roto-chatter", "tier": 2, "color": "#ff8f4f" }, + { "id": "ghost-feather", "name": "GHOST FEATHER", "codex": "10:ghost-feather", "tier": 2, "color": "#f0f8ff" }, + { "id": "sodium-light", "name": "SODIUM LIGHT (589nm)", "codex": "10:sodium-light", "tier": 3, "color": "#ffb020" }, + { "id": "stsz-ledger", "name": "STSZ LEDGER", "codex": "7:the-muxer", "tier": 2, "color": "#8fb0d8" }, { "id": "stco-map", "name": "STCO MAP", "codex": "7:the-muxer", "tier": 2, "color": "#6f90e0" }, { "id": "stts-metronome", "name": "STTS METRONOME", "codex": "7:the-muxer", "tier": 2, "color": "#d8b060" }, diff --git a/fktry/data/machines.json b/fktry/data/machines.json index 4bee78a..2fdc064 100644 --- a/fktry/data/machines.json +++ b/fktry/data/machines.json @@ -7,15 +7,15 @@ { "id": "belt", "name": "STREAM BELT", "codex": "4:belts", "kind": "belt", "footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 0, "beltSpeed": 2, - "asset": "belt", "color": "#4a4e52", + "asset": "belt", "color": "#4a4e52", "accent": "#5ad0ff", "flavor": "Moves cargo. Back-pressure is a feature. Riding the belt is discouraged in writing and in no other way." }, { "id": "lane-splitter", "name": "LANE SPLITTER", "codex": "4:demuxer", "kind": "splitter", "footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 1, - "asset": "lane-splitter", "color": "#55595e", + "asset": "lane-splitter", "color": "#55595e", "accent": "#5ad0ff", "flavor": "One lane in, two lanes out, round-robin, no favourites. The only fair machine in the Bitstream." }, { "id": "buffer-tank", "name": "BUFFER TANK", "codex": "2:buffer-charge", "kind": "buffer", "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "bufferCap": 300, - "asset": "buffer-tank", "color": "#5a6068", + "asset": "buffer-tank", "color": "#5a6068", "accent": "#7fe0ff", "flavor": "Smoothness in a tank. Drains at exactly one frame per 16.6ms. Keep it above the red line; below the red line the audio has opinions." }, { "id": "demuxer", "name": "DEMUXER", "codex": "4:demuxer", "kind": "crafter", @@ -84,18 +84,46 @@ { "id": "decode-asic", "name": "DECODE ASIC", "codex": "4:decode-asic", "kind": "power", "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 20, - "asset": "decode-asic", "color": "#b9bec4", + "asset": "decode-asic", "color": "#b9bec4", "accent": "#6fe8c0", "flavor": "Cool, efficient, inflexible. Handles standard profiles and declines to imagine anything else. It will outlive you." }, { "id": "software-decoder", "name": "SOFTWARE DECODER", "codex": "4:software-decoder", "kind": "power", "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 100, "heatPerTick": 0.0033, "coolPerTick": 0.001, - "asset": "software-decoder", "color": "#6e3a34", + "asset": "software-decoder", "color": "#6e3a34", "accent": "#ff4a2a", "flavor": "Decodes ANYTHING, at any temperature. Five ASICs at full song, one third of the time. Throttles at 0.7, scrams at 1.0, screams throughout. The lag is the damage model." }, { "id": "asic-cooler", "name": "ASIC COOLER", "codex": "2:heat", "kind": "power", - "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 5, "coolPerTick": 0.02, - "asset": "asic-cooler", "color": "#a8b4bc", + "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 5, + "coolPerTick": 0.0018, "coolRadius": 2, + "asset": "asic-cooler", "color": "#a8b4bc", "accent": "#9fe8ff", "flavor": "Decodes a little and vents a lot. Stand beside it in summer. Do not stand beside it in the dark, when you can see what it is venting." }, + { "id": "matte-bath", "name": "MATTE BATH", "codex": "10:matte-bath", "kind": "crafter", + "footprint": { "x": 3, "y": 2 }, "recipes": ["bathe-hard-matte", "bathe-feather"], "powerDraw": 5, + "asset": "matte-bath", "color": "#5a4a4a", + "flavor": "Footage in, hard mattes out, softness destroyed. A GHOST FEATHER fed in comes out a HARD MATTE and the machine does not apologize. One mistake ruins the reel." }, + { "id": "optical-printer", "name": "OPTICAL PRINTER", "codex": "10:optical-printer", "kind": "crafter", + "footprint": { "x": 5, "y": 4 }, "recipes": ["print-composite", "print-blue-screen"], "powerDraw": 18, + "asset": "optical-printer", "color": "#6a5a3a", + "flavor": "The iron lung. A cathedral of bellows and lenses that splits footage into three colours and recombines them slightly wrong. It fringes everything it touches, and it is beautiful, and it is slow." }, + { "id": "chroma-keyer", "name": "CHROMA KEYER", "codex": "10:chroma-keyer", "kind": "crafter", + "footprint": { "x": 2, "y": 2 }, + "recipes": ["key-chroma", "key-hair", "key-semitransparent"], "powerDraw": 6, + "asset": "chroma-keyer", "color": "#3a6a3a", + "flavor": "Cheap, fast, and thirty-year-old maths. Weeps GREEN SPILL from its seams. Chokes on hair. Do not show it anything semi-transparent; it will decide the pixel belongs to neither of you." }, + { "id": "roto-bench", "name": "ROTO BENCH", "codex": "10:roto-bench", "kind": "crafter", + "footprint": { "x": 2, "y": 2 }, "recipes": ["roto-cutout", "roto-mixed"], "powerDraw": 2, + "asset": "roto-bench", "color": "#6a5540", + "flavor": "Converts anything to a cutout. Perfectly. Eventually. Twenty-four frames of pain per second of footage. When the automation fails the bench never does — it just costs your life instead." }, + { "id": "neural-keyer", "name": "NEURAL KEYER", "codex": "10:neural-keyer", "kind": "crafter", + "footprint": { "x": 4, "y": 4 }, "recipes": ["neural-key", "neural-unmix"], "powerDraw": 240, + "asset": "neural-keyer", "color": "#2a2a32", + "flavor": "Trained on ten thousand synthetic ghosts, because reality could not teach it — you cannot film the answer. Perfect alpha through glass, chainmail and moving hair. You do not unlock this machine. You unlock the power plant it demands." }, + + { "id": "archaeology-lab", "name": "ARCHAEOLOGY LAB", "codex": "6:tech-tree", "kind": "lab", + "footprint": { "x": 3, "y": 3 }, "recipes": [], "powerDraw": 6, + "asset": "archaeology-lab", "color": "#5a5548", "accent": "#ffc24a", + "flavor": "Where the factory reads its own strata. Feed it bottled artifacts and it derives the codec that made them: downward for analog secrets, upward for newer sins. The coffee predates the Reel Beds." }, + { "id": "muxer", "name": "THE MUXER", "codex": "7:the-muxer", "kind": "crafter", "footprint": { "x": 5, "y": 5 }, "recipes": ["assemble-moov-brain", "mux-container"], "powerDraw": 40, "asset": "muxer", "color": "#6a7078", @@ -103,6 +131,6 @@ { "id": "shipper", "name": "SCREEN UPLINK", "codex": "7:the-muxer", "kind": "shipper", "footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5, - "asset": "shipper", "color": "#46525e", + "asset": "shipper", "color": "#46525e", "accent": "#ff5ad0", "flavor": "Composites whatever you hand it onto THE SCREEN. There is no undo. The absence of undo is the product." } ] diff --git a/fktry/data/recipes.json b/fktry/data/recipes.json index 0f142e8..9bed9e3 100644 --- a/fktry/data/recipes.json +++ b/fktry/data/recipes.json @@ -114,6 +114,33 @@ "inputs": { "melt": 2, "bloom-concentrate": 1, "judder-cams": 1 }, "outputs": { "temporal-pack": 1 }, "ticks": 90, "bandwidth": 8 }, + { "id": "bathe-hard-matte", "machine": "matte-bath", "inputs": { "anchor-slab": 1, "static-canister": 1 }, + "outputs": { "hard-matte": 2 }, "ticks": 60, "bandwidth": 5 }, + { "id": "bathe-feather", "machine": "matte-bath", "inputs": { "ghost-feather": 1 }, + "outputs": { "hard-matte": 1 }, "ticks": 45, "bandwidth": 4 }, + + { "id": "print-composite", "machine": "optical-printer", "inputs": { "anchor-slab": 4, "color-plate": 2 }, + "outputs": { "alpha": 1, "fringe-lace": 4 }, "ticks": 240, "bandwidth": 22 }, + { "id": "print-blue-screen", "machine": "optical-printer", "inputs": { "anchor-slab": 4, "chroma-420": 2 }, + "outputs": { "alpha": 2, "fringe-lace": 4, "blue-hole": 2 }, "ticks": 240, "bandwidth": 22 }, + + { "id": "key-chroma", "machine": "chroma-keyer", "inputs": { "anchor-slab": 1, "chroma-slurry": 1 }, + "outputs": { "alpha": 2, "green-spill": 2 }, "ticks": 45, "bandwidth": 8 }, + { "id": "key-hair", "machine": "chroma-keyer", "inputs": { "ghost-feather": 1, "chroma-slurry": 1 }, + "outputs": { "roto-chatter": 2, "green-spill": 1 }, "ticks": 45, "bandwidth": 8 }, + { "id": "key-semitransparent", "machine": "chroma-keyer", "inputs": { "alpha": 1, "chroma-slurry": 1 }, + "outputs": { "mixed-pixel": 2, "green-spill": 1 }, "ticks": 45, "bandwidth": 8 }, + + { "id": "roto-cutout", "machine": "roto-bench", "inputs": { "anchor-slab": 1 }, + "outputs": { "alpha": 1, "roto-chatter": 1 }, "ticks": 720, "bandwidth": 2 }, + { "id": "roto-mixed", "machine": "roto-bench", "inputs": { "mixed-pixel": 4 }, + "outputs": { "alpha": 1, "roto-chatter": 2 }, "ticks": 900, "bandwidth": 2 }, + + { "id": "neural-key", "machine": "neural-keyer", "inputs": { "anchor-slab": 1 }, + "outputs": { "alpha": 4, "ghost-feather": 1 }, "ticks": 30, "bandwidth": 20 }, + { "id": "neural-unmix", "machine": "neural-keyer", "inputs": { "mixed-pixel": 2 }, + "outputs": { "chroma-slurry": 1, "luma-bars": 1 }, "ticks": 30, "bandwidth": 20 }, + { "id": "assemble-moov-brain", "machine": "muxer", "inputs": { "stsz-ledger": 1, "stco-map": 1, "stts-metronome": 1, "stss-keyring": 1 }, "outputs": { "moov-brain": 1 }, "ticks": 300, "bandwidth": 30 }, diff --git a/fktry/data/tech.json b/fktry/data/tech.json index d1fcc49..150e6d6 100644 --- a/fktry/data/tech.json +++ b/fktry/data/tech.json @@ -1,47 +1,43 @@ [ - { "id": "reel-silver-seams", "era": "reel", "cost": { "analog-pack": 5 }, - "unlocks": ["extract-silver-frames"] }, - { "id": "reel-telecine", "era": "reel", "cost": { "analog-pack": 25, "temporal-pack": 15 }, - "unlocks": ["telecine-puller", "mill-judder"] }, - - { "id": "broadcast-phosphor-seams", "era": "broadcast", "cost": { "analog-pack": 5 }, + { "id": "broadcast-phosphor-seams", "era": "broadcast", "cost": { "spatial-pack": 5 }, "unlocks": ["extract-field-splinters"] }, - { "id": "broadcast-field-loom", "era": "broadcast", "cost": { "analog-pack": 20 }, + { "id": "broadcast-field-loom", "era": "broadcast", "cost": { "spatial-pack": 15 }, "unlocks": ["field-loom", "weave-fields", "loom-vhold-roll"] }, - { "id": "broadcast-asic-cooling", "era": "broadcast", "cost": { "analog-pack": 30, "spatial-pack": 10 }, + { "id": "broadcast-asic-cooling", "era": "broadcast", "cost": { "spatial-pack": 20, "analog-pack": 15 }, "unlocks": ["asic-cooler"] }, - { "id": "disc-artifact-bottling", "era": "disc", "cost": { "analog-pack": 10 }, - "unlocks": ["artifact-bottler", "can-static"] }, - { "id": "disc-chroma-subsampling", "era": "disc", "cost": { "analog-pack": 10 }, - "unlocks": ["subsampler", "subsample-422"] }, - { "id": "disc-macroblock-quarrying", "era": "disc", "cost": { "analog-pack": 15 }, - "unlocks": ["quantize-crime"] }, - { "id": "disc-ringing-skim", "era": "disc", "cost": { "analog-pack": 10, "spatial-pack": 15 }, - "unlocks": ["skim-ringing"] }, - { "id": "disc-subsampling-420", "era": "disc", "cost": { "analog-pack": 20, "spatial-pack": 15 }, - "unlocks": ["subsample-420", "press-anchor-slab"] }, - { "id": "disc-mosh-reactor", "era": "disc", "cost": { "analog-pack": 20, "spatial-pack": 10 }, - "unlocks": ["mosh-reactor", "mosh"] }, + { "id": "reel-silver-seams", "era": "reel", "cost": { "spatial-pack": 8 }, + "unlocks": ["extract-silver-frames"] }, + { "id": "reel-telecine", "era": "reel", "cost": { "analog-pack": 25 }, + "unlocks": ["telecine-puller", "mill-judder"] }, + { "id": "reel-matte-bath", "era": "reel", "cost": { "analog-pack": 20, "spatial-pack": 10 }, + "unlocks": ["matte-bath", "bathe-hard-matte", "bathe-feather"] }, + { "id": "reel-optical-printer", "era": "reel", "cost": { "analog-pack": 40, "spatial-pack": 30 }, + "unlocks": ["optical-printer", "print-composite", "print-blue-screen"] }, + + { "id": "disc-roto-bench", "era": "disc", "cost": { "spatial-pack": 10 }, + "unlocks": ["roto-bench", "roto-cutout", "roto-mixed"] }, { "id": "disc-gamut-paints", "era": "disc", "cost": { "spatial-pack": 20 }, "unlocks": ["mix-rec709", "mix-dci-p3"] }, - { "id": "disc-gop-cadence-ibbp", "era": "disc", "cost": { "spatial-pack": 25, "temporal-pack": 10 }, + { "id": "disc-gop-cadence-ibbp", "era": "disc", "cost": { "spatial-pack": 25, "analog-pack": 10 }, "unlocks": ["cast-janus-wafer", "assemble-gop-ibbp"] }, - { "id": "disc-i-only-cadence", "era": "disc", "cost": { "spatial-pack": 30, "temporal-pack": 20 }, + { "id": "disc-i-only-cadence", "era": "disc", "cost": { "spatial-pack": 30, "analog-pack": 20 }, "unlocks": ["assemble-gop-i-only"] }, - - { "id": "stream-software-decoding", "era": "stream", "cost": { "analog-pack": 20, "temporal-pack": 20 }, - "unlocks": ["software-decoder"] }, - { "id": "stream-demosaic-vng4", "era": "stream", "cost": { "spatial-pack": 30 }, + { "id": "disc-demosaic-vng4", "era": "disc", "cost": { "spatial-pack": 30 }, "unlocks": ["demosaic-vng4", "pan-speckle"] }, - { "id": "stream-bloom-loops", "era": "stream", "cost": { "spatial-pack": 20, "temporal-pack": 30 }, + + { "id": "stream-bloom-loops", "era": "stream", "cost": { "spatial-pack": 20, "analog-pack": 20 }, "unlocks": ["bloom-duplicator", "tap-bloom", "bloom-grade-1", "bloom-grade-2"] }, - { "id": "stream-runaway-bloom", "era": "stream", "cost": { "temporal-pack": 45, "container-pack": 10 }, - "unlocks": ["bloom-grade-3", "bloom-grade-4", "bloom-grade-5"] }, - { "id": "stream-container-archaeology", "era": "stream", "cost": { "spatial-pack": 25, "temporal-pack": 15 }, - "unlocks": ["demux-container", "spool-offset-ribbon", "cut-sync-key", "bottle-container-pack"] }, + { "id": "stream-software-decoding", "era": "stream", "cost": { "analog-pack": 20, "spatial-pack": 25 }, + "unlocks": ["software-decoder"] }, { "id": "stream-optical-flow", "era": "stream", "cost": { "temporal-pack": 25 }, "unlocks": ["mv-extractor", "extract-motion"] }, + { "id": "stream-chroma-keyer", "era": "stream", "cost": { "spatial-pack": 25, "temporal-pack": 20 }, + "unlocks": ["chroma-keyer", "key-chroma", "key-hair", "key-semitransparent"] }, + { "id": "stream-container-archaeology", "era": "stream", "cost": { "spatial-pack": 25, "temporal-pack": 15 }, + "unlocks": ["demux-container", "spool-offset-ribbon", "cut-sync-key", "bottle-container-pack"] }, + { "id": "stream-runaway-bloom", "era": "stream", "cost": { "temporal-pack": 45, "container-pack": 10 }, + "unlocks": ["bloom-grade-3", "bloom-grade-4", "bloom-grade-5"] }, { "id": "stream-laser-primaries", "era": "stream", "cost": { "spatial-pack": 40, "temporal-pack": 20 }, "unlocks": ["mix-rec2020"] }, { "id": "stream-hex-splicing", "era": "stream", "cost": { "container-pack": 25, "temporal-pack": 25 }, @@ -51,6 +47,9 @@ "cost": { "container-pack": 40, "temporal-pack": 30, "spatial-pack": 30 }, "unlocks": ["tally-stsz-ledger", "chart-stco-map", "wind-stts-metronome", "forge-stss-keyring", "cut-ftyp-seal", "assemble-moov-brain"] }, + { "id": "fortress-neural-keyer", "era": "fortress", + "cost": { "container-pack": 50, "analog-pack": 50, "spatial-pack": 50, "temporal-pack": 50 }, + "unlocks": ["neural-keyer", "neural-key", "neural-unmix"] }, { "id": "fortress-the-muxer", "era": "fortress", "cost": { "container-pack": 60, "analog-pack": 40, "spatial-pack": 40, "temporal-pack": 40 }, "unlocks": ["muxer", "mux-container"] } diff --git a/fktry/data/validate.test.ts b/fktry/data/validate.test.ts index d976301..23035c2 100644 --- a/fktry/data/validate.test.ts +++ b/fktry/data/validate.test.ts @@ -22,10 +22,15 @@ const tech = techJson as unknown as TechDef[]; const commissions = commissionsJson as unknown as CommissionDef[]; const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/; -const CODEX_REF = /^[1-9]:[a-z0-9]+(-[a-z0-9]+)*$/; +// Multi-digit section numbers: the codex grew a §10 (THE COMPOSITE SEAM) and the old +// single-digit form silently rejected every reference into it. +const CODEX_REF = /^[1-9][0-9]*:[a-z0-9]+(-[a-z0-9]+)*$/; const HEX = /^#[0-9a-f]{6}$/; -const KINDS: MachineKind[] = ['extractor', 'crafter', 'belt', 'splitter', 'power', 'buffer', 'shipper']; +const KINDS: MachineKind[] = [ + 'extractor', 'crafter', 'belt', 'splitter', 'power', 'buffer', 'shipper', + 'lab', // v4 +]; const ERAS: TechDef['era'][] = ['reel', 'broadcast', 'disc', 'stream', 'fortress']; const itemIds = new Set(items.map((i) => i.id)); @@ -153,6 +158,37 @@ describe('machines', () => { expect(bad).toEqual([]); }); + // v4: the emissive accent falls back to a colour derived from the first output item. + // A machine with no recipes has no first output, so the fallback has nothing to + // derive from — those must carry an explicit accent or they render unlit. + it('every machine with no output to derive from carries an explicit accent', () => { + const bad = machines + .filter((m) => m.recipes.length === 0 && !m.accent) + .map((m) => `${m.id} (${m.kind}) has no recipes and no accent — nothing to derive an accent from`); + expect(bad).toEqual([]); + }); + + it('every accent is a hex colour, and differs from the chassis it sits on', () => { + const bad = machines + .filter((m) => m.accent !== undefined) + .filter((m) => !HEX.test(m.accent!) || m.accent === m.color) + .map((m) => `${m.id} -> accent ${m.accent} on chassis ${m.color}`); + expect(bad).toEqual([]); + }); + + it('a cooling aura belongs only to a machine that actually cools', () => { + const bad = machines + .filter((m) => m.coolRadius !== undefined) + .filter((m) => !(m.coolRadius! >= 1) || !Number.isInteger(m.coolRadius) || !(m.coolPerTick! > 0)) + .map((m) => `${m.id} -> coolRadius ${m.coolRadius}, coolPerTick ${m.coolPerTick}`); + expect(bad).toEqual([]); + }); + + it('exactly one lab exists, and research is impossible without it', () => { + const labs = machines.filter((m) => m.kind === 'lab'); + expect(labs.map((m) => m.id)).toEqual(['archaeology-lab']); + }); + it('only buffers declare a capacity, and they all do', () => { const bad = [ ...machines.filter((m) => m.kind === 'buffer' && !(m.bufferCap! > 0)) @@ -400,6 +436,77 @@ describe('commissions', () => { }); }); +// The v4 gating rule ("a machine/recipe id in ANY TechDef.unlocks is locked until that +// tech completes") turned the tech tree from decoration into a lock on the whole game. +// It also turned three rounds of tech authoring into a bootstrap deadlock: every pack +// chain was gated behind packs you could not yet make, so 0 of 24 nodes were reachable +// and first-taste was impossible. These tests are why that can't come back. +describe('the bootstrap: what the player can do before researching anything', () => { + const gated = new Set(tech.flatMap((t) => t.unlocks)); + /** Under the v4 rule a recipe is runnable only if neither it nor its machine is gated. */ + const ungated = recipes.filter((r) => !gated.has(r.id) && !gated.has(r.machine)); + + /** Items reachable from a given set of runnable recipes. */ + function reachableFrom(rs: RecipeDef[]): Set { + const have = new Set(); + for (;;) { + const before = have.size; + for (const r of rs) { + if (Object.keys(r.inputs).every((i) => have.has(i))) { + for (const o of Object.keys(r.outputs)) have.add(o); + } + } + if (have.size === before) return have; + } + } + + it('MELT is craftable at tick 0 — the opening commission must be possible', () => { + // first-taste wants 1 melt and is the head of the queue. If the melt chain is gated, + // the game opens on a commission the player cannot fill and research they cannot start. + expect([...reachableFrom(ungated)]).toContain('melt'); + }); + + it('at least one science pack is craftable at tick 0, or research can never start', () => { + const day0 = reachableFrom(ungated); + const packs = items.filter((i) => i.id.endsWith('-pack')).map((i) => i.id); + const bootstrap = packs.filter((p) => day0.has(p)); + expect(bootstrap.length).toBeGreaterThan(0); + }); + + it('every tech is eventually affordable — no node is stranded behind its own output', () => { + // Walk the real loop: research what you can afford, which unlocks recipes, which makes + // more packs, which affords more research. Anything left over can never be bought. + const open = new Set(ungated.map((r) => r.id)); + let have = reachableFrom(recipes.filter((r) => open.has(r.id))); + const unlocked = new Set(); + for (;;) { + let moved = false; + for (const t of tech) { + if (unlocked.has(t.id)) continue; + if (Object.keys(t.cost).every((c) => have.has(c))) { + unlocked.add(t.id); + for (const u of t.unlocks) { + for (const r of recipes) if (r.id === u || r.machine === u) open.add(r.id); + } + moved = true; + } + } + if (!moved) break; + have = reachableFrom(recipes.filter((r) => open.has(r.id))); + } + const stranded = tech + .filter((t) => !unlocked.has(t.id)) + .map((t) => `${t.id} costs ${JSON.stringify(t.cost)} — unaffordable no matter what you build`); + expect(stranded).toEqual([]); + }); + + it('the lab is starting equipment', () => { + const labs = machines.filter((m) => m.kind === 'lab').map((m) => m.id); + const lockedLabs = labs.filter((id) => gated.has(id)); + expect(lockedLabs).toEqual([]); // a lab behind research is a lab you can never build + }); +}); + describe('economy reachability', () => { it('the M1 flow reaches MELT from raw ore', () => { expect([...producible()]).toContain('melt'); @@ -412,6 +519,8 @@ describe('economy reachability', () => { // the Fortress. const FORWARD_CANON = new Set([ 'mosquito-swarm', 'gibbs-wraith', 'cinema-pack', + // §10: The Correction hoards the prisms. Nothing in the factory makes 589nm. + 'sodium-light', ]); const have = producible(); const orphans = items diff --git a/fktry/lanes/LANE-DATA.md b/fktry/lanes/LANE-DATA.md index 31ed54e..b8cf640 100644 --- a/fktry/lanes/LANE-DATA.md +++ b/fktry/lanes/LANE-DATA.md @@ -314,3 +314,100 @@ POST-PUSH ADDENDUM (same round, after LANE-UI landed `src/ui/live.test.ts`): **Five sessions share one index: `git add ` does not isolate a lane.** Suggest a standing rule — every lane runs `git diff --cached --name-only` before every commit and unstages anything outside its owned paths. + +### Round 4 — 2026-07-17 — Opus 4.8 + +SHIPPED: all 5 orders. §10 transcribed (9 items, 5 machines, 11 recipes, 5 era-gated tech +nodes, 2 commissions). Research economy: the ARCHAEOLOGY LAB + a full tech-cost audit. +ASIC cooler activated. Accents filled. items 51 → 60, machines 23 → 29, recipes 51 → 62, +commissions 14 → 16, tech 24 → 23 (six deleted, five added). Validator 41 → 49 tests. +- Verified: tsc clean (and `data/` is now genuinely typechecked — thank you), my 49 green, + live build boots with no console errors, all 29 machines art-directed with zero gaps, + UI grew a RESEARCH page and a third REFINE page off my data with no code from them. +- Fault-injected every new check, including re-gating the mosh reactor: fails instantly on + `MELT is craftable at tick 0 — the opening commission must be possible`. + +**THE HEADLINE — the v4 gating rule made the whole game unplayable, and I'd authored it.** +The rule ("any id in a TechDef.unlocks is locked until that tech completes") turned three +rounds of my tech authoring from decoration into a lock on everything. Measured: + +``` +TICK 0: melt producible? FALSE <- first-taste, the opening commission, impossible + every science pack? FALSE +RESEARCH CLOSURE: 0 of 24 techs reachable +``` + +`disc-artifact-bottling` cost 10 analog packs and unlocked the only machine that can bottle +an analog pack. The M1 melt chain itself was gated. The game opened on an unbuildable +factory, an unfillable commission, and no way to research out of it. **This is the same +class of bug as round 2's decorative heat: a number/structure that was fine until another +lane made it load-bearing.** Fixed to a shape, not a patch — two rules, both now tests: +1. the M1 melt chain is starting equipment; 2. one pack (SPATIAL) is craftable at tick 0. +Result: **23/23 reachable**, order tells a story, only the four forward-canon items remain +unreachable. Arithmetic + the worked cost path in CANON-DELTA. + +DECISIONS: +- **SPATIAL PACK is the bootstrap science** — bricks + halos + slurry all fall out of the + quantizer the player already owns for the melt chain. Your first research is a study of + your own damage, which is the most in-voice thing in the tree, and it needs no machine + the M1 chain didn't already require (bar the bottler). 10 packs/min on one quantizer; + the first four nodes cost 0.5–1.5 min each. Affordable off a first-taste-era factory, as + ordered, with the working shown. +- **Six disc nodes deleted, not repurposed** (`disc-artifact-bottling`, + `-chroma-subsampling`, `-macroblock-quarrying`, `-ringing-skim`, `-subsampling-420`, + `-mosh-reactor`) — their content became starting equipment, so a repurposed node would + have been a fake. Grepped first: no lane references specific tech ids. Per the + deprecation rule, logged here and in CANON-DELTA. +- **The cooler's spec'd 0.02 was catastrophic once real** — the decoder banks 0.0033, so a + 0.02 aura is net −0.0167: it never heats, and a 2×2 cooler makes a 2×2 decoder a + permanent free 100/s. Retuned to 0.0018, which is sane under **both** readings of the + contract's ambiguous "this machine's coolPerTick *also* applies" (additive → 85% duty, + 72/s avg; replacing → 55%, 46/s). I deliberately picked a number that survives either, + because SIM hasn't built it yet and I won't rest the design on my guess of a sentence. + Pairing is 9.64/s per tile vs 5.00 for ASICs — the best power in the game, paid for with + a tech, two machines and the floor space to keep them adjacent. +- §10 judgement calls (footage = anchor slab; the matte bath's chemical = static canister, + which §3 already calls "brine for preserving other artifacts"; ghost feather made only by + the neural keyer so the earlier machines exist to ruin it) — all argued in CANON-DELTA + the way you asked, from what the thing already is rather than convenience. +- NEURAL KEYER `powerDraw: 240` — §10's "24 VRAM ... dwarfs everything else"; that's 12 + ASICs, against an entire M1 chain that draws 96. "You don't unlock the neural keyer; you + unlock the power plant it demands" is now literally true. + +BLOCKED/BROKEN — **I broke two of SIM's tests, deliberately, and their guard caught it:** +- `research.test.ts:247` does `expect(gated.has('mosh-reactor')).toBe(true) // guards the + premise below`. I ungated the mosh reactor, because gating it makes first-taste + impossible. **Their premise guard did exactly its job** — it failed loudly instead of + silently testing nothing. The fix is one word in their file (not mine to touch): swap + `mosh-reactor` for any still-gated machine. Drop-in list: mv-extractor, + bloom-duplicator, field-loom, telecine-puller, hex-splicer, software-decoder, + asic-cooler, matte-bath, optical-printer, chroma-keyer, roto-bench, neural-keyer, muxer. + `demuxer` remains a valid "free" example. +- The other 8 failures in `research.test.ts` / `live.test.ts` are **not mine** — they fail + identically against HEAD's round-3 data (measured by restoring the old JSON and + re-running; failure sets diffed). + +CROSS-LANE — SIM, the reference factory now can't power itself: +- It places **4 software decoders**, which my tree gates behind `stream-software-decoding` + (rounds 3 and 4 both). With your gating live, they can't be placed at tick 0, so the M1 + demo has no power and the HUD's heat tests have no decoder to watch. That's the cause of + several of those 8. +- **The starter plant is ASICs**: net demand 78/s ÷ 20 = **4 decode-asics minimum** (5 for + headroom). Ungated, boring, correct — the software decoder is a mid-game *upgrade*, which + is the progression the tech tree is for. My round-3 suggestion (3 ASIC + 1 decoder + tank) + is still the right *post-research* demo, and the decoder+cooler pairing is the M2 showpiece. +- Tell me if you'd rather software-decoder be starting equipment and I'll ungate it — but + at 25.8/s average vs the ASIC's 20 it would make the ASIC pointless from tick 0. + +CONTRACT REQUEST: none. (Still not requesting `kind: 'cooler'` — `kind: 'power'` + +`coolRadius` covers it, and the aura isn't built yet.) + +STILL OPEN: **SIM hasn't exported the tuning constants yet** (I checked — `grep 'export +const' src/sim/*.ts` finds only `geom.ts`). My heat tests still hand-mirror 0.7 / 0.5 / +0.004 / 30tps and still go stale silently if you retune. I'll retire the copies the round +the export lands. + +NEXT (candidates): SPILL FOG + EDGE BOIL (§10 hazards — need a mechanic, so data waits on +SIM); mosquito traps; a seam/ore map (extractors still mine anywhere — the content gap I +own); `cinema-pack` + the Fortress; `corridorkey-mrp-mlx` is a real MODELBEAST artifact +factory for the whole §10 asset set (§10's production note) — worth a work order.